Skip to content

Instantly share code, notes, and snippets.

@MarkAtOmniux
Last active March 23, 2024 22:40
Show Gist options
  • Save MarkAtOmniux/13ed64f976c6d747993c4efe0492a3ea to your computer and use it in GitHub Desktop.
Save MarkAtOmniux/13ed64f976c6d747993c4efe0492a3ea to your computer and use it in GitHub Desktop.
A Project Board Planner board built using Payload CMS
@import '~payload/scss';
.add-project-phase {
@include blur-bg;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&__wrapper {
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
gap: var(--base);
}
&__content {
display: flex;
flex-direction: column;
gap: var(--base);
> * {
margin: 0;
}
}
&__controls {
display: flex;
gap: var(--base);
margin-top: base(1);
.btn {
margin: 0;
}
}
}
import React from 'react';
import { Modal } from '@faceless-ui/modal';
import { Button } from 'payload/components';
import {
Form,
FormSubmit,
RenderFields,
fieldTypes,
} from 'payload/components/forms';
import { Project } from 'payload/generated-types';
import { useProject } from '../../../ProjectProvider';
import './addEditPhaseModal.styles.scss;
const baseClass = 'add-project-phase';
export const AddEditPhaseModal = () => {
const { addPhase, modalProps, editPhase, removePhase, toggleModal } =
useProject();
async function submit(fields, data) {
try {
if (modalProps.type === 'edit') {
editPhase(modalProps.phaseIndex, data.title);
return;
}
addPhase(data.title);
} catch (error) {
console.log(error);
}
}
const message = () => {
switch (modalProps.type) {
case 'add':
return 'Enter the phase you wish to add';
case 'edit':
return 'Enter the new title for the phase';
case 'remove':
return 'Are you sure you want to remove this phase?';
default:
return '';
}
};
return (
<Modal className={baseClass} slug='add-edit-project-phase'>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__content`}>
<h1>
{modalProps.type.charAt(0).toUpperCase() + modalProps.type.slice(1)}{' '}
phase
</h1>
<p>{message()}</p>
</div>
<Form
initialData={{
title: modalProps.type === 'edit' ? modalProps.phaseTitle : '',
}}
onSubmit={submit}>
{modalProps.type !== 'remove' && (
<RenderFields
fieldTypes={fieldTypes}
fieldSchema={[
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
]}
/>
)}
<div className={`${baseClass}__controls`}>
{modalProps.type === 'edit' ? (
<>
<FormSubmit>Update title</FormSubmit>
<Button
buttonStyle='error'
onClick={() => toggleModal()}
aria-label='delete phase'>
Cancel
</Button>
</>
) : modalProps.type === 'remove' ? (
<Button
buttonStyle='primary'
onClick={() => removePhase(modalProps.phaseIndex)}>
Remove phase
</Button>
) : (
<FormSubmit>Add phase</FormSubmit>
)}
</div>
</Form>
</div>
</Modal>
);
};
@import '~payload/scss';
.phase-grid {
display: flex;
grid-gap: var(--base);
padding-right: base(4);
overflow-x: scroll;
-ms-overflow-style: none;
width: 100%;
}
.button-column {
max-width: 24rem;
min-width: 24rem;
flex: 1;
width: 100%;
padding: base(1);
border-radius: base(0.5);
border: 1px solid var(--theme-elevation-100);
background-color: transparent;
min-height: 90vh;
transition: all 0.05s ease-in-out;
cursor: pointer;
&:hover {
border-color: var(--theme-elevation-200);
background: var(--theme-elevation-50);
}
}
import { Task as TaskType, User } from 'payload/generated-types';
import React, { FC, useEffect, useState } from 'react';
import { Phase } from './Phase';
import './editProject.styles.scss';
import { AddEditPhaseModal } from './AddEditPhaseModal';
import { DNDType, useProject } from './ProjectProvider';
import { PlusIcon } from 'lucide-react';
import {
DndContext,
closestCorners,
useSensor,
useSensors,
PointerSensor,
KeyboardSensor,
DragOverlay,
UniqueIdentifier,
} from '@dnd-kit/core';
import {
sortableKeyboardCoordinates,
SortableContext,
arrayMove,
} from '@dnd-kit/sortable';
import { Task } from './Task';
const EditProject: FC<{ user: User; id: string }> = ({ user, id }) => {
const { project, openModal, backgroundUpdate } = useProject();
const [phases, setPhases] = useState<DNDType[]>([]);
const [triggerUpdate, setTriggerUpdate] = useState(false);
const [activeId, setActiveId] = useState(undefined);
const findValueOfItems = (id: UniqueIdentifier, type: string) => {
if (type === 'phase') {
return phases.find((phase) => phase?.id === id);
}
if (type === 'task') {
return phases.find((phase) =>
phase.tasks?.find((task) => task?.id === id)
);
}
};
const findTaskField = (id: UniqueIdentifier | undefined, field: string) => {
const phase = findValueOfItems(id, 'task');
if (!phase) return '';
const item = phase.tasks.find((item) => item.id === id);
if (!item) return '';
return item[field];
};
const findPhaseField = (id: UniqueIdentifier | undefined, field: string) => {
const container = findValueOfItems(id, 'phase');
if (!container) return '';
return container[field];
};
const findPhaseItems = (id: UniqueIdentifier | undefined) => {
const container = findValueOfItems(id, 'phase');
if (!container) return [];
return container.tasks;
};
const handleDragStart = (event: any) => {
const { active } = event;
const { id } = active;
setActiveId(id);
};
const handleDragMove = (event: any) => {
const { active, over } = event;
// Handle items sorting
if (
active.id.toString().includes('task') &&
over?.id.toString().includes('task') &&
active &&
over &&
active.id !== over.id
) {
//Find the active container and over Container
const activePhase = findValueOfItems(active.id, 'task');
const overPhase = findValueOfItems(over.id, 'task');
// If the active or over container is undefined, return
if (!activePhase || !overPhase) {
return;
}
// Find the active and over container index
const activePhaseIndex = phases.findIndex(
(phase) => phase.id === activePhase.id
);
const overPhaseIndex = phases.findIndex(
(phase) => phase.id === overPhase.id
);
// Find the index of the active and over item
const activeItemIndex = activePhase.tasks.findIndex(
(task) => task.id === active.id
);
const overItemIndex = overPhase.tasks.findIndex(
(task) => task.id === over.id
);
// In the same container
if (activePhaseIndex === overPhaseIndex) {
let newItems = [...phases];
newItems[activePhaseIndex].tasks = arrayMove(
newItems[activePhaseIndex].tasks,
activeItemIndex,
overItemIndex
);
setPhases(newItems);
// update container
} else {
// Different container
let newItems = [...phases];
const [removedItem] = newItems[activePhaseIndex].tasks.splice(
activeItemIndex,
1
);
newItems[overPhaseIndex].tasks.splice(overItemIndex, 0, removedItem);
setPhases(newItems);
}
}
if (
active.id.toString().includes('task') &&
over?.id.toString().includes('phase') &&
active &&
over &&
active.id !== over.id
) {
// Find the active and over container
const activeContainer = findValueOfItems(active.id, 'task');
const overContainer = findValueOfItems(over.id, 'phase');
// If the active or over container is undefined, return
if (!activeContainer || !overContainer) {
return;
}
// Find the active container index and over container
const activeContainerIndex =
project.phases.findIndex((phase) => phase.id === activeContainer.id) !==
-1
? activeContainer.tasks.findIndex((task) => task.id === active.id)
: 0;
const overContainerIndex =
project.phases.findIndex((phase) => phase.id === overContainer.id) !==
-1
? overContainer.tasks.findIndex((task) => task.id === over.id)
: 0;
// Find the index of the active item in teh active container
const activeItemIndex = activeContainer.tasks.findIndex(
(task) => task.id === active.id
);
// Remove the active item from the active container
let newItems = [...phases];
const [removedItem] = newItems[activeContainerIndex]?.tasks.splice(
activeItemIndex,
1
);
newItems[overContainerIndex].tasks.push(removedItem);
setPhases(newItems);
}
};
const handleDragEnd = (event: any) => {
const { active, over } = event;
let newItems;
// Handle Phase Sorting
if (
active.id.toString().includes('phase') &&
over?.id.toString().includes('phase') &&
active &&
over &&
active.id !== over.id
) {
const activePhaseIndex = phases.findIndex(
(phase) => phase.id === active.id
);
const overPhaseIndex = phases.findIndex((phase) => phase.id === over.id);
// Swap the active and over container
newItems = [...phases];
newItems = arrayMove(newItems, activePhaseIndex, overPhaseIndex);
setPhases(newItems);
}
// Handle Item Sorting
if (
active.id.toString().includes('task') &&
over?.id.toString().includes('task') &&
active &&
over &&
active.id !== over.id
) {
// Find the active container and over Container
const activePhase = findValueOfItems(active.id, 'task');
const overPhase = findValueOfItems(over.id, 'task');
if (!activePhase || !overPhase) {
return;
}
const activePhaseIndex = phases.findIndex(
(phase) => phase.id === activePhase.id
);
const overPhaseIndex = phases.findIndex(
(phase) => phase.id === overPhase.id
);
// Find the index of the active and over item
const activeItemIndex = activePhase.tasks.findIndex(
(task) => task.id === active.id
);
const overItemIndex = overPhase.tasks.findIndex(
(task) => task.id === over.id
);
// In the same container
if (activePhaseIndex === overPhaseIndex) {
newItems = [...phases];
newItems[activePhaseIndex].tasks = arrayMove(
newItems[activePhaseIndex].tasks,
activeItemIndex,
overItemIndex
);
setPhases(newItems);
} else {
// Different container
newItems = [...phases];
const [removedItem] = newItems[activePhaseIndex].tasks.splice(
activeItemIndex,
1
);
newItems[overPhaseIndex].tasks.splice(overItemIndex, 0, removedItem);
setPhases(newItems);
}
}
if (
active.id.toString().includes('task') &&
over?.id.toString().includes('phase') &&
active &&
over &&
active.id !== over.id
) {
const activePhase = findValueOfItems(active.id, 'task');
const overPhase = findValueOfItems(over.id, 'phase');
const activePhaseIndex = phases.findIndex(
(phase) => phase.id === activePhase.id
);
const overPhaseIndex = phases.findIndex(
(phase) => phase.id === overPhase.id
);
const activeItemIndex = activePhase.tasks.findIndex(
(task) => task.id === active.id
);
newItems = [...phases];
const [removedItem] = newItems[activePhaseIndex].tasks.splice(
activeItemIndex,
1
);
newItems[overPhaseIndex].tasks.push(removedItem);
// interate over all tasks in each phase and remove any items in the array with a value of undefined
newItems = newItems.map((phase) => ({
...phase,
tasks: phase.tasks.filter((x) => x),
}));
setPhases(newItems);
}
setActiveId(null);
setTriggerUpdate(true);
};
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
useEffect(() => {
if (triggerUpdate && activeId === null && phases.length > 0) {
backgroundUpdate(phases);
setTriggerUpdate(false);
}
}, [activeId, phases, triggerUpdate]);
useEffect(() => {
if (project) {
console.log(project);
const newPhases =
project?.phases.map((x) => ({
title: x.title,
id: `${x.id}-phase`,
payloadId: x.id,
tasks: x.tasks.map((t: TaskType) => ({
title: t.title,
date: t.dueDate ?? '',
assignedTo: t.assignedTo as User[],
id: `${t.id}-task`,
payloadId: t.id,
})),
})) ?? [];
setPhases(newPhases);
}
}, [project]);
return (
project && (
<>
<h1>{project?.title}</h1>
<div className='phase-grid'>
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}>
<SortableContext items={phases.map((x) => x.id)}>
{(phases ?? []).map((phase, i) => (
<Phase
phaseIndex={i}
title={phase.title}
tasks={phase.tasks}
id={phase.id}
key={phase.id}
/>
))}
</SortableContext>
<DragOverlay adjustScale={false}>
{/* Drag Overlay For Task */}
{activeId && activeId.toString().includes('task') && (
<Task
title={findTaskField(activeId, 'title')}
date={findTaskField(activeId, 'date')}
id={activeId}
/>
)}
{/* Drag Overlay For Phase */}
{activeId && activeId.toString().includes('phase') && (
<Phase id={activeId} title={findPhaseField(activeId, 'title')}>
<div
style={{
display: 'flex',
alignItems: 'start',
flexDirection: 'column',
rowGap: '1rem',
}}>
{findPhaseItems(activeId).map((task) => (
<Task
key={task?.id}
title={task?.title}
date={task.date}
id={task?.id}
/>
))}
</div>
</Phase>
)}
</DragOverlay>
</DndContext>
<button onClick={() => openModal('add')} className='button-column'>
<h4>Add Phase</h4>
<div
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<PlusIcon size={24} />
</div>
</button>
</div>
<AddEditPhaseModal />
</>
)
);
};
export default EditProject;
import { DefaultTemplate, MinimalTemplate } from 'payload/components/templates';
import { useConfig, useDocumentInfo } from 'payload/components/utilities';
import { AdminViewProps } from 'payload/config';
import React, { useState, useEffect } from 'react';
import EditProject from './EditProject';
import { User } from 'payload/generated-types';
import { Gutter } from 'payload/components/elements';
import { ProjectProvider } from './ProjectProvider';
const Page: React.FC<AdminViewProps> = ({ user }) => {
const { id } = useDocumentInfo();
return (
<div
style={{
alignItems: 'start',
marginTop: '1rem',
justifyContent: 'start',
}}>
<Gutter right={false} negativeRight={true}>
{id && (
<ProjectProvider projectId={id as string}>
<EditProject user={user as unknown as User} id={id as string} />
</ProjectProvider>
)}
</Gutter>
</div>
);
};
export default Page;
@import '~payload/scss';
.phase-column {
max-width: 24rem;
min-width: 24rem;
flex: 1;
width: 100%;
padding: base(1);
border-radius: base(0.5);
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
background-color: var(--theme-elevation-50);
min-height: 90vh;
&__grip {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
border: 0;
padding: 0;
margin: 0;
background-color: transparent;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: base(1);
& > h5 {
margin-bottom: 0 !important;
}
}
&__draggable {
opacity: 60%;
}
&__add-task-button {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-top: base(0.5);
padding-bottom: base(0.5);
padding-left: base(0.5);
padding-right: base(0.5);
margin-top: base(0.5);
border-radius: base(0.25);
border: none;
background-color: var(--theme-elevation-50);
transition: all 0.02s ease-in-out;
cursor: pointer;
& > p {
margin: 0;
}
&:hover {
border-color: var(--theme-elevation-200);
background: var(--theme-elevation-150);
}
}
}
import React, { FC } from 'react';
import { GripVerticalIcon, PlusIcon } from 'lucide-react';
import { Button } from 'payload/components';
import { DNDType, useProject } from '../../../ProjectProvider';
import { CSS } from '@dnd-kit/utilities';
import { useDocumentDrawer } from 'payload/components/elements';
import { SortableContext, useSortable } from '@dnd-kit/sortable';
import { useDroppable } from '@dnd-kit/core';
import { Task } from '../Task/Task';
import './phase.styles.scss';
const baseClass = 'phase-column';
export const Phase: FC<{
phaseIndex?: number;
id: DNDType['id'];
title: string;
tasks?: DNDType['tasks'];
children?: React.ReactNode;
}> = ({ id, phaseIndex = 0, children, title, tasks }) => {
const { openModal, addTask } = useProject();
const [DocumentDrawer, _, { openDrawer, closeDrawer }] = useDocumentDrawer({
collectionSlug: 'tasks',
});
const {
attributes,
setNodeRef,
listeners,
transform,
transition,
isDragging,
} = useSortable({
id,
data: {
type: 'phase',
},
});
const { setNodeRef: droppableNode } = useDroppable({
id,
data: {
type: 'task',
},
});
return (
<div
{...attributes}
ref={setNodeRef}
className={`${baseClass} ${isDragging && baseClass + '__draggable'}`}
style={{ transition, transform: CSS.Translate.toString(transform) }}>
<div className={`${baseClass}__header`}>
<div
style={{
display: 'flex',
columnGap: '0.5rem',
alignItems: 'center',
}}>
<button {...listeners} className={`${baseClass}__grip`}>
<GripVerticalIcon size={20} />
</button>
<h5>{title}</h5>
</div>
<div
style={{ display: 'flex', columnGap: '1rem', alignItems: 'center' }}>
<Button
aria-label='edit phase title'
round
buttonStyle='none'
iconStyle='without-border'
icon='edit'
onClick={() => openModal('edit', phaseIndex, title)}
/>
<Button
aria-label='remove phase'
round
buttonStyle='none'
iconStyle='without-border'
icon='x'
onClick={() => openModal('remove', phaseIndex, title)}
/>
</div>
</div>
{children ? (
children
) : (
<SortableContext
items={(tasks ?? []).filter((x) => x).map((x) => x.id)}>
<div
ref={droppableNode}
style={{
display: 'flex',
alignItems: 'start',
flexDirection: 'column',
rowGap: '1rem',
minHeight: '1px',
}}>
{(tasks ?? [])
.filter((x) => x)
.map((task) => (
<Task
assignedTo={task?.assignedTo ?? []}
key={task.id}
date={task.date}
title={task.title}
id={task.id}
/>
))}
</div>
</SortableContext>
)}
<button onClick={openDrawer} className={`${baseClass}__add-task-button`}>
<p>Add Task</p>
<PlusIcon size={16} />
</button>
<DocumentDrawer
onSave={({ doc }) => {
addTask(phaseIndex, doc);
closeDrawer();
}}
/>
</div>
);
};
import { useStepNav } from 'payload/dist/admin/components/elements/StepNav';
import { Organization, Project, Task, User } from 'payload/generated-types';
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useModal } from '@faceless-ui/modal';
import {
UniqueIdentifier,
} from '@dnd-kit/core';
import { toast } from 'react-toastify';
type ProjectContextType = {
project: Project;
openModal: (
type: 'edit' | 'add' | 'remove',
phaseIndex?: number,
phaseTitle?: string
) => void;
addPhase: (title: string) => void;
removePhase: (index: number) => void;
editPhase: (index: number, title: string) => void;
addTask: (phaseIndex: number, task: Task) => void;
backgroundUpdate: (data: any) => void;
refresh: () => void;
toggleModal: () => void;
modalProps: {
type: 'edit' | 'add' | 'remove';
phaseIndex?: number;
phaseTitle?: string;
};
};
export type DNDType = {
id: UniqueIdentifier;
payloadId: string;
title: string;
tasks: {
id: UniqueIdentifier;
payloadId: string;
assignedTo: User[];
date?: string;
title: string;
}[];
};
const ProjectContext = createContext<ProjectContextType>({
project: {} as Project,
openModal: () => {},
addPhase: () => {},
removePhase: () => {},
editPhase: () => {},
backgroundUpdate: () => {},
addTask: () => {},
refresh: () => {},
toggleModal: () => {},
modalProps: { type: 'add', phaseTitle: '' },
});
export const ProjectProvider: React.FC<{
children: React.ReactNode;
projectId: string;
}> = ({ children, projectId }) => {
const [project, setProject] = useState<Project>(null);
const [modalProps, setModalProps] = useState<{
type: 'add' | 'edit' | 'remove';
phaseIndex?: number;
phaseTitle?: string;
}>({ type: 'add', phaseTitle: '' });
const { toggleModal } = useModal();
const { setStepNav } = useStepNav();
const openModal = (
type: 'edit' | 'add' | 'remove',
phaseIndex?: number,
phaseTitle?: string
) => {
setModalProps({ type, phaseIndex, phaseTitle });
toggleModal('add-edit-project-phase');
};
const updateProject = async (data: Partial<Project>) => {
const res = await fetch(`/api/projects/${projectId}`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const updatedProject = await res.json();
if (updatedProject.error) toast.error('Error updating project');
setProject(updatedProject.doc);
};
const fetchProject = async (id: string) => {
const res = await fetch(`/api/v1/projects/${id}`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
setProject(data);
setStepNav([
{
label: 'Projects',
url: `/admin/collections/projects?limit=10`,
},
{
label: data.title,
url: `/admin/projects/${data.id}`,
},
]);
};
const addPhase = async (title: string) => {
(project.phases ?? []).push({
title,
tasks: [],
});
project.phases.forEach((phase) => {
phase.tasks = phase.tasks.map((task: Task) => task.id);
});
await updateProject({ phases: project.phases });
toggleModal('add-edit-project-phase');
};
const editPhase = async (index: number, title: string) => {
project.phases[index].title = title;
project.phases.forEach((phase) => {
phase.tasks = phase.tasks.map((task: Task) => task.id);
});
await updateProject({ phases: project.phases });
toggleModal('add-edit-project-phase');
};
const removePhase = async (index: number) => {
project.phases.splice(index, 1);
project.phases.forEach((phase) => {
phase.tasks = phase.tasks.map((task: Task) => task.id);
});
await updateProject({ phases: project.phases });
toggleModal('add-edit-project-phase');
};
const addTask = async (phaseIndex: number, task: Task) => {
if (project.phases[phaseIndex].tasks === undefined) {
project.phases[phaseIndex].tasks = [task];
} else {
project.phases[phaseIndex].tasks.push(task);
}
// loop through each project phase and set the tasks object to id only
project.phases.forEach((phase) => {
phase.tasks = phase.tasks.map((task: Task) => task.id);
});
await updateProject({ phases: project.phases });
};
const backgroundUpdate = (data: DNDType[]) => {
// map data to a project type. remove task and phase from ids, push updates to the API
if (!project) return;
const updatedPhases: Partial<Project> = {
...project,
organization: (project.organization as Organization).id,
phases: data.map((phase) => ({
id: phase.payloadId,
title: phase.title,
tasks: (phase.tasks ?? [])
.filter((x) => x)
.map((task) => task.payloadId),
})),
};
updateProject(updatedPhases);
};
useEffect(() => {
fetchProject(projectId);
}, [projectId]);
return (
<ProjectContext.Provider
value={{
project,
openModal,
addPhase,
removePhase,
editPhase,
backgroundUpdate,
modalProps,
refresh: () => fetchProject(projectId),
toggleModal: () => toggleModal('add-edit-project-phase'),
addTask,
}}>
{children}
</ProjectContext.Provider>
);
};
export const useProject = () => useContext(ProjectContext);
import { CollectionConfig, PayloadRequest } from 'payload/types';
import Page from './views/Page';
export const Projects: CollectionConfig = {
slug: 'projects',
labels: {
singular: 'Project',
plural: 'Projects',
},
admin: {
useAsTitle: 'title',
components: {
views: {
Edit: Page,
},
},
},
fields: [
{
type: 'text',
name: 'title',
},
{
type: 'array',
name: 'phases',
fields: [
{
type: 'text',
name: 'title',
},
{
type: 'relationship',
name: 'tasks',
hasMany: true,
relationTo: 'tasks',
},
],
},
],
};
@import '~payload/scss';
.task {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
border-radius: base(0.25);
background-color: var(--theme-elevation-100);
border: 1px solid var(--theme-elevation-150);
transition: all 0.05s ease-in-out;
cursor: pointer;
&:hover {
background: var(--theme-elevation-100);
}
&__draggable {
opacity: 60%;
}
&__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
&__pill {
width: fit-content;
}
&__drag_container {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
padding-top: base(0.75);
padding-bottom: base(0.75);
padding-left: base(0.5);
padding-right: base(1);
row-gap: base(0.5);
& > p {
margin: 0;
}
}
&__edit {
padding-top: base(0.25);
padding-right: base(0.5);
}
}
import { User } from 'payload/generated-types';
import React, { FC } from 'react';
import { Button, useDocumentDrawer } from 'payload/components/elements';
import { DNDType, useProject } from '../../../ProjectProvider';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Pill } from 'payload/components';
import { format } from 'date-fns';
import './task.styles.scss';
const baseClass = 'task';
export const Task: FC<{
id: DNDType['tasks'][0]['id'];
title: string;
date?: string;
assignedTo?: User[];
}> = ({ title, id, date, assignedTo }) => {
const [DocumentDrawer, _, { openDrawer, closeDrawer }] = useDocumentDrawer({
collectionSlug: 'tasks,
id: id.toString().split('-task')[0],
});
const {
attributes,
setNodeRef,
listeners,
transform,
transition,
isDragging,
} = useSortable({
id,
data: {
type: 'task',
},
});
const { refresh } = useProject();
return (
<div
ref={setNodeRef}
{...attributes}
style={{ transition, transform: CSS.Translate.toString(transform) }}
className={`${baseClass} ${isDragging && baseClass + '__draggable'}`}>
<div className={`${baseClass}__drag_container`} {...listeners}>
<p>{title}</p>
{date && (
<Pill className={`${baseClass}__pill`} rounded pillStyle='light'>
{format(Date.parse(date), 'do LLL, yyyy')}
</Pill>
)}
</div>
<div className={`${baseClass}__edit`}>
<Button
aria-label='edit phase title'
round
buttonStyle='none'
iconStyle='without-border'
icon='edit'
onClick={openDrawer}
/>
</div>
<DocumentDrawer
onSave={({ doc }) => {
refresh();
closeDrawer();
}}
/>
</div>
);
};
import { CollectionConfig, PayloadRequest } from 'payload/types';
export const Tasks: CollectionConfig = {
slug: 'tasks',
labels: {
singular: 'Task',
plural: 'Tasks',
},
admin: {
useAsTitle: 'title',
hidden: () => true,
},
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Details',
fields: [
{
type: 'row',
fields: [
{
type: 'text',
name: 'title',
admin: {
width: '75%',
},
},
{
type: 'date',
name: 'dueDate',
admin: {
width: '25%',
},
},
],
},
{
type: 'relationship',
name: 'assignedTo',
relationTo: 'users',
hasMany: true,
admin: {
allowCreate: false,
},
},
],
},
{
label: 'Notes',
fields: [
{
type: 'textarea',
name: 'notes',
admin: {
rows: 6,
},
},
],
}
],
},
],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment