Enable column edition, and fix ordering (#683)
* Enable column edition, and fix ordering * Move queries to services * Add total amounts for board columns * Refactor totals selector as a family * Fix 0-index issue * Lint * Rename selector * Remove useless header * Address PR comments * Optimistically update board column names
This commit is contained in:
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useFilteredSearchCompanyQuery } from '@/companies/services';
|
||||
import { useFilteredSearchCompanyQuery } from '@/companies/queries';
|
||||
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
||||
import { AppHotkeyScope } from '@/lib/hotkeys/types/AppHotkeyScope';
|
||||
import { useFilteredSearchPeopleQuery } from '@/people/services';
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { useHandleCheckableCommentThreadTargetChange } from '@/comments/hooks/useHandleCheckableCommentThreadTargetChange';
|
||||
import { CompanyChip } from '@/companies/components/CompanyChip';
|
||||
import { useFilteredSearchCompanyQuery } from '@/companies/services';
|
||||
import { useFilteredSearchCompanyQuery } from '@/companies/queries';
|
||||
import { usePreviousHotkeyScope } from '@/lib/hotkeys/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
|
||||
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
|
||||
import { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_COMPANIES } from '@/companies/queries';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
import { Button } from '@/ui/components/buttons/Button';
|
||||
import { IconTrash } from '@/ui/icons';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_COMPANIES } from '@/companies/queries';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
import {
|
||||
CommentThread,
|
||||
|
||||
@ -3,7 +3,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_COMPANIES } from '@/companies/queries';
|
||||
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
|
||||
@ -3,7 +3,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_COMPANIES } from '@/companies/queries';
|
||||
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
|
||||
@ -5,7 +5,8 @@ import styled from '@emotion/styled';
|
||||
import { IconCurrencyDollar } from '@tabler/icons-react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/queries';
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/services';
|
||||
import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext';
|
||||
import { pipelineProgressIdScopedState } from '@/pipeline-progress/states/pipelineProgressIdScopedState';
|
||||
import { selectedBoardCardsState } from '@/pipeline-progress/states/selectedBoardCardsState';
|
||||
@ -19,7 +20,6 @@ import {
|
||||
PipelineProgress,
|
||||
useUpdateOnePipelineProgressMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { companyProgressesFamilyState } from '~/pages/opportunities/companyProgressesFamilyState';
|
||||
|
||||
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||
background-color: ${({ theme, selected }) =>
|
||||
|
||||
@ -5,7 +5,7 @@ import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState'
|
||||
import { useRecoilScopedValue } from '@/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { TableContext } from '@/ui/tables/states/TableContext';
|
||||
|
||||
import { useFilteredSearchCompanyQuery } from '../services';
|
||||
import { useFilteredSearchCompanyQuery } from '../queries';
|
||||
|
||||
export function FilterDropdownCompanySearchSelect() {
|
||||
const filterDropdownSearchInput = useRecoilScopedValue(
|
||||
|
||||
@ -4,7 +4,7 @@ import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState'
|
||||
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
||||
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
|
||||
import { useFilteredSearchCompanyQuery } from '../services';
|
||||
import { useFilteredSearchCompanyQuery } from '../queries';
|
||||
|
||||
export function NewCompanyBoardCard() {
|
||||
const [searchFilter] = useRecoilScopedState(
|
||||
|
||||
@ -4,9 +4,10 @@ import { useRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { usePreviousHotkeyScope } from '@/lib/hotkeys/hooks/usePreviousHotkeyScope';
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/queries';
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/services';
|
||||
import { BoardColumnContext } from '@/pipeline-progress/states/BoardColumnContext';
|
||||
import { boardState } from '@/pipeline-progress/states/boardState';
|
||||
import { currentPipelineState } from '@/pipeline-progress/states/currentPipelineState';
|
||||
import { pipelineStageIdScopedState } from '@/pipeline-progress/states/pipelineStageIdScopedState';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
@ -19,9 +20,8 @@ import {
|
||||
PipelineProgressableType,
|
||||
useCreateOnePipelineProgressMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { currentPipelineState } from '~/pages/opportunities/currentPipelineState';
|
||||
|
||||
import { useFilteredSearchCompanyQuery } from '../services';
|
||||
import { useFilteredSearchCompanyQuery } from '../queries';
|
||||
|
||||
export function NewCompanyProgressButton() {
|
||||
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { CompanyProgress } from '@/companies/types/CompanyProgress';
|
||||
|
||||
export const companyProgressesFamilyState = atomFamily<
|
||||
CompanyProgress | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'companyProgressesFamilyState',
|
||||
default: undefined,
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { defaultOrderBy } from '@/companies/services';
|
||||
import { defaultOrderBy } from '@/companies/queries';
|
||||
import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState';
|
||||
import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState';
|
||||
import {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useFilteredSearchCompanyQuery } from '@/companies/services';
|
||||
import { useFilteredSearchCompanyQuery } from '@/companies/queries';
|
||||
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
@ -6,7 +6,7 @@ import { EntityTableActionBarButton } from '@/ui/components/table/action-bar/Ent
|
||||
import { IconTrash } from '@/ui/icons/index';
|
||||
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
|
||||
|
||||
import { GET_PIPELINES } from '../queries';
|
||||
import { GET_PIPELINES } from '../services';
|
||||
import { selectedBoardCardsState } from '../states/selectedBoardCardsState';
|
||||
|
||||
export function BoardActionBarButtonDeletePipelineProgress() {
|
||||
|
||||
@ -62,10 +62,16 @@ export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) {
|
||||
[board, updatePipelineProgressStageInDB, setBoard],
|
||||
);
|
||||
|
||||
const sortedBoard = board
|
||||
? [...board].sort((a, b) => {
|
||||
return a.index - b.index;
|
||||
})
|
||||
: [];
|
||||
|
||||
return (board?.length ?? 0) > 0 ? (
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{board?.map((column) => (
|
||||
{sortedBoard.map((column) => (
|
||||
<RecoilScope
|
||||
SpecificContext={BoardColumnContext}
|
||||
key={column.pipelineStageId}
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Droppable, DroppableProvided } from '@hello-pangea/dnd';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
import { BoardColumn } from '@/ui/board/components/BoardColumn';
|
||||
import { useUpdatePipelineStageMutation } from '~/generated/graphql';
|
||||
|
||||
import { BoardColumnContext } from '../states/BoardColumnContext';
|
||||
import { boardColumnTotalsFamilySelector } from '../states/boardColumnTotalsFamilySelector';
|
||||
import { boardState } from '../states/boardState';
|
||||
import { pipelineStageIdScopedState } from '../states/pipelineStageIdScopedState';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
@ -47,10 +51,14 @@ export function EntityBoardColumn({
|
||||
column: BoardPipelineStageColumn;
|
||||
boardOptions: BoardOptions;
|
||||
}) {
|
||||
const [board, setBoard] = useRecoilState(boardState);
|
||||
const [pipelineStageId, setPipelineStageId] = useRecoilScopedState(
|
||||
pipelineStageIdScopedState,
|
||||
BoardColumnContext,
|
||||
);
|
||||
const boardColumnTotal = useRecoilValue(
|
||||
boardColumnTotalsFamilySelector(column.pipelineStageId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pipelineStageId !== column.pipelineStageId) {
|
||||
@ -58,10 +66,37 @@ export function EntityBoardColumn({
|
||||
}
|
||||
}, [column, setPipelineStageId, pipelineStageId]);
|
||||
|
||||
const [updatePipelineStage] = useUpdatePipelineStageMutation();
|
||||
function handleEditColumnTitle(value: string) {
|
||||
updatePipelineStage({
|
||||
variables: {
|
||||
id: pipelineStageId,
|
||||
name: value,
|
||||
},
|
||||
});
|
||||
setBoard([
|
||||
...(board || []).map((pipelineStage) => {
|
||||
if (pipelineStage.pipelineStageId === pipelineStageId) {
|
||||
return {
|
||||
...pipelineStage,
|
||||
name: value,
|
||||
};
|
||||
}
|
||||
return pipelineStage;
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Droppable droppableId={column.pipelineStageId}>
|
||||
{(droppableProvided) => (
|
||||
<BoardColumn title={`${column.title} `} colorCode={column.colorCode}>
|
||||
<BoardColumn
|
||||
onTitleEdit={handleEditColumnTitle}
|
||||
title={column.title}
|
||||
colorCode={column.colorCode}
|
||||
pipelineStageId={column.pipelineStageId}
|
||||
totalAmount={boardColumnTotal}
|
||||
>
|
||||
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
|
||||
{column.pipelineProgressIds.map((pipelineProgressId, index) => (
|
||||
<RecoilScope
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from './select';
|
||||
export * from './update';
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
|
||||
import { boardState } from './boardState';
|
||||
|
||||
export const boardColumnTotalsFamilySelector = selectorFamily({
|
||||
key: 'BoardColumnTotalsFamily',
|
||||
get:
|
||||
(pipelineStageId: string) =>
|
||||
({ get }) => {
|
||||
const board = get(boardState);
|
||||
const pipelineStage = board?.find(
|
||||
(pipelineStage: BoardPipelineStageColumn) =>
|
||||
pipelineStage.pipelineStageId === pipelineStageId,
|
||||
);
|
||||
|
||||
const pipelineProgresses = pipelineStage?.pipelineProgressIds.map(
|
||||
(pipelineProgressId: string) =>
|
||||
get(companyProgressesFamilyState(pipelineProgressId)),
|
||||
);
|
||||
const pipelineStageTotal: number =
|
||||
pipelineProgresses?.reduce(
|
||||
(acc: number, curr: any) => acc + curr?.pipelineProgress.amount,
|
||||
0,
|
||||
) || 0;
|
||||
|
||||
return pipelineStageTotal;
|
||||
},
|
||||
});
|
||||
1
front/src/modules/pipeline-stages/services/index.ts
Normal file
1
front/src/modules/pipeline-stages/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './update';
|
||||
10
front/src/modules/pipeline-stages/services/update.ts
Normal file
10
front/src/modules/pipeline-stages/services/update.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_PIPELINE_STAGE = gql`
|
||||
mutation UpdatePipelineStage($id: String, $name: String) {
|
||||
updateOnePipelineStage(where: { id: $id }, data: { name: { set: $name } }) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -1,33 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Pipeline } from '~/generated/graphql';
|
||||
|
||||
type OwnProps = {
|
||||
opportunity: Pipeline;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.span`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
:hover {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
`;
|
||||
|
||||
function PipelineChip({ opportunity }: OwnProps) {
|
||||
return (
|
||||
<StyledContainer data-testid="company-chip" key={opportunity.id}>
|
||||
{opportunity.icon && <span>{opportunity.icon}</span>}
|
||||
<span>{opportunity.name}</span>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default PipelineChip;
|
||||
@ -12,6 +12,7 @@ export const StyledBoard = styled.div`
|
||||
export type BoardPipelineStageColumn = {
|
||||
pipelineStageId: string;
|
||||
title: string;
|
||||
index: number;
|
||||
colorCode?: string;
|
||||
pipelineProgressIds: string[];
|
||||
};
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { debounce } from '@/utils/debounce';
|
||||
|
||||
import { EditColumnTitleInput } from './EditColumnTitleInput';
|
||||
|
||||
export const StyledColumn = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
@ -26,21 +30,53 @@ export const StyledColumnTitle = styled.h3`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const StyledAmount = styled.div`
|
||||
const StyledAmount = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
colorCode?: string;
|
||||
title: string;
|
||||
pipelineStageId?: string;
|
||||
onTitleEdit: (title: string) => void;
|
||||
totalAmount?: number;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function BoardColumn({ colorCode, title, children }: OwnProps) {
|
||||
export function BoardColumn({
|
||||
colorCode,
|
||||
title,
|
||||
onTitleEdit,
|
||||
totalAmount,
|
||||
children,
|
||||
}: OwnProps) {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [internalValue, setInternalValue] = React.useState(title);
|
||||
|
||||
function toggleEditMode() {
|
||||
setIsEditing(!isEditing);
|
||||
}
|
||||
|
||||
const debouncedOnUpdate = debounce(onTitleEdit, 200);
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
debouncedOnUpdate(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledColumn>
|
||||
<StyledHeader>
|
||||
<StyledColumnTitle color={colorCode}>• {title}</StyledColumnTitle>
|
||||
<StyledHeader onClick={toggleEditMode}>
|
||||
{isEditing ? (
|
||||
<EditColumnTitleInput
|
||||
color={colorCode}
|
||||
toggleEditMode={toggleEditMode}
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
<StyledColumnTitle color={colorCode}>• {title}</StyledColumnTitle>
|
||||
)}
|
||||
{!!totalAmount && <StyledAmount>${totalAmount}</StyledAmount>}
|
||||
</StyledHeader>
|
||||
{children}
|
||||
</StyledColumn>
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export enum ColumnHotkeyScope {
|
||||
EditColumnName = 'EditColumnNameHotkeyScope',
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
||||
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
|
||||
|
||||
import { ColumnHotkeyScope } from './ColumnHotkeyScope';
|
||||
|
||||
const StyledEditTitleInput = styled.input`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
}
|
||||
color: ${({ color }) => color};
|
||||
&:focus {
|
||||
color: ${({ color }) => color};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight};
|
||||
}
|
||||
margin: 0;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
export function EditColumnTitleInput({
|
||||
color,
|
||||
value,
|
||||
onChange,
|
||||
toggleEditMode,
|
||||
}: {
|
||||
color?: string;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
toggleEditMode: () => void;
|
||||
}) {
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
setHotkeyScope(ColumnHotkeyScope.EditColumnName, { goto: false });
|
||||
|
||||
useScopedHotkeys('enter', toggleEditMode, ColumnHotkeyScope.EditColumnName);
|
||||
useScopedHotkeys('esc', toggleEditMode, ColumnHotkeyScope.EditColumnName);
|
||||
return (
|
||||
<StyledEditTitleInput
|
||||
placeholder={'Enter column name'}
|
||||
color={color}
|
||||
autoFocus
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user