Uniformize folder structure (#693)

* Uniformize folder structure

* Fix icons

* Fix icons

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-16 14:29:28 -07:00
committed by GitHub
parent 900ec5572f
commit 6ced8434bd
462 changed files with 931 additions and 960 deletions

View File

@ -0,0 +1,49 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState } from 'recoil';
import { IconTrash } from '@/ui/icon/index';
import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton';
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
import { GET_PIPELINES } from '../queries';
import { boardState } from '../states/boardState';
import { selectedBoardCardsState } from '../states/selectedBoardCardsState';
export function BoardActionBarButtonDeletePipelineProgress() {
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
selectedBoardCardsState,
);
const [board, setBoard] = useRecoilState(boardState);
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
async function handleDeleteClick() {
setBoard(
board?.map((pipelineStage) => ({
...pipelineStage,
pipelineProgressIds: pipelineStage.pipelineProgressIds.filter(
(pipelineProgressId) =>
!selectedBoardItems.includes(pipelineProgressId),
),
})),
);
setSelectedBoardItems([]);
await deletePipelineProgress({
variables: {
ids: selectedBoardItems,
},
});
}
return (
<EntityTableActionBarButton
label="Delete"
icon={<IconTrash size={16} />}
type="warning"
onClick={handleDeleteClick}
/>
);
}

View File

@ -0,0 +1,87 @@
import { useCallback } from 'react';
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 { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import {
PipelineProgress,
PipelineStage,
useUpdateOnePipelineProgressStageMutation,
} from '~/generated/graphql';
import {
getOptimisticlyUpdatedBoard,
StyledBoard,
} from '../../ui/board/components/Board';
import { BoardColumnContext } from '../states/BoardColumnContext';
import { boardState } from '../states/boardState';
import { BoardOptions } from '../types/BoardOptions';
import { EntityBoardColumn } from './EntityBoardColumn';
export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) {
const [board, setBoard] = useRecoilState(boardState);
const [updatePipelineProgressStage] =
useUpdateOnePipelineProgressStageMutation();
const updatePipelineProgressStageInDB = useCallback(
async (
pipelineProgressId: NonNullable<PipelineProgress['id']>,
pipelineStageId: NonNullable<PipelineStage['id']>,
) => {
updatePipelineProgressStage({
variables: {
id: pipelineProgressId,
pipelineStageId,
},
});
},
[updatePipelineProgressStage],
);
const onDragEnd: OnDragEndResponder = useCallback(
async (result) => {
if (!board) return;
const newBoard = getOptimisticlyUpdatedBoard(board, result);
if (!newBoard) return;
setBoard(newBoard);
try {
const draggedEntityId = result.draggableId;
const destinationColumnId = result.destination?.droppableId;
draggedEntityId &&
destinationColumnId &&
updatePipelineProgressStageInDB &&
(await updatePipelineProgressStageInDB(
draggedEntityId,
destinationColumnId,
));
} catch (e) {
console.error(e);
}
},
[board, updatePipelineProgressStageInDB, setBoard],
);
const sortedBoard = board
? [...board].sort((a, b) => {
return a.index - b.index;
})
: [];
return (board?.length ?? 0) > 0 ? (
<StyledBoard>
<DragDropContext onDragEnd={onDragEnd}>
{sortedBoard.map((column) => (
<RecoilScope
SpecificContext={BoardColumnContext}
key={column.pipelineStageId}
>
<EntityBoardColumn boardOptions={boardOptions} column={column} />
</RecoilScope>
))}
</DragDropContext>
</StyledBoard>
) : (
<></>
);
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { ActionBar } from '@/ui/action-bar/components/ActionBar';
import { selectedBoardCardsState } from '../states/selectedBoardCardsState';
type OwnProps = {
children: React.ReactNode | React.ReactNode[];
};
export function EntityBoardActionBar({ children }: OwnProps) {
const selectedBoardCards = useRecoilValue(selectedBoardCardsState);
return <ActionBar selectedIds={selectedBoardCards}>{children}</ActionBar>;
}

View File

@ -0,0 +1,45 @@
import { useEffect } from 'react';
import { Draggable } from '@hello-pangea/dnd';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { BoardCardContext } from '../states/BoardCardContext';
import { pipelineProgressIdScopedState } from '../states/pipelineProgressIdScopedState';
import { BoardOptions } from '../types/BoardOptions';
export function EntityBoardCard({
boardOptions,
pipelineProgressId,
index,
}: {
boardOptions: BoardOptions;
pipelineProgressId: string;
index: number;
}) {
const [pipelineProgressIdFromRecoil, setPipelineProgressId] =
useRecoilScopedState(pipelineProgressIdScopedState, BoardCardContext);
useEffect(() => {
if (pipelineProgressIdFromRecoil !== pipelineProgressId) {
setPipelineProgressId(pipelineProgressId);
}
}, [pipelineProgressId, setPipelineProgressId, pipelineProgressIdFromRecoil]);
return (
<Draggable
key={pipelineProgressId}
draggableId={pipelineProgressId}
index={index}
>
{(draggableProvided) => (
<div
ref={draggableProvided?.innerRef}
{...draggableProvided?.dragHandleProps}
{...draggableProvided?.draggableProps}
>
{boardOptions.cardComponent}
</div>
)}
</Draggable>
);
}

View File

@ -0,0 +1,111 @@
import { useEffect } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { Droppable, DroppableProvided } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
import { BoardColumn } from '@/ui/board/components/BoardColumn';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { useUpdatePipelineStageMutation } from '~/generated/graphql';
import { GET_PIPELINES } from '../queries';
import { BoardCardContext } from '../states/BoardCardContext';
import { BoardColumnContext } from '../states/BoardColumnContext';
import { boardColumnTotalsFamilySelector } from '../states/boardColumnTotalsFamilySelector';
import { pipelineStageIdScopedState } from '../states/pipelineStageIdScopedState';
import { BoardOptions } from '../types/BoardOptions';
import { EntityBoardCard } from './EntityBoardCard';
const StyledPlaceholder = styled.div`
min-height: 1px;
`;
const StyledNewCardButtonContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(40)};
`;
const BoardColumnCardsContainer = ({
children,
droppableProvided,
}: {
children: React.ReactNode;
droppableProvided: DroppableProvided;
}) => {
return (
<div
ref={droppableProvided?.innerRef}
{...droppableProvided?.droppableProps}
>
{children}
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
</div>
);
};
export function EntityBoardColumn({
column,
boardOptions,
}: {
column: BoardPipelineStageColumn;
boardOptions: BoardOptions;
}) {
const [pipelineStageId, setPipelineStageId] = useRecoilScopedState(
pipelineStageIdScopedState,
BoardColumnContext,
);
const boardColumnTotal = useRecoilValue(
boardColumnTotalsFamilySelector(column.pipelineStageId),
);
useEffect(() => {
if (pipelineStageId !== column.pipelineStageId) {
setPipelineStageId(column.pipelineStageId);
}
}, [column, setPipelineStageId, pipelineStageId]);
const [updatePipelineStage] = useUpdatePipelineStageMutation();
function handleEditColumnTitle(value: string) {
updatePipelineStage({
variables: {
id: pipelineStageId,
name: value,
},
refetchQueries: [getOperationName(GET_PIPELINES) || ''],
});
}
return (
<Droppable droppableId={column.pipelineStageId}>
{(droppableProvided) => (
<BoardColumn
onTitleEdit={handleEditColumnTitle}
title={column.title}
colorCode={column.colorCode}
pipelineStageId={column.pipelineStageId}
totalAmount={boardColumnTotal}
>
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
{column.pipelineProgressIds.map((pipelineProgressId, index) => (
<RecoilScope
SpecificContext={BoardCardContext}
key={pipelineProgressId}
>
<EntityBoardCard
index={index}
pipelineProgressId={pipelineProgressId}
boardOptions={boardOptions}
/>
</RecoilScope>
))}
</BoardColumnCardsContainer>
<StyledNewCardButtonContainer>
<RecoilScope>{boardOptions.newCardComponent}</RecoilScope>
</StyledNewCardButtonContainer>
</BoardColumn>
)}
</Droppable>
);
}

View File

@ -0,0 +1,2 @@
export * from './select';
export * from './update';

View File

@ -0,0 +1,85 @@
import { gql } from '@apollo/client';
export const GET_PIPELINES = gql`
query GetPipelines($where: PipelineWhereInput) {
findManyPipeline(where: $where) {
id
name
pipelineProgressableType
pipelineStages {
id
name
color
index
pipelineProgresses {
id
}
}
}
}
`;
export const GET_PIPELINE_PROGRESS = gql`
query GetPipelineProgress($where: PipelineProgressWhereInput) {
findManyPipelineProgress(where: $where, orderBy: { createdAt: asc }) {
id
progressableType
progressableId
amount
closeDate
}
}
`;
export const UPDATE_PIPELINE_PROGRESS = gql`
mutation UpdateOnePipelineProgress(
$id: String
$amount: Int
$closeDate: DateTime
) {
updateOnePipelineProgress(
where: { id: $id }
data: { amount: { set: $amount }, closeDate: { set: $closeDate } }
) {
id
amount
closeDate
}
}
`;
export const UPDATE_PIPELINE_PROGRESS_STAGE = gql`
mutation UpdateOnePipelineProgressStage(
$id: String
$pipelineStageId: String
) {
updateOnePipelineProgress(
where: { id: $id }
data: { pipelineStage: { connect: { id: $pipelineStageId } } }
) {
id
}
}
`;
export const ADD_ENTITY_TO_PIPELINE = gql`
mutation CreateOnePipelineProgress(
$uuid: String!
$entityType: PipelineProgressableType!
$entityId: String!
$pipelineId: String!
$pipelineStageId: String!
) {
createOnePipelineProgress(
data: {
id: $uuid
progressableType: $entityType
progressableId: $entityId
pipeline: { connect: { id: $pipelineId } }
pipelineStage: { connect: { id: $pipelineStageId } }
}
) {
id
}
}
`;

View File

@ -0,0 +1,18 @@
import { gql } from '@apollo/client';
export const DELETE_PIPELINE_PROGRESS = gql`
mutation DeleteManyPipelineProgress($ids: [String!]) {
deleteManyPipelineProgress(where: { id: { in: $ids } }) {
count
}
}
`;
export const UPDATE_PIPELINE_STAGE = gql`
mutation UpdatePipelineStage($id: String, $name: String) {
updateOnePipelineStage(where: { id: $id }, data: { name: { set: $name } }) {
id
name
}
}
`;

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const BoardCardContext = createContext<string | null>(null);

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const BoardColumnContext = createContext<string | null>(null);

View File

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

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
export const boardColumnsState = atom<BoardPipelineStageColumn[]>({
key: 'boardColumnsState',
default: [],
});

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
export const boardState = atom<BoardPipelineStageColumn[] | undefined>({
key: 'boardState',
default: undefined,
});

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { Pipeline } from '~/generated/graphql';
export const currentPipelineState = atom<Pipeline | undefined>({
key: 'currentPipelineState',
default: undefined,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isBoardLoadedState = atom<boolean>({
key: 'isBoardLoadedState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const pipelineProgressIdScopedState = atomFamily<string | null, string>({
key: 'pipelineProgressIdScopedState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const pipelineStageIdScopedState = atomFamily<string | null, string>({
key: 'pipelineStageIdScopedState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const selectedBoardCardsState = atom<string[]>({
key: 'isBoardCardSelectedFamilyState',
default: [],
});

View File

@ -0,0 +1,4 @@
export type BoardOptions = {
newCardComponent: React.ReactNode;
cardComponent: React.ReactNode;
};