Skip to content

Commit

Permalink
Ajoute le drag and drop sur les étapes de la fiche action
Browse files Browse the repository at this point in the history
  • Loading branch information
cparthur committed Dec 17, 2024
1 parent ddd91ff commit 03cc459
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,43 +1,78 @@
import classNames from 'classnames';
import { useState } from 'react';
import classNames from 'classnames';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

import { RouterOutput } from '@/api/utils/trpc/client';

import { FicheActionEtapeType } from '@/backend/fiches/fiche-action-etape/fiche-action-etape.table';
import { Button, Checkbox } from '@/ui';

import { useEtapesDispatch } from '../etapes-context';
import ModalDeleteEtape from './modal-delete-etape';
import { useUpsertEtape } from './use-upsert-etape';
import { Textarea } from './textarea';
import { useUpsertEtape } from './use-upsert-etape';

type Props = {
etape: FicheActionEtapeType;
isCreationEtape?: boolean;
etape: RouterOutput['plans']['fiches']['etapes']['list'][0];
isReadonly: boolean;
};

export const Etape = ({ etape, isReadonly, isCreationEtape }: Props) => {
export const Etape = ({ etape, isReadonly }: Props) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: etape.id,
disabled: isReadonly,
});

const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};

const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);

const dispatchEtapes = useEtapesDispatch();
const { mutate: updateEtape } = useUpsertEtape();

return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={classNames(
'group relative flex items-start w-full p-4 rounded-lg hover:bg-grey-2',
{ 'bg-error-2 bg-opacity-50': isDeleteModalOpen }
'group relative flex items-start w-full p-4 rounded-lg',

{
'bg-error-2 bg-opacity-50': isDeleteModalOpen,
'hover:bg-grey-2': !isReadonly,
'cursor-default': isReadonly,
'z-10': isDragging,
}
)}
>
<Checkbox
checked={etape.realise}
disabled={isReadonly && isCreationEtape}
onChange={() =>
!isCreationEtape &&
disabled={isReadonly}
onChange={() => {
dispatchEtapes({
type: 'toggleRealise',
payload: {
etapeId: etape.id,
},
});
updateEtape({
id: etape.id,
ficheId: etape.ficheId,
ordre: etape.ordre,
...etape,
realise: !etape.realise,
})
}
});
}}
/>
<Textarea
nom={etape.nom}
Expand All @@ -46,11 +81,15 @@ export const Etape = ({ etape, isReadonly, isCreationEtape }: Props) => {
disabled={isReadonly}
onBlur={(newTitle) => {
if (newTitle.length) {
dispatchEtapes({
type: 'updateNom',
payload: {
etapeId: etape.id,
nom: newTitle,
},
});
updateEtape({
id: etape.id,
ficheId: etape.ficheId,
ordre: etape.ordre,
realise: etape.realise,
...etape,
nom: newTitle,
});
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { Modal, ModalFooterOKCancel } from '@/ui';
import { OpenState } from '@/ui/utils/types';

import { useDeleteEtape } from './use-delete-etape';
import { useEtapesDispatch } from '../etapes-context';

type Props = {
openState: OpenState;
etapeId: number;
};

const ModalDeleteEtape = ({ openState, etapeId }: Props) => {
const dispatchEtapes = useEtapesDispatch();
const { mutate: deleteEtape } = useDeleteEtape();

return (
Expand All @@ -24,6 +26,10 @@ const ModalDeleteEtape = ({ openState, etapeId }: Props) => {
children: 'Confirmer',
onClick: () => {
deleteEtape({ etapeId });
dispatchEtapes({
type: 'delete',
payload: { etapeId },
});
close();
},
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { createContext, ReactNode, useContext, useReducer } from 'react';

import { RouterInput, RouterOutput } from '@/api/utils/trpc/client';

type Action =
| {
type: 'create';
payload: RouterOutput['plans']['fiches']['etapes']['upsert'];
}
| {
type: 'toggleRealise';
payload: {
etapeId: number;
};
}
| {
type: 'updateNom';
payload: {
etapeId: number;
nom: string;
};
}
| {
type: 'updateOrder';
payload: {
etapeId: number;
oldOrder: number;
newOrder: number;
};
}
| {
type: 'delete';
payload: RouterInput['plans']['fiches']['etapes']['delete'];
};

type State = {
etapes: RouterOutput['plans']['fiches']['etapes']['list'];
};

type Dispatch = (action: Action) => void;

const StateContext = createContext<State | undefined>(undefined);
const DispatchContext = createContext<Dispatch | undefined>(undefined);

const panelReducer = (state: State, action: Action) => {
const { etapes } = state;
switch (action.type) {
case 'create': {
const { payload: newEtape } = action;
return {
...state,
etapes: [...etapes, newEtape],
};
}
case 'toggleRealise': {
const { etapeId } = action.payload;
return {
...state,
etapes: etapes.map((etape) =>
etapeId === etape.id ? { ...etape, realise: !etape.realise } : etape
),
};
}
case 'updateNom': {
const { etapeId, nom: newNom } = action.payload;
return {
...state,
etapes: etapes.map((etape) =>
etapeId === etape.id ? { ...etape, nom: newNom } : etape
),
};
}
case 'updateOrder': {
const { etapeId, oldOrder, newOrder } = action.payload;
return {
...state,
etapes: [
...etapes.map((e) => {
if (e.id === etapeId) {
return { ...e, ordre: newOrder };
}
if (e.ordre > oldOrder && e.ordre <= newOrder) {
return { ...e, ordre: e.ordre - 1 };
}
if (e.ordre < oldOrder && e.ordre >= newOrder) {
return { ...e, ordre: e.ordre + 1 };
}
return e;
}),
].sort((a, b) => a.ordre - b.ordre),
};
}
case 'delete': {
const { etapeId } = action.payload;
return {
...state,
etapes: etapes.filter((etape) => etape.id !== etapeId),
};
}
default:
return state;
}
};

export const EtapesProvider = ({
initialState,
children,
}: {
initialState: State;
children: ReactNode;
}) => {
const [state, dispatch] = useReducer(panelReducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
};

export const useEtapesState = () => {
const context = useContext(StateContext);

if (context === undefined) {
throw new Error('usePanelSate must be used within a EtapesProvider');
}
return context;
};

export const useEtapesDispatch = () => {
const context = useContext(DispatchContext);

if (context === undefined) {
throw new Error('usePanelDispatch must be used within a EtapesProvider');
}
return context;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
closestCenter,
DndContext,
DragEndEvent,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';

import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';

import { RouterOutput } from '@/api/utils/trpc/client';

import { Etape, useUpsertEtape } from './etape';
import { useEtapesDispatch } from './etapes-context';

type Etapes = RouterOutput['plans']['fiches']['etapes']['list'];

type Props = {
ficheId: number;
etapes: Etapes;
isReadonly: boolean;
};

const EtapesList = ({ ficheId, etapes, isReadonly }: Props) => {
const dispatchEtapes = useEtapesDispatch();
const { mutateAsync: updateEtapeOrder } = useUpsertEtape();

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 20,
},
})
);

const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (!(over && active.id !== over.id)) {
return;
}

const activeEtape = etapes.find((etape) => etape.id === active.id);
const overEtape = etapes.find((etape) => etape.id === over.id);

if (activeEtape && overEtape) {
updateEtapeOrder({
id: activeEtape.id,
ficheId,
ordre: overEtape.ordre,
});
dispatchEtapes({
type: 'updateOrder',
payload: {
etapeId: activeEtape.id,
oldOrder: activeEtape.ordre,
newOrder: overEtape.ordre,
},
});
}
};

return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={etapes} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-1">
{etapes.map((etape) => (
<Etape key={etape.id} etape={etape} isReadonly={isReadonly} />
))}
</div>
</SortableContext>
</DndContext>
);
};

export default EtapesList;
Loading

0 comments on commit 03cc459

Please sign in to comment.