feat: add Opportunities Views dropdown (#1503)
* feat: add Opportunities Views dropdown Closes #1454 * feat: persist Opportunities view filters and sorts Closes #1456 * feat: create/edit/delete Opportunities views Closes #1455, Closes #1457 * fix: add missing Opportunities view mock --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
EntityBoard,
|
||||||
|
type EntityBoardProps,
|
||||||
|
} from '@/ui/board/components/EntityBoard';
|
||||||
|
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
|
||||||
|
import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu';
|
||||||
|
import { useBoardViews } from '@/views/hooks/useBoardViews';
|
||||||
|
|
||||||
|
import { HooksCompanyBoard } from '../../components/HooksCompanyBoard';
|
||||||
|
import { CompanyBoardRecoilScopeContext } from '../../states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
||||||
|
|
||||||
|
type OwnProps = Pick<
|
||||||
|
EntityBoardProps,
|
||||||
|
'boardOptions' | 'onColumnAdd' | 'onColumnDelete' | 'onEditColumnTitle'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const CompanyBoard = ({ boardOptions, ...props }: OwnProps) => {
|
||||||
|
const { handleViewsChange, handleViewSubmit } = useBoardViews({
|
||||||
|
availableFilters: boardOptions.filters,
|
||||||
|
availableSorts: boardOptions.sorts,
|
||||||
|
objectId: 'company',
|
||||||
|
scopeContext: CompanyBoardRecoilScopeContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HooksCompanyBoard />
|
||||||
|
<EntityBoard
|
||||||
|
boardOptions={boardOptions}
|
||||||
|
defaultViewName="All opportunities"
|
||||||
|
onViewsChange={handleViewsChange}
|
||||||
|
onViewSubmit={handleViewSubmit}
|
||||||
|
scopeContext={CompanyBoardRecoilScopeContext}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<EntityBoardActionBar />
|
||||||
|
<EntityBoardContextMenu />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,50 +1,52 @@
|
|||||||
import type { ComponentProps, Context, ReactNode } from 'react';
|
import { type ComponentProps, useCallback } from 'react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||||
import { TopBar } from '@/ui/top-bar/TopBar';
|
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
import { FilterDropdownButton } from '@/ui/view-bar/components/FilterDropdownButton';
|
import { ViewBar, type ViewBarProps } from '@/ui/view-bar/components/ViewBar';
|
||||||
import { SortDropdownButton } from '@/ui/view-bar/components/SortDropdownButton';
|
|
||||||
import ViewBarDetails from '@/ui/view-bar/components/ViewBarDetails';
|
|
||||||
import { FiltersHotkeyScope } from '@/ui/view-bar/types/FiltersHotkeyScope';
|
|
||||||
import { SortType } from '@/ui/view-bar/types/interface';
|
|
||||||
|
|
||||||
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||||
|
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
||||||
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
|
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
|
||||||
|
|
||||||
import { BoardOptionsDropdown } from './BoardOptionsDropdown';
|
import { BoardOptionsDropdown } from './BoardOptionsDropdown';
|
||||||
|
|
||||||
type OwnProps<SortField> = ComponentProps<'div'> & {
|
export type BoardHeaderProps<SortField> = ComponentProps<'div'> & {
|
||||||
viewName: string;
|
|
||||||
viewIcon?: ReactNode;
|
|
||||||
availableSorts?: Array<SortType<SortField>>;
|
|
||||||
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||||
context: Context<string | null>;
|
} & Pick<
|
||||||
};
|
ViewBarProps<SortField>,
|
||||||
|
| 'availableSorts'
|
||||||
const StyledIcon = styled.div`
|
| 'defaultViewName'
|
||||||
display: flex;
|
| 'onViewsChange'
|
||||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
| 'onViewSubmit'
|
||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
| 'scopeContext'
|
||||||
|
>;
|
||||||
& > svg {
|
|
||||||
font-size: ${({ theme }) => theme.icon.size.sm};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function BoardHeader<SortField>({
|
export function BoardHeader<SortField>({
|
||||||
viewName,
|
|
||||||
viewIcon,
|
|
||||||
availableSorts,
|
|
||||||
onStageAdd,
|
onStageAdd,
|
||||||
context,
|
onViewsChange,
|
||||||
|
scopeContext,
|
||||||
...props
|
...props
|
||||||
}: OwnProps<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}>
|
||||||
<TopBar
|
<ViewBar
|
||||||
{...props}
|
{...props}
|
||||||
|
onViewsChange={onViewsChange}
|
||||||
|
optionsDropdownKey={BoardOptionsDropdownKey}
|
||||||
|
OptionsDropdownButton={OptionsDropdownButton}
|
||||||
|
scopeContext={scopeContext}
|
||||||
displayBottomBorder={false}
|
displayBottomBorder={false}
|
||||||
leftComponent={
|
leftComponent={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,28 +1,29 @@
|
|||||||
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
||||||
import type { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
|
||||||
|
|
||||||
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
|
||||||
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
||||||
|
|
||||||
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
|
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
|
||||||
import { BoardOptionsDropdownContent } from './BoardOptionsDropdownContent';
|
import {
|
||||||
|
BoardOptionsDropdownContent,
|
||||||
|
type BoardOptionsDropdownContentProps,
|
||||||
|
} from './BoardOptionsDropdownContent';
|
||||||
|
|
||||||
type BoardOptionsDropdownProps = {
|
type BoardOptionsDropdownProps = Pick<
|
||||||
customHotkeyScope: HotkeyScope;
|
BoardOptionsDropdownContentProps,
|
||||||
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
'customHotkeyScope' | 'onStageAdd' | 'onViewsChange' | 'scopeContext'
|
||||||
};
|
>;
|
||||||
|
|
||||||
export function BoardOptionsDropdown({
|
export function BoardOptionsDropdown({
|
||||||
customHotkeyScope,
|
customHotkeyScope,
|
||||||
onStageAdd,
|
...props
|
||||||
}: BoardOptionsDropdownProps) {
|
}: BoardOptionsDropdownProps) {
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
buttonComponents={<BoardOptionsDropdownButton />}
|
buttonComponents={<BoardOptionsDropdownButton />}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<BoardOptionsDropdownContent
|
<BoardOptionsDropdownContent
|
||||||
|
{...props}
|
||||||
customHotkeyScope={customHotkeyScope}
|
customHotkeyScope={customHotkeyScope}
|
||||||
onStageAdd={onStageAdd}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
dropdownHotkeyScope={customHotkeyScope}
|
dropdownHotkeyScope={customHotkeyScope}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { 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 } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -22,14 +22,21 @@ 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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
|
import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView';
|
||||||
|
import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector';
|
||||||
|
import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState';
|
||||||
|
import type { View } from '@/ui/view-bar/types/View';
|
||||||
|
|
||||||
import { boardColumnsState } from '../states/boardColumnsState';
|
import { boardColumnsState } from '../states/boardColumnsState';
|
||||||
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||||
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
||||||
|
|
||||||
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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledIconSettings = styled(IconSettings)`
|
const StyledIconSettings = styled(IconSettings)`
|
||||||
@ -51,10 +58,13 @@ type ColumnForCreate = {
|
|||||||
export function BoardOptionsDropdownContent({
|
export function BoardOptionsDropdownContent({
|
||||||
customHotkeyScope,
|
customHotkeyScope,
|
||||||
onStageAdd,
|
onStageAdd,
|
||||||
|
onViewsChange,
|
||||||
|
scopeContext,
|
||||||
}: BoardOptionsDropdownContentProps) {
|
}: BoardOptionsDropdownContentProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const stageInputRef = useRef<HTMLInputElement>(null);
|
const stageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const viewEditInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [currentMenu, setCurrentMenu] = useState<
|
const [currentMenu, setCurrentMenu] = useState<
|
||||||
BoardOptionsMenu | undefined
|
BoardOptionsMenu | undefined
|
||||||
@ -62,7 +72,8 @@ export function BoardOptionsDropdownContent({
|
|||||||
|
|
||||||
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
|
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||||
|
|
||||||
const resetMenu = () => setCurrentMenu(undefined);
|
const viewsById = useRecoilScopedValue(viewsByIdScopedSelector, scopeContext);
|
||||||
|
const viewEditMode = useRecoilValue(viewEditModeState);
|
||||||
|
|
||||||
const handleStageSubmit = () => {
|
const handleStageSubmit = () => {
|
||||||
if (
|
if (
|
||||||
@ -85,6 +96,23 @@ export function BoardOptionsDropdownContent({
|
|||||||
onStageAdd?.(columnToCreate);
|
onStageAdd?.(columnToCreate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { upsertView } = useUpsertView({
|
||||||
|
onViewsChange,
|
||||||
|
scopeContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleViewNameSubmit = async () => {
|
||||||
|
const name = viewEditInputRef.current?.value;
|
||||||
|
await upsertView(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetMenu = () => setCurrentMenu(undefined);
|
||||||
|
|
||||||
|
const handleMenuNavigate = (menu: BoardOptionsMenu) => {
|
||||||
|
handleViewNameSubmit();
|
||||||
|
setCurrentMenu(menu);
|
||||||
|
};
|
||||||
|
|
||||||
const { closeDropdownButton } = useDropdownButton({
|
const { closeDropdownButton } = useDropdownButton({
|
||||||
key: BoardOptionsDropdownKey,
|
key: BoardOptionsDropdownKey,
|
||||||
});
|
});
|
||||||
@ -101,6 +129,7 @@ export function BoardOptionsDropdownContent({
|
|||||||
Key.Enter,
|
Key.Enter,
|
||||||
() => {
|
() => {
|
||||||
handleStageSubmit();
|
handleStageSubmit();
|
||||||
|
handleViewNameSubmit();
|
||||||
closeDropdownButton();
|
closeDropdownButton();
|
||||||
},
|
},
|
||||||
customHotkeyScope.scope,
|
customHotkeyScope.scope,
|
||||||
@ -110,14 +139,29 @@ export function BoardOptionsDropdownContent({
|
|||||||
<StyledDropdownMenu>
|
<StyledDropdownMenu>
|
||||||
{!currentMenu && (
|
{!currentMenu && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader>
|
{!!viewEditMode.mode ? (
|
||||||
<StyledIconSettings size={theme.icon.size.md} />
|
<DropdownMenuInput
|
||||||
Settings
|
ref={viewEditInputRef}
|
||||||
</DropdownMenuHeader>
|
autoFocus
|
||||||
|
placeholder={
|
||||||
|
viewEditMode.mode === 'create' ? 'New view' : 'View name'
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
viewEditMode.viewId
|
||||||
|
? viewsById[viewEditMode.viewId]?.name
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuHeader>
|
||||||
|
<StyledIconSettings size={theme.icon.size.md} />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
)}
|
||||||
<StyledDropdownMenuSeparator />
|
<StyledDropdownMenuSeparator />
|
||||||
<StyledDropdownMenuItemsContainer>
|
<StyledDropdownMenuItemsContainer>
|
||||||
<MenuItemNavigate
|
<MenuItemNavigate
|
||||||
onClick={() => setCurrentMenu(BoardOptionsMenu.Stages)}
|
onClick={() => handleMenuNavigate(BoardOptionsMenu.Stages)}
|
||||||
LeftIcon={IconLayoutKanban}
|
LeftIcon={IconLayoutKanban}
|
||||||
text="Stages"
|
text="Stages"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { type Context, useCallback, useRef } from 'react';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
|
||||||
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 { BoardHeader } from '@/ui/board/components/BoardHeader';
|
import {
|
||||||
|
BoardHeader,
|
||||||
|
type 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 { IconList } from '@/ui/icon';
|
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
@ -22,6 +22,7 @@ import {
|
|||||||
PipelineStage,
|
PipelineStage,
|
||||||
useUpdateOnePipelineProgressStageMutation,
|
useUpdateOnePipelineProgressStageMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
import { PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By } from '~/generated/graphql';
|
||||||
|
|
||||||
import { useCurrentCardSelected } from '../hooks/useCurrentCardSelected';
|
import { useCurrentCardSelected } from '../hooks/useCurrentCardSelected';
|
||||||
import { useSetCardSelected } from '../hooks/useSetCardSelected';
|
import { useSetCardSelected } from '../hooks/useSetCardSelected';
|
||||||
@ -33,6 +34,17 @@ import { BoardOptions } from '../types/BoardOptions';
|
|||||||
|
|
||||||
import { EntityBoardColumn } from './EntityBoardColumn';
|
import { EntityBoardColumn } from './EntityBoardColumn';
|
||||||
|
|
||||||
|
export type EntityBoardProps = {
|
||||||
|
boardOptions: BoardOptions;
|
||||||
|
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||||
|
onColumnDelete?: (boardColumnId: string) => void;
|
||||||
|
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
|
||||||
|
scopeContext: Context<string | null>;
|
||||||
|
} & Pick<
|
||||||
|
BoardHeaderProps<PipelineProgresses_Order_By>,
|
||||||
|
'defaultViewName' | 'onViewsChange' | 'onViewSubmit'
|
||||||
|
>;
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -46,19 +58,17 @@ const StyledBoardHeader = styled(BoardHeader)`
|
|||||||
|
|
||||||
export function EntityBoard({
|
export function EntityBoard({
|
||||||
boardOptions,
|
boardOptions,
|
||||||
|
defaultViewName,
|
||||||
onColumnAdd,
|
onColumnAdd,
|
||||||
onColumnDelete,
|
onColumnDelete,
|
||||||
onEditColumnTitle,
|
onEditColumnTitle,
|
||||||
}: {
|
onViewsChange,
|
||||||
boardOptions: BoardOptions;
|
onViewSubmit,
|
||||||
onColumnAdd?: (boardColumn: BoardColumnDefinition) => void;
|
scopeContext,
|
||||||
onColumnDelete?: (boardColumnId: string) => void;
|
}: EntityBoardProps) {
|
||||||
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
|
|
||||||
}) {
|
|
||||||
const [boardColumns] = useRecoilState(boardColumnsState);
|
const [boardColumns] = useRecoilState(boardColumnsState);
|
||||||
const setCardSelected = useSetCardSelected();
|
const setCardSelected = useSetCardSelected();
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const [updatePipelineProgressStage] =
|
const [updatePipelineProgressStage] =
|
||||||
useUpdateOnePipelineProgressStageMutation();
|
useUpdateOnePipelineProgressStageMutation();
|
||||||
|
|
||||||
@ -131,11 +141,12 @@ export function EntityBoard({
|
|||||||
return (boardColumns?.length ?? 0) > 0 ? (
|
return (boardColumns?.length ?? 0) > 0 ? (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<StyledBoardHeader
|
<StyledBoardHeader
|
||||||
viewName="All opportunities"
|
defaultViewName={defaultViewName}
|
||||||
viewIcon={<IconList size={theme.icon.size.md} />}
|
|
||||||
availableSorts={boardOptions.sorts}
|
availableSorts={boardOptions.sorts}
|
||||||
onStageAdd={onColumnAdd}
|
onStageAdd={onColumnAdd}
|
||||||
context={CompanyBoardRecoilScopeContext}
|
onViewsChange={onViewsChange}
|
||||||
|
onViewSubmit={onViewSubmit}
|
||||||
|
scopeContext={scopeContext}
|
||||||
/>
|
/>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
<StyledBoard ref={boardRef}>
|
<StyledBoard ref={boardRef}>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { type FormEvent, useCallback, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
|
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
|
||||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||||
@ -11,22 +10,14 @@ import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDrop
|
|||||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
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 { tableColumnsScopedState } from '@/ui/table/states/tableColumnsScopedState';
|
|
||||||
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 { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
|
import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView';
|
||||||
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
|
|
||||||
import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState';
|
|
||||||
import { savedSortsFamilyState } from '@/ui/view-bar/states/savedSortsFamilyState';
|
|
||||||
import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector';
|
import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector';
|
||||||
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 type { View } from '@/ui/view-bar/types/View';
|
import type { View } from '@/ui/view-bar/types/View';
|
||||||
|
|
||||||
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 { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
|
import { TableOptionsDropdownKey } from '../../types/TableOptionsDropdownKey';
|
||||||
@ -35,31 +26,27 @@ import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
|
|||||||
import { TableOptionsDropdownColumnVisibility } from './TableOptionsDropdownSection';
|
import { TableOptionsDropdownColumnVisibility } from './TableOptionsDropdownSection';
|
||||||
|
|
||||||
type TableOptionsDropdownButtonProps = {
|
type TableOptionsDropdownButtonProps = {
|
||||||
onViewsChange?: (views: View[]) => void;
|
onViewsChange?: (views: View[]) => void | Promise<void>;
|
||||||
onImport?: () => void;
|
onImport?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Option {
|
type TableOptionsMenu = 'properties';
|
||||||
Properties = 'Properties',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableOptionsDropdownContent({
|
export function TableOptionsDropdownContent({
|
||||||
onViewsChange,
|
onViewsChange,
|
||||||
onImport,
|
onImport,
|
||||||
}: TableOptionsDropdownButtonProps) {
|
}: TableOptionsDropdownButtonProps) {
|
||||||
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
|
|
||||||
|
|
||||||
const { closeDropdownButton } = useDropdownButton({
|
const { closeDropdownButton } = useDropdownButton({
|
||||||
key: TableOptionsDropdownKey,
|
key: TableOptionsDropdownKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
|
const [selectedMenu, setSelectedMenu] = useState<
|
||||||
undefined,
|
TableOptionsMenu | undefined
|
||||||
);
|
>(undefined);
|
||||||
|
|
||||||
const viewEditInputRef = useRef<HTMLInputElement>(null);
|
const viewEditInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState);
|
const viewEditMode = useRecoilValue(viewEditModeState);
|
||||||
const visibleTableColumns = useRecoilScopedValue(
|
const visibleTableColumns = useRecoilScopedValue(
|
||||||
visibleTableColumnsScopedSelector,
|
visibleTableColumnsScopedSelector,
|
||||||
TableRecoilScopeContext,
|
TableRecoilScopeContext,
|
||||||
@ -73,83 +60,22 @@ export function TableOptionsDropdownContent({
|
|||||||
TableRecoilScopeContext,
|
TableRecoilScopeContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetViewEditMode = useCallback(() => {
|
const { upsertView } = useUpsertView({
|
||||||
setViewEditMode({ mode: undefined, viewId: undefined });
|
onViewsChange,
|
||||||
|
scopeContext: TableRecoilScopeContext,
|
||||||
|
});
|
||||||
|
|
||||||
if (viewEditInputRef.current) {
|
const handleViewNameSubmit = async () => {
|
||||||
viewEditInputRef.current.value = '';
|
const name = viewEditInputRef.current?.value;
|
||||||
}
|
await upsertView(name);
|
||||||
}, [setViewEditMode]);
|
};
|
||||||
|
|
||||||
const handleViewNameSubmit = useRecoilCallback(
|
const handleSelectMenu = (option: TableOptionsMenu) => {
|
||||||
({ set, snapshot }) =>
|
handleViewNameSubmit();
|
||||||
async (event?: FormEvent) => {
|
setSelectedMenu(option);
|
||||||
event?.preventDefault();
|
};
|
||||||
|
|
||||||
const name = viewEditInputRef.current?.value;
|
const resetMenu = () => setSelectedMenu(undefined);
|
||||||
|
|
||||||
if (!viewEditMode.mode || !name) {
|
|
||||||
return resetViewEditMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = await snapshot.getPromise(viewsScopedState(tableScopeId));
|
|
||||||
|
|
||||||
if (viewEditMode.mode === 'create') {
|
|
||||||
const viewToCreate = { id: v4(), name };
|
|
||||||
const nextViews = [...views, viewToCreate];
|
|
||||||
|
|
||||||
const currentColumns = await snapshot.getPromise(
|
|
||||||
tableColumnsScopedState(tableScopeId),
|
|
||||||
);
|
|
||||||
set(savedTableColumnsFamilyState(viewToCreate.id), currentColumns);
|
|
||||||
|
|
||||||
const selectedFilters = await snapshot.getPromise(
|
|
||||||
filtersScopedState(tableScopeId),
|
|
||||||
);
|
|
||||||
set(savedFiltersFamilyState(viewToCreate.id), selectedFilters);
|
|
||||||
|
|
||||||
const selectedSorts = await snapshot.getPromise(
|
|
||||||
sortsScopedState(tableScopeId),
|
|
||||||
);
|
|
||||||
set(savedSortsFamilyState(viewToCreate.id), selectedSorts);
|
|
||||||
|
|
||||||
set(viewsScopedState(tableScopeId), nextViews);
|
|
||||||
await Promise.resolve(onViewsChange?.(nextViews));
|
|
||||||
|
|
||||||
set(currentViewIdScopedState(tableScopeId), viewToCreate.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewEditMode.mode === 'edit') {
|
|
||||||
const nextViews = views.map((view) =>
|
|
||||||
view.id === viewEditMode.viewId ? { ...view, name } : view,
|
|
||||||
);
|
|
||||||
|
|
||||||
set(viewsScopedState(tableScopeId), nextViews);
|
|
||||||
await Promise.resolve(onViewsChange?.(nextViews));
|
|
||||||
}
|
|
||||||
|
|
||||||
return resetViewEditMode();
|
|
||||||
},
|
|
||||||
[
|
|
||||||
onViewsChange,
|
|
||||||
resetViewEditMode,
|
|
||||||
tableScopeId,
|
|
||||||
viewEditMode.mode,
|
|
||||||
viewEditMode.viewId,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectOption = useCallback(
|
|
||||||
(option: Option) => {
|
|
||||||
handleViewNameSubmit();
|
|
||||||
setSelectedOption(option);
|
|
||||||
},
|
|
||||||
[handleViewNameSubmit],
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetSelectedOption = useCallback(() => {
|
|
||||||
setSelectedOption(undefined);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
Key.Escape,
|
||||||
@ -163,7 +89,7 @@ export function TableOptionsDropdownContent({
|
|||||||
Key.Enter,
|
Key.Enter,
|
||||||
() => {
|
() => {
|
||||||
handleViewNameSubmit();
|
handleViewNameSubmit();
|
||||||
resetSelectedOption();
|
resetMenu();
|
||||||
closeDropdownButton();
|
closeDropdownButton();
|
||||||
},
|
},
|
||||||
TableOptionsHotkeyScope.Dropdown,
|
TableOptionsHotkeyScope.Dropdown,
|
||||||
@ -171,7 +97,7 @@ export function TableOptionsDropdownContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDropdownMenu>
|
<StyledDropdownMenu>
|
||||||
{!selectedOption && (
|
{!selectedMenu && (
|
||||||
<>
|
<>
|
||||||
{!!viewEditMode.mode ? (
|
{!!viewEditMode.mode ? (
|
||||||
<DropdownMenuInput
|
<DropdownMenuInput
|
||||||
@ -192,7 +118,7 @@ export function TableOptionsDropdownContent({
|
|||||||
<StyledDropdownMenuSeparator />
|
<StyledDropdownMenuSeparator />
|
||||||
<StyledDropdownMenuItemsContainer>
|
<StyledDropdownMenuItemsContainer>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => handleSelectOption(Option.Properties)}
|
onClick={() => handleSelectMenu('properties')}
|
||||||
LeftIcon={IconTag}
|
LeftIcon={IconTag}
|
||||||
text="Properties"
|
text="Properties"
|
||||||
/>
|
/>
|
||||||
@ -206,12 +132,9 @@ export function TableOptionsDropdownContent({
|
|||||||
</StyledDropdownMenuItemsContainer>
|
</StyledDropdownMenuItemsContainer>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{selectedOption === Option.Properties && (
|
{selectedMenu === 'properties' && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader
|
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||||
StartIcon={IconChevronLeft}
|
|
||||||
onClick={resetSelectedOption}
|
|
||||||
>
|
|
||||||
Properties
|
Properties
|
||||||
</DropdownMenuHeader>
|
</DropdownMenuHeader>
|
||||||
<StyledDropdownMenuSeparator />
|
<StyledDropdownMenuSeparator />
|
||||||
|
|||||||
85
front/src/modules/ui/view-bar/hooks/useUpsertView.ts
Normal file
85
front/src/modules/ui/view-bar/hooks/useUpsertView.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Context, useCallback } from 'react';
|
||||||
|
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
|
|
||||||
|
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||||
|
import { filtersScopedState } from '../states/filtersScopedState';
|
||||||
|
import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState';
|
||||||
|
import { savedSortsFamilyState } from '../states/savedSortsFamilyState';
|
||||||
|
import { sortsScopedState } from '../states/sortsScopedState';
|
||||||
|
import { viewEditModeState } from '../states/viewEditModeState';
|
||||||
|
import { viewsScopedState } from '../states/viewsScopedState';
|
||||||
|
import type { View } from '../types/View';
|
||||||
|
|
||||||
|
export const useUpsertView = ({
|
||||||
|
onViewsChange,
|
||||||
|
scopeContext,
|
||||||
|
}: {
|
||||||
|
onViewsChange?: (views: View[]) => void | Promise<void>;
|
||||||
|
scopeContext: Context<string | null>;
|
||||||
|
}) => {
|
||||||
|
const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
|
||||||
|
const sorts = useRecoilScopedValue(sortsScopedState, scopeContext);
|
||||||
|
|
||||||
|
const [, setCurrentViewId] = useRecoilScopedState(
|
||||||
|
currentViewIdScopedState,
|
||||||
|
scopeContext,
|
||||||
|
);
|
||||||
|
const [views, setViews] = useRecoilScopedState(
|
||||||
|
viewsScopedState,
|
||||||
|
scopeContext,
|
||||||
|
);
|
||||||
|
const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState);
|
||||||
|
|
||||||
|
const resetViewEditMode = useCallback(
|
||||||
|
() => setViewEditMode({ mode: undefined, viewId: undefined }),
|
||||||
|
[setViewEditMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const upsertView = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
async (name?: string) => {
|
||||||
|
if (!viewEditMode.mode || !name) return resetViewEditMode();
|
||||||
|
|
||||||
|
if (viewEditMode.mode === 'create') {
|
||||||
|
const viewToCreate = { id: v4(), name };
|
||||||
|
const nextViews = [...views, viewToCreate];
|
||||||
|
|
||||||
|
set(savedFiltersFamilyState(viewToCreate.id), filters);
|
||||||
|
set(savedSortsFamilyState(viewToCreate.id), sorts);
|
||||||
|
|
||||||
|
setViews(nextViews);
|
||||||
|
await onViewsChange?.(nextViews);
|
||||||
|
|
||||||
|
setCurrentViewId(viewToCreate.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewEditMode.mode === 'edit') {
|
||||||
|
const nextViews = views.map((view) =>
|
||||||
|
view.id === viewEditMode.viewId ? { ...view, name } : view,
|
||||||
|
);
|
||||||
|
|
||||||
|
setViews(nextViews);
|
||||||
|
await onViewsChange?.(nextViews);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resetViewEditMode();
|
||||||
|
},
|
||||||
|
[
|
||||||
|
filters,
|
||||||
|
onViewsChange,
|
||||||
|
resetViewEditMode,
|
||||||
|
setCurrentViewId,
|
||||||
|
setViews,
|
||||||
|
sorts,
|
||||||
|
viewEditMode.mode,
|
||||||
|
viewEditMode.viewId,
|
||||||
|
views,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { upsertView };
|
||||||
|
};
|
||||||
56
front/src/modules/views/hooks/useBoardViews.ts
Normal file
56
front/src/modules/views/hooks/useBoardViews.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { type Context } from 'react';
|
||||||
|
|
||||||
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
|
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
|
||||||
|
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
|
||||||
|
import type { FilterDefinitionByEntity } from '@/ui/view-bar/types/FilterDefinitionByEntity';
|
||||||
|
import type { SortType } from '@/ui/view-bar/types/interface';
|
||||||
|
import { ViewType } from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { useViewFilters } from './useViewFilters';
|
||||||
|
import { useViews } from './useViews';
|
||||||
|
import { useViewSorts } from './useViewSorts';
|
||||||
|
|
||||||
|
export const useBoardViews = <Entity, SortField>({
|
||||||
|
availableFilters,
|
||||||
|
availableSorts,
|
||||||
|
objectId,
|
||||||
|
scopeContext,
|
||||||
|
}: {
|
||||||
|
availableFilters: FilterDefinitionByEntity<Entity>[];
|
||||||
|
availableSorts: SortType<SortField>[];
|
||||||
|
objectId: 'company';
|
||||||
|
scopeContext: Context<string | null>;
|
||||||
|
}) => {
|
||||||
|
const filters = useRecoilScopedValue(filtersScopedState, scopeContext);
|
||||||
|
const sorts = useRecoilScopedValue(sortsScopedState, scopeContext);
|
||||||
|
|
||||||
|
const { handleViewsChange, isFetchingViews } = useViews({
|
||||||
|
objectId,
|
||||||
|
onViewCreate: handleViewCreate,
|
||||||
|
type: ViewType.Pipeline,
|
||||||
|
scopeContext,
|
||||||
|
});
|
||||||
|
const { createViewFilters, persistFilters } = useViewFilters({
|
||||||
|
availableFilters,
|
||||||
|
scopeContext,
|
||||||
|
skipFetch: isFetchingViews,
|
||||||
|
});
|
||||||
|
const { createViewSorts, persistSorts } = useViewSorts({
|
||||||
|
availableSorts,
|
||||||
|
scopeContext,
|
||||||
|
skipFetch: isFetchingViews,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleViewCreate(viewId: string) {
|
||||||
|
await createViewFilters(filters, viewId);
|
||||||
|
await createViewSorts(sorts, viewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewSubmit = async () => {
|
||||||
|
await persistFilters();
|
||||||
|
await persistSorts();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleViewsChange, handleViewSubmit };
|
||||||
|
};
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
|
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
|
||||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||||
import { tableColumnsScopedState } from '@/ui/table/states/tableColumnsScopedState';
|
import { tableColumnsScopedState } from '@/ui/table/states/tableColumnsScopedState';
|
||||||
@ -41,6 +39,7 @@ export const useTableViews = <Entity, SortField>({
|
|||||||
objectId,
|
objectId,
|
||||||
onViewCreate: handleViewCreate,
|
onViewCreate: handleViewCreate,
|
||||||
type: ViewType.Table,
|
type: ViewType.Table,
|
||||||
|
scopeContext: TableRecoilScopeContext,
|
||||||
});
|
});
|
||||||
const { createViewFields, persistColumns } = useTableViewFields({
|
const { createViewFields, persistColumns } = useTableViewFields({
|
||||||
objectId,
|
objectId,
|
||||||
@ -64,11 +63,11 @@ export const useTableViews = <Entity, SortField>({
|
|||||||
await createViewSorts(sorts, viewId);
|
await createViewSorts(sorts, viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleViewSubmit = useCallback(async () => {
|
const handleViewSubmit = async () => {
|
||||||
await persistColumns();
|
await persistColumns();
|
||||||
await persistFilters();
|
await persistFilters();
|
||||||
await persistSorts();
|
await persistSorts();
|
||||||
}, [persistColumns, persistFilters, persistSorts]);
|
};
|
||||||
|
|
||||||
return { handleViewsChange, handleViewSubmit };
|
return { handleViewsChange, handleViewSubmit };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import type { Context } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
|
||||||
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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
@ -9,7 +9,7 @@ import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamily
|
|||||||
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 { 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 { View } from '@/ui/view-bar/types/View';
|
import type { View } from '@/ui/view-bar/types/View';
|
||||||
import {
|
import {
|
||||||
useCreateViewMutation,
|
useCreateViewMutation,
|
||||||
useDeleteViewMutation,
|
useDeleteViewMutation,
|
||||||
@ -22,24 +22,23 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
|||||||
export const useViews = ({
|
export const useViews = ({
|
||||||
objectId,
|
objectId,
|
||||||
onViewCreate,
|
onViewCreate,
|
||||||
|
scopeContext,
|
||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
objectId: 'company' | 'person';
|
objectId: 'company' | 'person';
|
||||||
onViewCreate: (viewId: string) => Promise<void>;
|
onViewCreate?: (viewId: string) => Promise<void>;
|
||||||
|
scopeContext: Context<string | null>;
|
||||||
type: ViewType;
|
type: ViewType;
|
||||||
}) => {
|
}) => {
|
||||||
const [currentViewId, setCurrentViewId] = useRecoilScopedState(
|
const [currentViewId, setCurrentViewId] = useRecoilScopedState(
|
||||||
currentViewIdScopedState,
|
currentViewIdScopedState,
|
||||||
TableRecoilScopeContext,
|
scopeContext,
|
||||||
);
|
);
|
||||||
const [views, setViews] = useRecoilScopedState(
|
const [views, setViews] = useRecoilScopedState(
|
||||||
viewsScopedState,
|
viewsScopedState,
|
||||||
TableRecoilScopeContext,
|
scopeContext,
|
||||||
);
|
|
||||||
const viewsById = useRecoilScopedValue(
|
|
||||||
viewsByIdScopedSelector,
|
|
||||||
TableRecoilScopeContext,
|
|
||||||
);
|
);
|
||||||
|
const viewsById = useRecoilScopedValue(viewsByIdScopedSelector, scopeContext);
|
||||||
|
|
||||||
const [createViewMutation] = useCreateViewMutation();
|
const [createViewMutation] = useCreateViewMutation();
|
||||||
const [updateViewMutation] = useUpdateViewMutation();
|
const [updateViewMutation] = useUpdateViewMutation();
|
||||||
@ -51,12 +50,12 @@ export const useViews = ({
|
|||||||
data: {
|
data: {
|
||||||
...view,
|
...view,
|
||||||
objectId,
|
objectId,
|
||||||
type: ViewType.Table,
|
type,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.view) await onViewCreate(data.view.id);
|
if (data?.view) await onViewCreate?.(data.view.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateView = (view: View) =>
|
const updateView = (view: View) =>
|
||||||
@ -97,15 +96,22 @@ export const useViews = ({
|
|||||||
|
|
||||||
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
|
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
|
||||||
|
|
||||||
// If there is no current view selected,
|
if (!nextViews.length) return;
|
||||||
// or if the current view cannot be found in the views list (user switched workspaces)
|
|
||||||
if (
|
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
|
||||||
nextViews.length &&
|
|
||||||
(!currentViewId || !nextViews.some((view) => view.id === currentViewId))
|
const currentViewExists = nextViews.some(
|
||||||
) {
|
(view) => view.id === currentViewId,
|
||||||
setCurrentViewId(nextViews[0].id);
|
);
|
||||||
handleResetSavedViews();
|
|
||||||
}
|
if (currentViewExists) return;
|
||||||
|
|
||||||
|
// currentView does not exist in the list = the user has switched workspaces
|
||||||
|
// and currentViewId is outdated.
|
||||||
|
// Select the first view in the list.
|
||||||
|
setCurrentViewId(nextViews[0].id);
|
||||||
|
// Reset outdated view recoil states.
|
||||||
|
handleResetSavedViews();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { HooksCompanyBoard } from '@/companies/components/HooksCompanyBoard';
|
import { CompanyBoard } from '@/companies/board/components/CompanyBoard';
|
||||||
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
||||||
import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton';
|
import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton';
|
||||||
import { usePipelineStages } from '@/pipeline/hooks/usePipelineStages';
|
import { usePipelineStages } from '@/pipeline/hooks/usePipelineStages';
|
||||||
import { EntityBoard } from '@/ui/board/components/EntityBoard';
|
|
||||||
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
|
|
||||||
import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu';
|
|
||||||
import { BoardOptionsContext } from '@/ui/board/contexts/BoardOptionsContext';
|
import { BoardOptionsContext } from '@/ui/board/contexts/BoardOptionsContext';
|
||||||
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||||
import { IconTargetArrow } from '@/ui/icon';
|
import { IconTargetArrow } from '@/ui/icon';
|
||||||
@ -60,16 +57,16 @@ export function Opportunities() {
|
|||||||
</StyledPageHeader>
|
</StyledPageHeader>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<BoardOptionsContext.Provider value={opportunitiesBoardOptions}>
|
<BoardOptionsContext.Provider value={opportunitiesBoardOptions}>
|
||||||
<RecoilScope SpecificContext={CompanyBoardRecoilScopeContext}>
|
<RecoilScope
|
||||||
<HooksCompanyBoard />
|
scopeId="opportunities"
|
||||||
<EntityBoard
|
SpecificContext={CompanyBoardRecoilScopeContext}
|
||||||
|
>
|
||||||
|
<CompanyBoard
|
||||||
boardOptions={opportunitiesBoardOptions}
|
boardOptions={opportunitiesBoardOptions}
|
||||||
onEditColumnTitle={handleEditColumnTitle}
|
|
||||||
onColumnAdd={handlePipelineStageAdd}
|
onColumnAdd={handlePipelineStageAdd}
|
||||||
onColumnDelete={handlePipelineStageDelete}
|
onColumnDelete={handlePipelineStageDelete}
|
||||||
|
onEditColumnTitle={handleEditColumnTitle}
|
||||||
/>
|
/>
|
||||||
<EntityBoardActionBar />
|
|
||||||
<EntityBoardContextMenu />
|
|
||||||
</RecoilScope>
|
</RecoilScope>
|
||||||
</BoardOptionsContext.Provider>
|
</BoardOptionsContext.Provider>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|||||||
@ -26,18 +26,20 @@ import {
|
|||||||
SearchCompanyQuery,
|
SearchCompanyQuery,
|
||||||
SearchPeopleQuery,
|
SearchPeopleQuery,
|
||||||
SearchUserQuery,
|
SearchUserQuery,
|
||||||
|
ViewType,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
import { mockedActivities, mockedTasks } from './mock-data/activities';
|
import { mockedActivities, mockedTasks } from './mock-data/activities';
|
||||||
import {
|
import {
|
||||||
mockedCompaniesData,
|
mockedCompaniesData,
|
||||||
mockedCompanyViewFields,
|
mockedCompanyBoardViews,
|
||||||
mockedCompanyViews,
|
mockedCompanyTableColumns,
|
||||||
|
mockedCompanyTableViews,
|
||||||
} from './mock-data/companies';
|
} from './mock-data/companies';
|
||||||
import {
|
import {
|
||||||
mockedPeopleData,
|
mockedPeopleData,
|
||||||
mockedPersonViewFields,
|
mockedPersonTableColumns,
|
||||||
mockedPersonViews,
|
mockedPersonTableViews,
|
||||||
} from './mock-data/people';
|
} from './mock-data/people';
|
||||||
import { mockedPipelineProgressData } from './mock-data/pipeline-progress';
|
import { mockedPipelineProgressData } from './mock-data/pipeline-progress';
|
||||||
import { mockedPipelinesData } from './mock-data/pipelines';
|
import { mockedPipelinesData } from './mock-data/pipelines';
|
||||||
@ -237,12 +239,18 @@ export const graphqlMocks = [
|
|||||||
const {
|
const {
|
||||||
where: {
|
where: {
|
||||||
objectId: { equals: objectId },
|
objectId: { equals: objectId },
|
||||||
|
type: { equals: type },
|
||||||
},
|
},
|
||||||
} = req.variables;
|
} = req.variables;
|
||||||
|
|
||||||
return res(
|
return res(
|
||||||
ctx.data({
|
ctx.data({
|
||||||
views: objectId === 'company' ? mockedCompanyViews : mockedPersonViews,
|
views:
|
||||||
|
objectId === 'person'
|
||||||
|
? mockedPersonTableViews
|
||||||
|
: type === ViewType.Table
|
||||||
|
? mockedCompanyTableViews
|
||||||
|
: mockedCompanyBoardViews,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@ -256,9 +264,9 @@ export const graphqlMocks = [
|
|||||||
return res(
|
return res(
|
||||||
ctx.data({
|
ctx.data({
|
||||||
viewFields:
|
viewFields:
|
||||||
viewId === mockedCompanyViews[0].id
|
viewId === mockedCompanyTableViews[0].id
|
||||||
? mockedCompanyViewFields
|
? mockedCompanyTableColumns
|
||||||
: mockedPersonViewFields,
|
: mockedPersonTableColumns,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -144,7 +144,17 @@ export const mockedCompaniesData: Array<MockedCompany> = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockedCompanyViews: View[] = [
|
export const mockedCompanyBoardViews: View[] = [
|
||||||
|
{
|
||||||
|
__typename: 'View',
|
||||||
|
id: '1e8f93e6-ae0e-43ba-8121-a7a763286351',
|
||||||
|
name: 'All opportunities',
|
||||||
|
objectId: 'company',
|
||||||
|
type: ViewType.Pipeline,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockedCompanyTableViews: View[] = [
|
||||||
{
|
{
|
||||||
__typename: 'View',
|
__typename: 'View',
|
||||||
id: 'e6a2232d-ca6c-42df-b78e-ca0343f545a9',
|
id: 'e6a2232d-ca6c-42df-b78e-ca0343f545a9',
|
||||||
@ -154,15 +164,16 @@ export const mockedCompanyViews: View[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockedCompanyViewFields = companiesAvailableColumnDefinitions.map<
|
export const mockedCompanyTableColumns =
|
||||||
Omit<ViewField, 'view'>
|
companiesAvailableColumnDefinitions.map<Omit<ViewField, 'view'>>(
|
||||||
>((viewFieldDefinition) => ({
|
(viewFieldDefinition) => ({
|
||||||
__typename: 'ViewField',
|
__typename: 'ViewField',
|
||||||
name: viewFieldDefinition.name,
|
name: viewFieldDefinition.name,
|
||||||
index: viewFieldDefinition.index,
|
index: viewFieldDefinition.index,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
key: viewFieldDefinition.key,
|
key: viewFieldDefinition.key,
|
||||||
objectId: 'company',
|
objectId: 'company',
|
||||||
size: viewFieldDefinition.size,
|
size: viewFieldDefinition.size,
|
||||||
viewId: 'e6a2232d-ca6c-42df-b78e-ca0343f545a9',
|
viewId: mockedCompanyTableViews[0].id,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export const mockedPeopleData: MockedPerson[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockedPersonViews: View[] = [
|
export const mockedPersonTableViews: View[] = [
|
||||||
{
|
{
|
||||||
__typename: 'View',
|
__typename: 'View',
|
||||||
id: 'afd7737a-bf1d-41a3-8863-c277b56a657b',
|
id: 'afd7737a-bf1d-41a3-8863-c277b56a657b',
|
||||||
@ -146,7 +146,7 @@ export const mockedPersonViews: View[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockedPersonViewFields = peopleAvailableColumnDefinitions.map<
|
export const mockedPersonTableColumns = peopleAvailableColumnDefinitions.map<
|
||||||
Omit<ViewField, 'view'>
|
Omit<ViewField, 'view'>
|
||||||
>((viewFieldDefinition) => ({
|
>((viewFieldDefinition) => ({
|
||||||
__typename: 'ViewField',
|
__typename: 'ViewField',
|
||||||
@ -156,5 +156,5 @@ export const mockedPersonViewFields = peopleAvailableColumnDefinitions.map<
|
|||||||
key: viewFieldDefinition.key,
|
key: viewFieldDefinition.key,
|
||||||
objectId: 'person',
|
objectId: 'person',
|
||||||
size: viewFieldDefinition.size,
|
size: viewFieldDefinition.size,
|
||||||
viewId: 'afd7737a-bf1d-41a3-8863-c277b56a657b',
|
viewId: mockedPersonTableViews[0].id,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user