From 185615ca1206e2b5b3241ab75486847f6092dfb0 Mon Sep 17 00:00:00 2001 From: Yogesh070 Date: Mon, 21 Aug 2023 18:47:14 +0545 Subject: [PATCH] label crud done --- src/components/BorderedContainer.tsx | 11 ++ src/components/Item/ItemDetailsModal.tsx | 4 +- src/components/Label/LabelForm.tsx | 177 ++++++++++++++++++ src/layout/SettingsLayout.tsx | 64 +++++++ .../projects/[projectId]/settings/index.tsx | 42 ++--- .../[projectId]/settings/labels/index.tsx | 170 +++++++++++++++++ src/store/project.store.ts | 116 ++++++++++++ src/styles/globals.css | 12 +- 8 files changed, 563 insertions(+), 33 deletions(-) create mode 100644 src/components/BorderedContainer.tsx create mode 100644 src/components/Label/LabelForm.tsx create mode 100644 src/layout/SettingsLayout.tsx create mode 100644 src/pages/w/[workspaceId]/projects/[projectId]/settings/labels/index.tsx diff --git a/src/components/BorderedContainer.tsx b/src/components/BorderedContainer.tsx new file mode 100644 index 0000000..6ae3de6 --- /dev/null +++ b/src/components/BorderedContainer.tsx @@ -0,0 +1,11 @@ +import {theme} from 'antd'; +import React from 'react'; + +const {useToken} = theme; + +const BorderedContainer = (props:React.DetailedHTMLProps, HTMLDivElement>) => { + const {token} = useToken(); + return
{props.children}
; +}; + +export default BorderedContainer; diff --git a/src/components/Item/ItemDetailsModal.tsx b/src/components/Item/ItemDetailsModal.tsx index 071770b..7d1ed0e 100644 --- a/src/components/Item/ItemDetailsModal.tsx +++ b/src/components/Item/ItemDetailsModal.tsx @@ -18,7 +18,7 @@ import {api} from '../../utils/api'; import Checklist from '../Checklist/Checklist'; import {useProjectStore} from '../../store/project.store'; import {useState} from 'react'; -import LabelSelect from '../LabelDropdown/LabelSelect'; +import LabelSelect from '../Label/LabelDropdown/LabelSelect'; import dayjs from 'dayjs'; import React from 'react'; @@ -247,7 +247,7 @@ const ItemDetailsModal: React.FC = ( } options={[ { - value: null, + value: undefined, label: 'Unassigned', }, ...userOptions, diff --git a/src/components/Label/LabelForm.tsx b/src/components/Label/LabelForm.tsx new file mode 100644 index 0000000..9a91965 --- /dev/null +++ b/src/components/Label/LabelForm.tsx @@ -0,0 +1,177 @@ +import {Tag, Form, Input, Button, ColorPicker} from 'antd'; +import type {FormInstance} from 'antd/es/form'; +import {useRouter} from 'next/router'; +import React, {useEffect} from 'react'; +import {useProjectStore} from '../../store/project.store'; +import {api} from '../../utils/api'; + +import {ReloadOutlined} from '@ant-design/icons'; +import type {Label} from '@prisma/client'; + +type FieldType = { + title: string; + description?: string; + color: string; +}; + +interface Props { + form: FormInstance; + forEdit?: boolean; + onCancel?: (e: React.MouseEvent) => void; + layout?: 'vertical' | 'horizontal'; + onFinish?: (value: Label) => void; +} + +const LabelForm = (props: Props) => { + const router = useRouter(); + const {projectId} = router.query; + const {addLabel, editLabel} = useProjectStore(); + + useEffect(() => { + if (props.forEdit) { + props.form.setFieldsValue({ + title: props.form.getFieldValue('title'), + description: props.form.getFieldValue('description'), + color: props.form.getFieldValue('color'), + }); + } + }, [props.forEdit, props.form]); + + const {mutate: createLabel, isLoading: isCreating} = + api.project.createProjectLabels.useMutation({ + onSuccess: (data) => { + addLabel(data); + props.form.resetFields(); + props.onFinish?.(data); + }, + }); + + const {mutate: editLabelAPI, isLoading: isEditing} = + api.project.updateProjectLabel.useMutation({ + onSuccess: (data) => { + editLabel(data); + props.onFinish?.(data); + }, + }); + + const handleSubmit = (values: FieldType) => { + if (props.forEdit) { + editLabelAPI({ + id: props.form.getFieldValue('id'), + title: values.title, + color: values.color, + description: values.description, + }); + return; + } + createLabel({ + projectId: projectId as string, + title: values.title, + color: values.color, + description: values.description, + }); + }; + + const getRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; ++i) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; + }; + const color: string = Form.useWatch('color', props.form); + const titleValue: string = Form.useWatch('name', props.form); + const getNameValue = () => { + if (titleValue === undefined || titleValue.length === 0) + return 'Label preview'; + + return titleValue ?? 'Label preview'; + }; + + return ( + <> + + {getNameValue()} + +
+ + name="title" + label="Title" + rules={[{required: true}, {min: 4}]} + > + + + name="description" label="Description"> + + +
+ name="color" label="Color" required> + { + props.form.setFieldsValue({color: hex}); + }} + presets={[ + { + label: 'Recommended', + colors: [ + '#F5222D', + '#FA8C16', + '#FADB14', + '#8BBB11', + '#52C41A', + '#13A8A8', + '#1677FF', + '#2F54EB', + '#722ED1', + '#EB2F96', + ], + }, + ]} + /> + +
+ + +
+ + +
+
+ + + ); +}; + +export default LabelForm; diff --git a/src/layout/SettingsLayout.tsx b/src/layout/SettingsLayout.tsx new file mode 100644 index 0000000..2d18291 --- /dev/null +++ b/src/layout/SettingsLayout.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Board from './Board'; + +import type {ReactNode} from 'react'; +import {Button, Divider, Typography} from 'antd'; +import {useRouter} from 'next/router'; + +import {InfoCircleOutlined, TagsOutlined} from '@ant-design/icons'; + +const {Title} = Typography; + +const SettingsLayout = ({children}: {children: ReactNode}) => { + const settingsOptions = [ + { + icon: , + label: 'Overview', + route: 'settings', + href: '', + }, + { + icon: , + label: 'Labels', + route: 'labels', + href: 'labels', + }, + ]; + const router = useRouter(); + return ( + <> + + Settings +
+ {settingsOptions.map((option, index) => { + const key = String(index + 1); + return ( + + ); + })} +
+ + {children} +
+ + ); +}; + +export default SettingsLayout; diff --git a/src/pages/w/[workspaceId]/projects/[projectId]/settings/index.tsx b/src/pages/w/[workspaceId]/projects/[projectId]/settings/index.tsx index 33cff3d..059902c 100644 --- a/src/pages/w/[workspaceId]/projects/[projectId]/settings/index.tsx +++ b/src/pages/w/[workspaceId]/projects/[projectId]/settings/index.tsx @@ -1,17 +1,15 @@ import React from 'react'; -import Board from '../../../../../../layout/Board'; -import { Button, Input, Form, Select, Badge, Breadcrumb, message, Skeleton } from 'antd'; +import { Button, Input, Form, Select, Badge, message, Skeleton, Typography } from 'antd'; import { api } from '../../../../../../utils/api'; import { useRouter } from 'next/router'; import Image from 'next/image'; import AddUserPopUp from '../../../../../../components/AddUserPopUp.tsx/AddUserPopUp'; import CustomDivider from '../../../../../../components/CustomDivider/CustomDivider'; -import Link from 'next/link'; import { ProjectStatus } from '@prisma/client' -import { HomeOutlined } from '@ant-design/icons'; import type { FormInstance } from 'antd/lib/form/Form'; import type { User } from '@prisma/client'; +import SettingsLayout from '../../../../../../layout/SettingsLayout'; type FormInitialValues = { name: string | undefined; @@ -19,6 +17,8 @@ type FormInitialValues = { defaultAssignee: string | undefined | null; }; +const { Text, Title } = Typography; + const Settings = () => { const router = useRouter(); @@ -86,30 +86,13 @@ const Settings = () => { return ( <> - , - }, - { - title: ( - - {projectDetails.data?.name} - - ), - }, - { - title: 'Settings', - }, - ]} - /> -
+
{'dp'}
-

{projectDetails.data?.name} {projectDetails.data?.status == ProjectStatus.ACTIVE ? : + {projectDetails.data?.name} {projectDetails.data?.status == ProjectStatus.ACTIVE ? <Badge status="success" /> : <Badge status="error" />} - </h1> +

@@ -157,12 +140,12 @@ const Settings = () => { /> - +
-

Danger Zone

-

When deleting a project, all of the data and resources within that project will be permanently removed and cannot be recovered. -

+ Danger Zone + When deleting a project, all of the data and resources within that project will be permanently removed and cannot be recovered. +
+ +
+ ); + }, + }, + ]; + + return ( + <> +
+ Labels + +
+ {isFormVisible && ( + + { + setIsFormVisible(!isFormVisible); + }} + /> + + )} + + + { + handleCancel(); + }} + /> + + + ); +}; + +export default Labels; + +Labels.getLayout = function getLayout(page: React.ReactElement) { + return {page}; +}; diff --git a/src/store/project.store.ts b/src/store/project.store.ts index a69394e..2c02fc8 100644 --- a/src/store/project.store.ts +++ b/src/store/project.store.ts @@ -3,21 +3,33 @@ import type { RouterOutputs } from '../utils/api'; type Project = RouterOutputs['project']['getProjectById']; type ProjectWorkflowWithIssues = RouterOutputs['project']['getProjectWorkflows']['workflows'][number]; +type CheckList = RouterOutputs['issue']['getChecklistsInIssue'][0]; +type Comment = RouterOutputs['issue']['getCommentsByIssueId']; +type Label = RouterOutputs['project']['getProjectLabels'][number]; type State = { project: Project | null, workflows : ProjectWorkflowWithIssues[], + labels : Label[], } type Action = { setProject: (project: Project) => void, setProjectWorkflows: (workflows: ProjectWorkflowWithIssues[]) => void, addIssueToWorkflow: (workflowId: string, issue: ProjectWorkflowWithIssues['issue'][number]) => void, + setLabels: (labels: Label[]) => void, + addLabel: (label: Label) => void, + deleteLabel: (labelId: String) => void, + editLabel: (label: Label) => void, + setChecklist: (workflowId:string ,issueId: string, checklist: CheckList[]) => void, + setComment: (workflowId:string ,issueId: string, comment: Comment[]) => void, + deleteComment : (workflowId:string ,issueId: string, commentId: string) => void, } export const useProjectStore = create()((set) => ({ project: null, workflows: [], + labels: [], setProject: (project) => set({ project }), setProjectWorkflows: (workflows) => set({ workflows }), addIssueToWorkflow(workflowId, issue) { @@ -32,5 +44,109 @@ export const useProjectStore = create()((set) => ({ return workflow; }), })); + }, + setLabels(labels) { + set({ labels }); + }, + addLabel(label) { + set((state) => ({ labels: [...state.labels, label] })); + }, + deleteLabel(labelId) { + set((state) => ({ labels: state.labels.filter((l) => l.id !== labelId) })); + }, + editLabel(label) { + set((state) => ({ + labels: state.labels.map((l) => { + if (l.id === label.id) { + return label; + } + return l; + }), + })); + }, + setChecklist(workFlowId, issueId, checklist) { + + set((state) => { + const workflows = state.workflows.map((workflow) => { + if (workflow.id === workFlowId) { + return { + ...workflow, + issue: workflow.issue.map((issue) => { + if (issue.id === issueId) { + return { + ...issue, + checklist, + }; + } + return issue; + }), + }; + } + return workflow; + }); + return { workflows }; + }); + + //if i cannot have workflowId as a parameter, then i can do this + // set((state) => ({ + // workflows: state.workflows.map((workflow) => { + // return { + // ...workflow, + // issue: workflow.issue.map((issue) => { + // if (issue.id === issueId) { + // return { + // ...issue, + // checklist, + // }; + // } + // return issue; + // }), + // }; + // } + // )} + // )); + }, + setComment(workFlowId, issueId, comment) { + set((state) => ({ + workflows: state.workflows.map((workflow) => { + if (workflow.id === workFlowId) { + return { + ...workflow, + issue: workflow.issue.map((issue) => { + if (issue.id === issueId) { + return { + ...issue, + comment, + }; + } + return issue; + }), + }; + } + return workflow; + }), + })); + } + , + deleteComment(workFlowId, issueId, commentId) { + set((state) => ({ + workflows: state.workflows.map((workflow) => { + if (workflow.id === workFlowId) { + return { + ...workflow, + issue: workflow.issue.map((issue) => { + if (issue.id === issueId) { + return { + ...issue, + // comment: issue.comment.filter((comment) => comment.id !== commentId), + }; + } + return issue; + }), + }; + } + return workflow; + }), + })); } })); diff --git a/src/styles/globals.css b/src/styles/globals.css index 32533f8..a99eb2e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -78,6 +78,10 @@ a { align-items: center; } +.items-end{ + align-items: flex-end; +} + .justify-center { justify-content: center; } @@ -86,6 +90,10 @@ a { justify-content: space-between; } +.justify-end { + justify-content: flex-end; +} + .gap-1 { gap: 1rem; } @@ -224,7 +232,9 @@ a { .m-4 { margin: 16px; } - +.mb-0{ + margin-bottom: 0 !important; +} .mb-1-2 { margin-bottom: 0.5rem; }