Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
87
front/src/modules/pipeline/components/EntityBoard.tsx
Normal file
87
front/src/modules/pipeline/components/EntityBoard.tsx
Normal 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>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
@ -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>;
|
||||
}
|
||||
45
front/src/modules/pipeline/components/EntityBoardCard.tsx
Normal file
45
front/src/modules/pipeline/components/EntityBoardCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
111
front/src/modules/pipeline/components/EntityBoardColumn.tsx
Normal file
111
front/src/modules/pipeline/components/EntityBoardColumn.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2
front/src/modules/pipeline/queries/index.ts
Normal file
2
front/src/modules/pipeline/queries/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './select';
|
||||
export * from './update';
|
||||
85
front/src/modules/pipeline/queries/select.ts
Normal file
85
front/src/modules/pipeline/queries/select.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
18
front/src/modules/pipeline/queries/update.ts
Normal file
18
front/src/modules/pipeline/queries/update.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
3
front/src/modules/pipeline/states/BoardCardContext.ts
Normal file
3
front/src/modules/pipeline/states/BoardCardContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardCardContext = createContext<string | null>(null);
|
||||
3
front/src/modules/pipeline/states/BoardColumnContext.ts
Normal file
3
front/src/modules/pipeline/states/BoardColumnContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardColumnContext = createContext<string | null>(null);
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
8
front/src/modules/pipeline/states/boardColumnsState.ts
Normal file
8
front/src/modules/pipeline/states/boardColumnsState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
|
||||
export const boardColumnsState = atom<BoardPipelineStageColumn[]>({
|
||||
key: 'boardColumnsState',
|
||||
default: [],
|
||||
});
|
||||
8
front/src/modules/pipeline/states/boardState.ts
Normal file
8
front/src/modules/pipeline/states/boardState.ts
Normal 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,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Pipeline } from '~/generated/graphql';
|
||||
|
||||
export const currentPipelineState = atom<Pipeline | undefined>({
|
||||
key: 'currentPipelineState',
|
||||
default: undefined,
|
||||
});
|
||||
6
front/src/modules/pipeline/states/isBoardLoadedState.ts
Normal file
6
front/src/modules/pipeline/states/isBoardLoadedState.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isBoardLoadedState = atom<boolean>({
|
||||
key: 'isBoardLoadedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const pipelineProgressIdScopedState = atomFamily<string | null, string>({
|
||||
key: 'pipelineProgressIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const pipelineStageIdScopedState = atomFamily<string | null, string>({
|
||||
key: 'pipelineStageIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const selectedBoardCardsState = atom<string[]>({
|
||||
key: 'isBoardCardSelectedFamilyState',
|
||||
default: [],
|
||||
});
|
||||
4
front/src/modules/pipeline/types/BoardOptions.ts
Normal file
4
front/src/modules/pipeline/types/BoardOptions.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type BoardOptions = {
|
||||
newCardComponent: React.ReactNode;
|
||||
cardComponent: React.ReactNode;
|
||||
};
|
||||
Reference in New Issue
Block a user