mirror of
https://github.com/pikami/scrummie-poker.git
synced 2024-12-03 15:25:42 +00:00
Added ticket content with rich editor
This commit is contained in:
parent
40b1ef6f0c
commit
efeeb10746
@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-react": "^9.3.0",
|
||||
"@tanstack/react-form": "^0.33.0",
|
||||
"@tanstack/react-router": "^1.62.0",
|
||||
"appwrite": "^16.0.2",
|
||||
|
@ -1,12 +1,14 @@
|
||||
import classNames from 'classnames';
|
||||
import { PencilIcon } from './icons';
|
||||
|
||||
interface CardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
onClick?: () => void;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
const Card: React.FC<CardProps> = ({ title, description, onClick }) => {
|
||||
const Card: React.FC<CardProps> = ({ title, description, onClick, onEdit }) => {
|
||||
const className = classNames(
|
||||
'p-4 border rounded-lg shadow-sm transition',
|
||||
{
|
||||
@ -19,8 +21,22 @@ const Card: React.FC<CardProps> = ({ title, description, onClick }) => {
|
||||
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<h3 className="text-lg font-bold">{title}</h3>
|
||||
<p className="mt-2">{description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold">{title}</h3>
|
||||
{onEdit && (
|
||||
<button
|
||||
className="ml-2 p-1 text-gray-600 hover:text-gray-900 dark:text-nero-400 dark:hover:text-nero-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
aria-label="Edit card"
|
||||
>
|
||||
<PencilIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{description && <p className="mt-2">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, children }) => {
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'relative h-full w-80 transform space-y-6 bg-white p-6 shadow-lg transition-transform dark:bg-nero-900',
|
||||
'relative h-full w-2/3 transform space-y-6 bg-white p-6 shadow-lg transition-transform dark:bg-nero-900',
|
||||
{
|
||||
'translate-x-0': isOpen,
|
||||
'translate-x-full': !isOpen,
|
||||
|
72
src/components/HtmlEmbed.tsx
Normal file
72
src/components/HtmlEmbed.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
interface HtmlEmbedProps {
|
||||
body: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const HtmlEmbed: React.FC<HtmlEmbedProps> = ({ body, className }) => {
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const styles = `
|
||||
body {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
body {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.x),body:not(.x){height:auto!important}
|
||||
p:first-child{margin-top:0;}
|
||||
p:last-child{margin-bottom:0;}
|
||||
a[href]{color: #3781B8;text-decoration:none;}
|
||||
a[href]:hover{text-decoration:underline;}
|
||||
blockquote[type=cite] {margin:0 0 0 .8ex;border-left: 1px #ccc solid;padding-left: 1ex;}
|
||||
img { max-width: 100%; }
|
||||
ul, ol { padding: 0; margin: 0 0 10px 25px; }
|
||||
ul { list-style-type: disc; }`;
|
||||
|
||||
const cnt = `
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'none'" />
|
||||
<base target="_blank" />
|
||||
<style>
|
||||
${styles}
|
||||
</style>
|
||||
</head>
|
||||
<body>${body}</body>
|
||||
</html>`;
|
||||
|
||||
const props = {
|
||||
csp: "script-src 'none'",
|
||||
};
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className={className}
|
||||
srcDoc={cnt}
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HtmlEmbed;
|
102
src/components/RichEditor.tsx
Normal file
102
src/components/RichEditor.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||
import {
|
||||
ClassicEditor,
|
||||
Bold,
|
||||
Essentials,
|
||||
Italic,
|
||||
Mention,
|
||||
Paragraph,
|
||||
Undo,
|
||||
Heading,
|
||||
Font,
|
||||
Code,
|
||||
Strikethrough,
|
||||
Table,
|
||||
SourceEditing,
|
||||
List,
|
||||
Link,
|
||||
Image,
|
||||
Indent,
|
||||
Subscript,
|
||||
Superscript,
|
||||
BlockQuote,
|
||||
CodeBlock,
|
||||
TodoList,
|
||||
} from 'ckeditor5';
|
||||
import 'ckeditor5/ckeditor5.css';
|
||||
|
||||
interface RichEditorProps {
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
onBlur: () => void;
|
||||
}
|
||||
|
||||
const RichEditor: React.FC<RichEditorProps> = ({ value, onChange, onBlur }) => {
|
||||
return (
|
||||
<CKEditor
|
||||
data={value}
|
||||
onBlur={onBlur}
|
||||
onChange={(_, editor) => onChange(editor.getData())}
|
||||
editor={ClassicEditor}
|
||||
config={{
|
||||
toolbar: {
|
||||
items: [
|
||||
'undo',
|
||||
'redo',
|
||||
'|',
|
||||
'heading',
|
||||
'|',
|
||||
'fontfamily',
|
||||
'fontsize',
|
||||
'fontColor',
|
||||
'fontBackgroundColor',
|
||||
'|',
|
||||
'bold',
|
||||
'italic',
|
||||
'strikethrough',
|
||||
'subscript',
|
||||
'superscript',
|
||||
'code',
|
||||
'|',
|
||||
'link',
|
||||
'blockQuote',
|
||||
'codeBlock',
|
||||
'|',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'todoList',
|
||||
'outdent',
|
||||
'indent',
|
||||
],
|
||||
shouldNotGroupWhenFull: false,
|
||||
},
|
||||
plugins: [
|
||||
Undo,
|
||||
Heading,
|
||||
Font,
|
||||
Code,
|
||||
Bold,
|
||||
Strikethrough,
|
||||
Essentials,
|
||||
Italic,
|
||||
Mention,
|
||||
Paragraph,
|
||||
Table,
|
||||
SourceEditing,
|
||||
List,
|
||||
Link,
|
||||
Image,
|
||||
Indent,
|
||||
Subscript,
|
||||
Superscript,
|
||||
BlockQuote,
|
||||
CodeBlock,
|
||||
TodoList,
|
||||
],
|
||||
initialData: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichEditor;
|
@ -1,6 +1,5 @@
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useEstimationsList } from '../../lib/context/estimationsList';
|
||||
import { useUser } from '../../lib/context/user';
|
||||
import Input from '../Input';
|
||||
import Button from '../Button';
|
||||
|
||||
@ -11,7 +10,6 @@ interface CreateEstimationSessionFormProps {
|
||||
const CreateEstimationSessionForm: React.FC<
|
||||
CreateEstimationSessionFormProps
|
||||
> = ({ onCreated }) => {
|
||||
const user = useUser();
|
||||
const estimationsList = useEstimationsList();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
@ -20,7 +18,6 @@ const CreateEstimationSessionForm: React.FC<
|
||||
onSubmit: async ({ value }) => {
|
||||
await estimationsList?.add({
|
||||
name: value.name,
|
||||
userId: user.current?.$id,
|
||||
});
|
||||
onCreated();
|
||||
},
|
||||
|
21
src/components/icons/PencilIcon.tsx
Normal file
21
src/components/icons/PencilIcon.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
const PencilIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={`size-6 ${props.className}`}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default PencilIcon;
|
3
src/components/icons/index.ts
Normal file
3
src/components/icons/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import PencilIcon from './PencilIcon';
|
||||
|
||||
export { PencilIcon };
|
@ -13,15 +13,17 @@ import {
|
||||
} from '../appwrite';
|
||||
import { DatabaseModels, EntityModels } from '../types';
|
||||
import { useUser } from './user';
|
||||
import { EstimationSessionTicket } from '../types/entityModels';
|
||||
import { mapDatabaseToEntity } from '../mappers/estimationSession';
|
||||
import { EditTicketRequest, CreateTicketRequest } from '../types/requestModels';
|
||||
import { EstimationSessionTicket } from '../types/entityModels';
|
||||
|
||||
interface EstimationContextType {
|
||||
setSessionId: (sessionId: string) => void;
|
||||
setActiveTicket: (ticketId: string) => Promise<void>;
|
||||
setRevealed: (revealed: boolean) => Promise<void>;
|
||||
setVote: (estimate: string) => Promise<void>;
|
||||
createTicket: (ticket: Omit<EstimationSessionTicket, 'id'>) => Promise<void>;
|
||||
createTicket: (ticket: CreateTicketRequest) => Promise<void>;
|
||||
updateTicket: (ticket: EditTicketRequest) => Promise<void>;
|
||||
currentSessionData?: EntityModels.EstimationSession;
|
||||
}
|
||||
|
||||
@ -118,14 +120,15 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||
});
|
||||
};
|
||||
|
||||
const createTicket = async (ticket: Omit<EstimationSessionTicket, 'id'>) => {
|
||||
const newTicketsValue = currentSessionData?.tickets
|
||||
.concat([
|
||||
{
|
||||
...ticket,
|
||||
id: crypto.randomUUID(),
|
||||
},
|
||||
])
|
||||
const createTicket = async ({ name, content }: CreateTicketRequest) => {
|
||||
const newTicket: EstimationSessionTicket = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
content,
|
||||
};
|
||||
|
||||
const newTicketsValue = [newTicket]
|
||||
.concat(currentSessionData?.tickets ?? [])
|
||||
.map((x) => JSON.stringify(x));
|
||||
|
||||
await databases.updateDocument<DatabaseModels.EstimationSession>(
|
||||
@ -138,6 +141,29 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||
);
|
||||
};
|
||||
|
||||
const updateTicket = async ({ id, name, content }: EditTicketRequest) => {
|
||||
const editedTicket = currentSessionData?.tickets.find((x) => x.id === id);
|
||||
if (!editedTicket) {
|
||||
return;
|
||||
}
|
||||
|
||||
editedTicket.name = name;
|
||||
editedTicket.content = content;
|
||||
|
||||
const newTicketsValue = currentSessionData?.tickets.map((x) =>
|
||||
JSON.stringify(x),
|
||||
);
|
||||
|
||||
await databases.updateDocument<DatabaseModels.EstimationSession>(
|
||||
DATABASE_ID,
|
||||
ESTIMATION_SESSION_COLLECTION_ID,
|
||||
sessionId,
|
||||
{
|
||||
tickets: newTicketsValue,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EstimationContext.Provider
|
||||
value={{
|
||||
@ -146,6 +172,7 @@ export const EstimationContextProvider = (props: PropsWithChildren) => {
|
||||
setRevealed,
|
||||
setVote,
|
||||
createTicket,
|
||||
updateTicket,
|
||||
currentSessionData,
|
||||
}}
|
||||
>
|
||||
|
@ -21,7 +21,7 @@ import { useUser } from './user';
|
||||
|
||||
interface EstimationsListContextType {
|
||||
current: EntityModels.EstimationSession[];
|
||||
add: (estimationSession: { name: string; userId?: string }) => Promise<void>;
|
||||
add: (estimationSession: { name: string }) => Promise<void>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ export function EstimationsListContextProvider(props: PropsWithChildren) {
|
||||
EntityModels.EstimationSession[]
|
||||
>([]);
|
||||
|
||||
const add = async (estimationSession: { name: string; userId?: string }) => {
|
||||
const add = async (estimationSession: { name: string }) => {
|
||||
if (!userData) {
|
||||
throw Error('Tried to create new estimation with no user context');
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ interface EstimationSession {
|
||||
interface EstimationSessionTicket {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface SessionState {
|
||||
|
10
src/lib/types/requestModels.ts
Normal file
10
src/lib/types/requestModels.ts
Normal file
@ -0,0 +1,10 @@
|
||||
interface CreateTicketRequest {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface EditTicketRequest extends CreateTicketRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type { CreateTicketRequest, EditTicketRequest };
|
@ -5,9 +5,9 @@ import TaskSidebar from './components/TaskSidebar';
|
||||
import VoteSelection from './components/VoteSelection';
|
||||
import VoteList from './components/VoteList';
|
||||
import { Button, ButtonColor, Drawer } from '../../components';
|
||||
import CreateTicketForm from './components/CreateTicketForm';
|
||||
import CopyInput from '../../components/CopyInput';
|
||||
import EditTicketForm from './components/EditTicketForm';
|
||||
import PlayerList from './components/PlayerList';
|
||||
import HtmlEmbed from '../../components/HtmlEmbed';
|
||||
|
||||
const fibonacciSequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100];
|
||||
|
||||
@ -16,8 +16,10 @@ const route = getRouteApi('/_authenticated/estimate/session/$sessionId');
|
||||
const Estimation: React.FC = () => {
|
||||
const { sessionId } = route.useParams();
|
||||
const estimationState = useEstimationContext();
|
||||
useEffect(() => estimationState?.setSessionId(sessionId), [sessionId]);
|
||||
const [isDrawerOpen, setDrawerOpen] = useState(false);
|
||||
const [editingTicketId, setEditingTicketId] = useState<string>('');
|
||||
|
||||
useEffect(() => estimationState?.setSessionId(sessionId), [sessionId]);
|
||||
|
||||
if (!estimationState?.currentSessionData) {
|
||||
return null; // TODO: Add a loader
|
||||
@ -28,6 +30,7 @@ const Estimation: React.FC = () => {
|
||||
setVote,
|
||||
setRevealed,
|
||||
createTicket,
|
||||
updateTicket,
|
||||
currentSessionData: {
|
||||
tickets: tickets,
|
||||
sessionState: {
|
||||
@ -47,24 +50,25 @@ const Estimation: React.FC = () => {
|
||||
tickets={tickets}
|
||||
onSelectTicket={(ticket) => setActiveTicket(ticket.id)}
|
||||
onAddTicket={() => setDrawerOpen(true)}
|
||||
onEditTicket={(ticketId) => {
|
||||
setEditingTicketId(ticketId);
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex w-full flex-grow flex-col p-6">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="align-middle text-xl font-semibold">
|
||||
Invite others to join your session
|
||||
</span>
|
||||
<CopyInput value={`${window.location.origin}/join/${sessionId}`} />
|
||||
</div>
|
||||
{currentTicket ? (
|
||||
<>
|
||||
<h1 className="mb-4 text-2xl font-bold">{currentTicket.name}</h1>
|
||||
<p className="mb-8 text-gray-700 dark:text-gray-200">
|
||||
{currentTicket.id}
|
||||
</p>
|
||||
<div className="grow">
|
||||
<HtmlEmbed
|
||||
className="h-full w-full"
|
||||
body={currentTicket.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VoteSelection
|
||||
className="mb-4 mt-auto flex flex-wrap gap-1 space-x-4"
|
||||
className="mb-4 flex flex-wrap gap-1 space-x-4"
|
||||
onSelect={(vote) => setVote(vote)}
|
||||
options={fibonacciSequence.map((x) => `${x}`)}
|
||||
value={currentPlayerVote}
|
||||
@ -86,12 +90,26 @@ const Estimation: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PlayerList players={players ?? []} />
|
||||
<PlayerList sessionId={sessionId} players={players ?? []} />
|
||||
|
||||
<Drawer isOpen={isDrawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||
<CreateTicketForm
|
||||
onCreate={async (ticket) => {
|
||||
await createTicket(ticket);
|
||||
<Drawer
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setEditingTicketId('');
|
||||
}}
|
||||
>
|
||||
<EditTicketForm
|
||||
initialData={tickets.find((x) => x.id === editingTicketId)}
|
||||
onSave={async (ticket) => {
|
||||
if (editingTicketId.length > 0) {
|
||||
await updateTicket({
|
||||
...ticket,
|
||||
id: editingTicketId,
|
||||
});
|
||||
} else {
|
||||
await createTicket(ticket);
|
||||
}
|
||||
setDrawerOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
@ -1,18 +1,28 @@
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { Button, Input } from '../../../components';
|
||||
import { EstimationSessionTicket } from '../../../lib/types/entityModels';
|
||||
import RichEditor from '../../../components/RichEditor';
|
||||
|
||||
interface CreateTicketFormProps {
|
||||
onCreate: (ticket: Omit<EstimationSessionTicket, 'id'>) => Promise<void>;
|
||||
interface EditTicketFormData {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const CreateTicketForm: React.FC<CreateTicketFormProps> = ({ onCreate }) => {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
interface EditTicketFormProps {
|
||||
initialData?: EditTicketFormData;
|
||||
onSave: (ticket: EditTicketFormData) => Promise<void>;
|
||||
}
|
||||
|
||||
const EditTicketForm: React.FC<EditTicketFormProps> = ({
|
||||
initialData,
|
||||
onSave,
|
||||
}) => {
|
||||
const form = useForm<EditTicketFormData>({
|
||||
defaultValues: initialData ?? {
|
||||
name: '',
|
||||
content: '',
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
await onCreate(value);
|
||||
await onSave(value);
|
||||
},
|
||||
});
|
||||
|
||||
@ -40,12 +50,22 @@ const CreateTicketForm: React.FC<CreateTicketFormProps> = ({ onCreate }) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<form.Field
|
||||
name="content"
|
||||
children={(field) => (
|
||||
<RichEditor
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(value) => field.handleChange(value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" fullWidth>
|
||||
Create
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTicketForm;
|
||||
export default EditTicketForm;
|
@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import { EntityModels } from '../../../lib/types';
|
||||
import CopyInput from '../../../components/CopyInput';
|
||||
|
||||
interface PlayerListProps {
|
||||
sessionId: string;
|
||||
players: EntityModels.Player[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const PlayerList: React.FC<PlayerListProps> = ({
|
||||
sessionId,
|
||||
players,
|
||||
title = 'Players',
|
||||
}) => {
|
||||
@ -32,6 +35,13 @@ const PlayerList: React.FC<PlayerListProps> = ({
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-2">
|
||||
<div className="text-l align-middle font-semibold">
|
||||
Invite others to join your session
|
||||
</div>
|
||||
<CopyInput value={`${window.location.origin}/join/${sessionId}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ interface TaskSidebarProps {
|
||||
tickets: EstimationSessionTicket[];
|
||||
onSelectTicket: (ticket: EstimationSessionTicket) => void;
|
||||
onAddTicket: () => void;
|
||||
onEditTicket: (ticketId: string) => void;
|
||||
}
|
||||
|
||||
const TaskSidebar: React.FC<TaskSidebarProps> = ({
|
||||
@ -13,6 +14,7 @@ const TaskSidebar: React.FC<TaskSidebarProps> = ({
|
||||
tickets,
|
||||
onSelectTicket,
|
||||
onAddTicket,
|
||||
onEditTicket,
|
||||
}) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -23,8 +25,8 @@ const TaskSidebar: React.FC<TaskSidebarProps> = ({
|
||||
<Card
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
description={item.id}
|
||||
onClick={() => onSelectTicket(item)}
|
||||
onEdit={() => onEditTicket(item.id)}
|
||||
/>
|
||||
)}
|
||||
onAddItem={onAddTicket}
|
||||
|
Loading…
Reference in New Issue
Block a user