dev: promote stage release to production (#155)

* refractor: removed modules from user.context

* refractor: removed cycles from user context

* refractor: removed state from user context

* feat: implement channel protocol for tracking issue-activites

* refactor: remove blocking code and add todo

* refactor: refactor the consumer with function modules

* feat: add columns for identifiers for easier redirection

* style: minor padding, coloring and consistency changes

* feat: track blocker issues

* feat: track issue after creation

* feat: add runworker in procfile

* refractor: moved all context provider to _app for more clarity

* dev: added our icons

* refractor: removed issues from user context

* refactor: rename db names to plural and remove admin register file

* refactor: integrate permission layer in endpoints

* feat: create product email html templates

* refractor: changed to getServerSide from getInitialProps, removed unused component imports and minor refractoring

* feat: remirror added

* feat: workspace member user details endpoint

* fix: resolved build issue

* refactor:  remove www

* feat: workspace details on user endpoint

* feat: added authorization in project settings

refractor: improved code readability

* fix: removed hard-coded workspace slug value, and added workspace in user interface

* refactor: invitation workflow for already existing users

* feat: modified remirror, fix: issue details sidebar

* fix: merge conflicts

* fix: merge conflicts

* fix: added missing dependencies

* refactor: remove user dependency from invitations

* refactor: issue description context is updated with manager

* dev: redis instance rewrite for ssl settings and remove REDIS_TLS env variable

* chore: upgrade python package requirements

* dev: added new migrations for changes

* dev: ssl config for django channels redis connection

* chore: upgrade channels requirements

* refactor: better function for connecting with redis ssl django channels

* chore: cleanup on manifest file

* revert: user endpoint changes

* build: setup asgi

* refactor: update invitation endpoint to do bulk operations

* style: cycles page, custom listbox, issue details page

* refractor: removed folder that were moved to workspaceSlug

* dev: uvicorn in requirements

* Update index.tsx

* refactor: get workspace slug on user endpoint

* fix: workspace slug redirections and slug value in user context

* fix: user context bugs, drag and drop in cycles and modules

* fix: merge conflicts

* fix: user context and create issue modal

* refactor: add extra columns for json and html description and script for back migrating old issues

* refactor: move all 500 errors to 400

* refractor: removed active project, active workspace, projects, and workspaces from user context

* refractor: change from /home to /, added home page redirection logic

added explict GET method on fetch request, and fixed invitation page not fetching all invitations

* fix: passing project id in command palette

* style: home page, feat: image in remirror

* fix: bugs

* chore: remove test_runner workflow from github actions

* dev: update Procfile worker count and python runtime upgrade

* refactor: update response from 404 to 403

* feat: filtering using both name and issue identifier in command palette

showing my issues instead of project issue in command palette, hiding again according to route in command palette

* fix: mutation on different CRUD operations

* fix: redirection in my issues pages

* feat: added authorization in workspace settings, moved command palette to app-layout

* feat: endpoint and column to store my issue props

* style: authorization new design,

fix: made whole button on authorization page clickable, lib/auth on unsuccessful api call redirecting to error page

* feat: return project details on modules and cycles

* fix: create cycle and state coming below issue modal, showing loader for rich text editor

refractor: changed from sprint to cycle in issue type

* fix: issue delete mustation

and some code refractor

* fix: mutation bugs, remirror bugs, style: consistent droopdowns and buttons

* feat: user role in model

* dev: added new migrations

* fix: add url for workspace availability check

* feat: onboarding screens

* fix: update url for workspace name check and add authentication layer and
fix invitation endpoint

* refactor: bulk invitations message

* refactor: response on workspace invitarions

* refactor: update identifier endpoint

* refactor: invitations endpoint

* feat: onboarding logic and validations

* fix: email striep

* dev: added workspace space member unique_together

* chore: back populate neccesary data for description field

* feat: emoji-picker gets close on select, public will be default option in create project

* fix: update error in project creation

* fix: mutation error on issue count in kanban view

some minor code refractoring

* fix: module bugs

* fix: issue activities and issue comments mutation handled at issue detail

* fix: error message for creating updates without permissions

* fix: showing no user left to invite in project invite

fix: - mutation in project settings control, style: - showing loader in project settings controller, - showing request pending for user that hasn't accepted invitation

* refactor: file asset upload directory

* fix: update last workspace id on user invitation accept

* style: onboarding screens

* style: cycles, issue activity

* feat: add json and html column in issue comments

* fix: submitting create issue modal on enter click, project not getting deselected

* feat: file size validator

* fix: emoji picker not closing on all emoji select

* feat: added validation in identifier such that it only accept uppercase text

* dev: commenting is now richer

* fix: shortcuts not getting opened in settings layouts

* style: showing sidebar on unauthorized pages

* fix: error code on exception

* fix: add issue button is working on my issues pages

* feat: new way of assets

* fix: updated activity content for description field

* fix: mutation on project settings control

style: blocker and blocked changed to outline button

* fix: description activity logging

* refactor: check for workspace slug on workspace creation

* fix: typo on workspace url check

* fix: workspace name uniqueness

* fix: remove workspace from read only field

* fix: file upload endpoint, workspace slug check

* chore: drop unique_together constraint for name and workspace

* chore: settings files cleanup and use PubSub backend on django channels

* chore: change in channels backend

* refactor: issue activity api to combine comments

* fix: instance created at key

* fix: result list

* style: create project, cycle modal, view dropdown

* feat: merged issue activities and issue comments into a single section

* fix: remirror dynamic update of issue description

* fix: removed commented code

* fix: issue acitivties mutation

* fix: empty comments cant be submitted

* fix: workspace avatar has been updated while loading

* refactor: update docker-compose to run redis and database in heroku and docker environment

* refactor: removesingle docker file configuration

* refactor: update take off script to run in asgi

* docs: added workspace, quickstart documentation

* fix: reading editor values on focus out

* refactor: cleanup environment variables and create .env.example

* refactor: add extra variables in example env

* fix: warning and erros on console

lazy loading images with low priority, added validation on onboarding for user to either join or create workspace, on onboarding user can't click button while form is getting submitted, profile page going into loading state when updated, refractor: made some state local, removed unnecessary console logs and comments, changed some variable and function name to make more sence

* feat: env examples

* fix: workspace member does not exist

* fi: remove pagination from issue list api

* refactor: remove env example from root

* feat: documentation for projects on plane

* feat: create code of conduct and contributing guidelines

* fix: update docker setup to check handle redis

* revert: bring back pagination to avoid breaking

* feat: made image uploader modal, used it in profile page and workspace page,

delete project from project settings page, join project modal in project list page

* feat: create workspace page, style: made ui consistent

* style: updated onboarding and create workspace page design

* style: responsive sidebar

* fix: updated ui imports
This commit is contained in:
Vamsi Kurama 2023-01-10 23:55:47 +05:30 committed by GitHub
parent a960ddedf7
commit bef166a65f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
395 changed files with 20119 additions and 18322 deletions

View file

@ -1,79 +1,84 @@
import React, { useCallback, useEffect, useState } from "react";
// next
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// react beautiful dnd
import useSWR, { mutate } from "swr";
import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext } from "react-beautiful-dnd";
// hook
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// services
import stateServices from "lib/services/state.service";
import issuesServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
import projectService from "lib/services/project.service";
// fetching keys
import { STATE_LIST } from "constants/fetch-keys";
import { STATE_LIST, PROJECT_ISSUES_LIST, PROJECT_MEMBERS } from "constants/fetch-keys";
// components
import SingleBoard from "components/project/issues/BoardView/single-board";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
// ui
import { Spinner } from "ui";
// types
import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types";
import ConfirmIssueDeletion from "../confirm-issue-deletion";
import type { IState, IIssue, NestedKeyOf, IssueResponse } from "types";
type Props = {
properties: Properties;
selectedGroup: NestedKeyOf<IIssue> | null;
groupedByIssues: {
[key: string]: IIssue[];
};
members: IProjectMember[] | undefined;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
};
const BoardView: React.FC<Props> = ({
properties,
selectedGroup,
groupedByIssues,
members,
handleDeleteIssue,
partialUpdateIssue,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isIssueOpen, setIsIssueOpen] = useState(false);
const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false);
const [issueDeletionData, setIssueDeletionData] = useState<IIssue | undefined>();
const [preloadedData, setPreloadedData] = useState<
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
const { activeWorkspace } = useUser();
const router = useRouter();
const { projectId } = router.query;
const { workspaceSlug, projectId } = router.query;
const { data: states, mutate: mutateState } = useSWR<IState[]>(
projectId && activeWorkspace ? STATE_LIST(projectId as string) : null,
activeWorkspace
? () => stateServices.getStates(activeWorkspace.slug, projectId as string)
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug
? () => stateServices.getStates(workspaceSlug as string, projectId as string)
: null
);
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null,
{
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
if (err?.status === 403) return;
setTimeout(() => revalidate(revalidateOpts), 5000);
},
}
);
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const handleOnDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) return;
const { source, destination, type } = result;
const draggedItem = groupedByIssues[source.droppableId][source.index];
if (destination.droppableId === "trashBox") {
const removedItem = groupedByIssues[source.droppableId][source.index];
setIssueDeletionData(removedItem);
setIssueDeletionData(draggedItem);
setIsIssueDeletionOpen(true);
} else {
if (type === "state") {
@ -95,10 +100,10 @@ const BoardView: React.FC<Props> = ({
newStates[destination.index].sequence = sequenceNumber;
mutateState(newStates, false);
if (!activeWorkspace) return;
if (!workspaceSlug) return;
stateServices
.patchState(
activeWorkspace.slug,
workspaceSlug as string,
projectId as string,
newStates[destination.index].id,
{
@ -115,20 +120,18 @@ const BoardView: React.FC<Props> = ({
if (source.droppableId !== destination.droppableId) {
const sourceGroup = source.droppableId; // source group id
const destinationGroup = destination.droppableId; // destination group id
if (!sourceGroup || !destinationGroup) return;
// removed/dragged item
const removedItem = groupedByIssues[source.droppableId][source.index];
if (!sourceGroup || !destinationGroup) return;
if (selectedGroup === "priority") {
// update the removed item for mutation
removedItem.priority = destinationGroup;
draggedItem.priority = destinationGroup;
// patch request
issuesServices.patchIssue(
activeWorkspace!.slug,
workspaceSlug as string,
projectId as string,
removedItem.id,
draggedItem.id,
{
priority: destinationGroup,
}
@ -139,39 +142,52 @@ const BoardView: React.FC<Props> = ({
// update the removed item for mutation
if (!destinationStateId || !destinationState) return;
removedItem.state = destinationStateId;
removedItem.state_detail = destinationState;
draggedItem.state = destinationStateId;
draggedItem.state_detail = destinationState;
// patch request
issuesServices.patchIssue(
activeWorkspace!.slug,
workspaceSlug as string,
projectId as string,
removedItem.id,
draggedItem.id,
{
state: destinationStateId,
}
);
}
// remove item from the source group
groupedByIssues[source.droppableId].splice(source.index, 1);
// add item to the destination group
groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem);
// mutate the issues
if (!workspaceSlug || !projectId) return;
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
(prevData) => {
if (!prevData) return prevData;
const updatedIssues = prevData.results.map((issue) => {
if (issue.id === draggedItem.id)
return {
...draggedItem,
state_detail: destinationState,
state: destinationStateId,
};
return issue;
});
return {
...prevData,
results: updatedIssues,
};
},
false
);
}
}
}
}
},
[activeWorkspace, mutateState, groupedByIssues, projectId, selectedGroup, states]
[workspaceSlug, mutateState, groupedByIssues, projectId, selectedGroup, states]
);
useEffect(() => {
if (isOpen) return;
const timer = setTimeout(() => {
setPreloadedData(undefined);
clearTimeout(timer);
}, 500);
}, [isOpen]);
return (
<>
<ConfirmIssueDeletion
@ -198,7 +214,7 @@ const BoardView: React.FC<Props> = ({
{...provided.droppableProps}
ref={provided.innerRef}
>
<div className="flex gap-x-4 h-full overflow-x-auto overflow-y-hidden pb-3">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden pb-3">
{Object.keys(groupedByIssues).map((singleGroup, index) => (
<SingleBoard
key={singleGroup}
@ -223,7 +239,7 @@ const BoardView: React.FC<Props> = ({
bgColor={
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.color
: undefined
: "#000000"
}
handleDeleteIssue={handleDeleteIssue}
partialUpdateIssue={partialUpdateIssue}
@ -238,7 +254,7 @@ const BoardView: React.FC<Props> = ({
</DragDropContext>
</div>
) : (
<div className="h-full w-full flex justify-center items-center">
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}

View file

@ -1,14 +1,13 @@
// react
import React, { useState } from "react";
// swr
import { useRouter } from "next/router";
import useSWR from "swr";
// react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import workspaceService from "lib/services/workspace.service";
// hooks
import useUser from "lib/hooks/useUser";
// icons
import {
ArrowsPointingInIcon,
@ -61,9 +60,10 @@ const SingleBoard: React.FC<Props> = ({
partialUpdateIssue,
}) => {
// Collapse/Expand
const [show, setShow] = useState(true);
const [isCollapsed, setIsCollapsed] = useState(true);
const { activeWorkspace } = useUser();
const router = useRouter();
const { workspaceSlug } = router.query;
if (selectedGroup === "priority")
groupTitle === "high"
@ -75,40 +75,42 @@ const SingleBoard: React.FC<Props> = ({
: (bgColor = "#ff0000");
const { data: people } = useSWR<IWorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
return (
<Draggable draggableId={groupTitle} index={index}>
{(provided, snapshot) => (
<div
className={`rounded flex-shrink-0 h-full ${
className={`h-full flex-shrink-0 rounded ${
snapshot.isDragging ? "border-theme shadow-lg" : ""
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
} ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
<div
className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}
>
<div
className={`flex justify-between p-3 pb-0 ${
!show ? "flex-col bg-gray-50 rounded-md border" : ""
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`}
>
<div className={`flex items-center ${!show ? "flex-col gap-2" : "gap-1"}`}>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<button
type="button"
{...provided.dragHandleProps}
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
!show ? "" : "rotate-90"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
!isCollapsed ? "" : "rotate-90"
} ${selectedGroup !== "state_detail.name" ? "hidden" : ""}`}
>
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
<EllipsisHorizontalIcon className="mt-[-0.7rem] h-4 w-4 text-gray-600" />
</button>
<div
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
style={{
border: `2px solid ${bgColor}`,
@ -118,7 +120,7 @@ const SingleBoard: React.FC<Props> = ({
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !show ? "vertical-rl" : "horizontal-tb",
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
@ -127,21 +129,21 @@ const SingleBoard: React.FC<Props> = ({
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="text-gray-500 text-sm ml-0.5">
<span className="ml-0.5 text-sm text-gray-500">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
<div className={`flex items-center ${!show ? "flex-col pb-2" : ""}`}>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setShow(!show);
setIsCollapsed((prevData) => !prevData);
}}
>
{show ? (
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
@ -149,7 +151,7 @@ const SingleBoard: React.FC<Props> = ({
</button>
<button
type="button"
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setIsIssueOpen(true);
if (selectedGroup !== null)
@ -167,9 +169,9 @@ const SingleBoard: React.FC<Props> = ({
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
className={`mt-3 h-full space-y-3 overflow-y-auto px-3 pb-3 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!show ? "hidden" : "block"}`}
} ${!isCollapsed ? "hidden" : "block"}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
@ -212,7 +214,7 @@ const SingleBoard: React.FC<Props> = ({
{provided.placeholder}
<button
type="button"
className="flex items-center text-xs font-medium hover:bg-gray-100 p-2 rounded duration-300 outline-none"
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
onClick={() => {
setIsIssueOpen(true);
if (selectedGroup !== null) {
@ -224,7 +226,7 @@ const SingleBoard: React.FC<Props> = ({
}
}}
>
<PlusIcon className="h-3 w-3 mr-1" />
<PlusIcon className="mr-1 h-3 w-3" />
Create
</button>
</div>

View file

@ -1,14 +1,15 @@
import React, { useEffect, useRef, useState } from "react";
// swr
import { mutate } from "swr";
// headless ui
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
// services
import stateServices from "lib/services/state.service";
import issuesServices from "lib/services/issues.service";
// fetch api
import { STATE_LIST } from "constants/fetch-keys";
// hooks
import useUser from "lib/hooks/useUser";
import { STATE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// common
import { groupBy } from "constants/common";
// icons
@ -26,10 +27,19 @@ type Props = {
const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, onClose, data }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [issuesWithThisStateExist, setIssuesWithThisStateExist] = useState(true);
const { activeWorkspace, issues } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const cancelButtonRef = useRef(null);
@ -40,9 +50,9 @@ const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, onClose, data }) => {
const handleDeletion = async () => {
setIsDeleteLoading(true);
if (!data || !activeWorkspace || issuesWithThisStateExist) return;
if (!data || !workspaceSlug || issuesWithThisStateExist) return;
await stateServices
.deleteState(activeWorkspace.slug, data.project, data.id)
.deleteState(workspaceSlug as string, data.project, data.id)
.then(() => {
mutate<IState[]>(
STATE_LIST(data.project),

View file

@ -1,11 +1,13 @@
import React, { useEffect } from "react";
// swr
import { useRouter } from "next/router";
import { mutate } from "swr";
// react hook form
import { Controller, useForm } from "react-hook-form";
// react color
import { TwitterPicker } from "react-color";
// headless
import { Dialog, Popover, Transition } from "@headlessui/react";
// services
import stateService from "lib/services/state.service";
@ -13,8 +15,6 @@ import stateService from "lib/services/state.service";
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Button, Input, Select, TextArea } from "ui";
// icons
@ -37,15 +37,8 @@ const defaultValues: Partial<IState> = {
};
const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, handleClose }) => {
const onClose = () => {
handleClose();
const timeout = setTimeout(() => {
reset(defaultValues);
clearTimeout(timeout);
}, 500);
};
const { activeWorkspace } = useUser();
const router = useRouter();
const { workspaceSlug } = router.query;
const {
register,
@ -59,14 +52,27 @@ const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, hand
defaultValues,
});
useEffect(() => {
if (data) {
reset(data);
} else {
reset(defaultValues);
}
}, [data, reset]);
const onClose = () => {
handleClose();
reset(defaultValues);
};
const onSubmit = async (formData: IState) => {
if (!activeWorkspace) return;
if (!workspaceSlug) return;
const payload: IState = {
...formData,
};
if (!data) {
await stateService
.createState(activeWorkspace.slug, projectId, payload)
.createState(workspaceSlug as string, projectId, payload)
.then((res) => {
mutate<IState[]>(STATE_LIST(projectId), (prevData) => [...(prevData ?? []), res], false);
onClose();
@ -80,7 +86,7 @@ const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, hand
});
} else {
await stateService
.updateState(activeWorkspace.slug, projectId, data.id, payload)
.updateState(workspaceSlug as string, projectId, data.id, payload)
.then((res) => {
mutate<IState[]>(
STATE_LIST(projectId),
@ -107,17 +113,9 @@ const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, hand
}
};
useEffect(() => {
if (data) {
reset(data);
} else {
reset(defaultValues);
}
}, [data, reset]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Dialog as="div" className="relative z-30" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -185,14 +183,14 @@ const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, hand
{({ open }) => (
<>
<Popover.Button
className={`group bg-white rounded-md inline-flex items-center text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
className={`group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
open ? "text-gray-900" : "text-gray-500"
}`}
>
<span>Color</span>
{watch("color") && watch("color") !== "" && (
<span
className="w-4 h-4 ml-2 rounded"
className="ml-2 h-4 w-4 rounded"
style={{
backgroundColor: watch("color") ?? "green",
}}
@ -215,7 +213,7 @@ const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, hand
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="fixed z-50 transform left-5 mt-3 px-2 w-screen max-w-xs sm:px-0">
<Popover.Panel className="fixed left-5 z-50 mt-3 w-screen max-w-xs transform px-2 sm:px-0">
<Controller
name="color"
control={control}

View file

@ -1,21 +1,22 @@
import React, { useRef, useState } from "react";
// swr
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// fetching keys
import { CYCLE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
import { CYCLE_ISSUES, PROJECT_ISSUES_LIST, MODULE_ISSUES } from "constants/fetch-keys";
// services
import issueServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { Button } from "ui";
// types
import type { IIssue, IssueResponse } from "types";
import type { CycleIssueResponse, IIssue, IssueResponse, ModuleIssueResponse } from "types";
type Props = {
isOpen: boolean;
@ -23,14 +24,20 @@ type Props = {
data?: IIssue;
};
const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) => {
const ConfirmIssueDeletion: React.FC<Props> = (props) => {
const { isOpen, handleClose, data } = props;
const cancelButtonRef = useRef(null);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const cancelButtonRef = useRef(null);
useEffect(() => {
setIsDeleteLoading(false);
}, [isOpen]);
const onClose = () => {
setIsDeleteLoading(false);
@ -39,13 +46,13 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
const handleDeletion = async () => {
setIsDeleteLoading(true);
if (!data || !activeWorkspace) return;
if (!data || !workspaceSlug) return;
const projectId = data.project;
await issueServices
.deleteIssue(activeWorkspace.slug, projectId, data.id)
.deleteIssue(workspaceSlug as string, projectId, data.id)
.then(() => {
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(activeWorkspace.slug, projectId),
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId),
(prevData) => {
return {
...(prevData as IssueResponse),
@ -55,7 +62,25 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
},
false
);
mutate(CYCLE_ISSUES(data.issue_cycle?.id ?? ""));
const moduleId = data.issue_module?.module;
const cycleId = data.issue_cycle?.cycle;
if (moduleId) {
mutate<ModuleIssueResponse[]>(
MODULE_ISSUES(moduleId),
(prevData) => prevData?.filter((i) => i.issue !== data.id),
false
);
}
if (cycleId) {
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId),
(prevData) => prevData?.filter((i) => i.issue !== data.id),
false
);
}
setToastAlert({
title: "Success",
type: "success",
@ -99,7 +124,7 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div>
<div className="mx-auto h-16 w-16 grid place-items-center rounded-full bg-red-100">
<div className="mx-auto grid h-16 w-16 place-items-center rounded-full bg-red-100">
<ExclamationTriangleIcon
className="h-8 w-8 text-red-600"
aria-hidden="true"
@ -107,10 +132,10 @@ const ConfirmIssueDeletion: React.FC<Props> = ({ isOpen, handleClose, data }) =>
</div>
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 mt-3"
className="mt-3 text-lg font-medium leading-6 text-gray-900"
>
Are you sure you want to delete {`"`}
{activeProject?.identifier}-{data?.sequence_id} - {data?.name}?{`"`}
{data?.project_detail.identifier}-{data?.sequence_id} - {data?.name}?{`"`}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">

View file

@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react";
// next
import dynamic from "next/dynamic";
import Link from "next/link";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
// swr
import { mutate } from "swr";
// react hook form
import useSWR, { mutate } from "swr";
import { Controller, useForm } from "react-hook-form";
// headless
import { Dialog, Menu, Transition } from "@headlessui/react";
// services
import issuesServices from "lib/services/issues.service";
@ -15,9 +15,9 @@ import issuesServices from "lib/services/issues.service";
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// ui
import { Button, TextArea } from "ui";
import { Button, Input, Loader } from "ui";
// icons
import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
import { EllipsisHorizontalIcon, XMarkIcon } from "@heroicons/react/24/outline";
// components
import SelectState from "components/project/issues/create-update-issue-modal/select-state";
import SelectCycles from "components/project/issues/create-update-issue-modal/select-cycle";
@ -36,12 +36,21 @@ import {
PROJECT_ISSUES_LIST,
CYCLE_ISSUES,
USER_ISSUE,
PROJECTS_LIST,
MODULE_ISSUES,
} from "constants/fetch-keys";
// common
import { renderDateFormat, cosineSimilarity } from "constants/common";
import projectService from "lib/services/project.service";
import modulesService from "lib/services/modules.service";
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader>
<Loader.Item height="12rem" width="100%"></Loader.Item>
</Loader>
),
});
type Props = {
@ -56,9 +65,10 @@ type Props = {
const defaultValues: Partial<IIssue> = {
project: "",
name: "",
// description: "",
description: "",
description_html: "<p></p>",
state: "",
sprints: null,
cycle: null,
priority: null,
labels_list: [],
};
@ -71,31 +81,31 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
prePopulateData,
isUpdatingSingleIssue = false,
}) => {
const [createMore, setCreateMore] = useState(false);
const [isCycleModalOpen, setIsCycleModalOpen] = useState(false);
const [isStateModalOpen, setIsStateModalOpen] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [mostSimilarIssue, setMostSimilarIssue] = useState<string | undefined>();
// const [issueDescriptionValue, setIssueDescriptionValue] = useState("");
// const handleDescriptionChange: any = (value: any) => {
// console.log(value);
// setIssueDescriptionValue(value);
// };
const router = useRouter();
const { workspaceSlug } = router.query;
const handleClose = () => {
setIsOpen(false);
if (data) {
resetForm();
}
};
const { activeWorkspace, activeProject, user, issues } = useUser();
const { user } = useUser();
const { setToastAlert } = useToast();
const { data: issues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId) : null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId)
: null
);
const { data: projects } = useSWR(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null
);
const {
register,
formState: { errors, isSubmitting },
@ -104,21 +114,52 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
setError,
control,
watch,
setValue,
} = useForm<IIssue>({
defaultValues,
mode: "all",
reValidateMode: "onChange",
});
useEffect(() => {
if (data) setIsOpen(true);
}, [data, setIsOpen]);
useEffect(() => {
if (projects && projects.length > 0)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [projectId, projects]);
useEffect(() => {
reset({
...defaultValues,
...watch(),
...data,
project: activeProject ?? "",
...prePopulateData,
});
}, [data, prePopulateData, reset, activeProject, isOpen, watch]);
useEffect(() => {
return () => setMostSimilarIssue(undefined);
}, []);
const resetForm = () => {
const timeout = setTimeout(() => {
reset(defaultValues);
clearTimeout(timeout);
}, 500);
reset({ ...defaultValues, project: activeProject ?? undefined });
};
const handleClose = () => {
setIsOpen(false);
if (data) {
resetForm();
}
};
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!activeWorkspace || !activeProject) return;
if (!workspaceSlug || !projectId) return;
await issuesServices
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, {
.addIssueToCycle(workspaceSlug as string, projectId, cycleId, {
issues: [issueId],
})
.then((res) => {
@ -131,7 +172,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
);
} else
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId),
(prevData) => {
return {
...(prevData as IssueResponse),
@ -143,36 +184,49 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
},
false
);
setToastAlert({
title: "Success",
type: "success",
message: "Issue added to cycle successfully",
});
})
.catch((err) => {
console.log(err);
});
};
const addIssueToModule = async (issueId: string, moduleId: string) => {
if (!workspaceSlug || !projectId) return;
await modulesService
.addIssuesToModule(workspaceSlug as string, projectId, moduleId as string, {
issues: [issueId],
})
.then((res) => {
console.log(res);
mutate(MODULE_ISSUES(moduleId as string));
})
.catch((e) => console.log(e));
};
const onSubmit = async (formData: IIssue) => {
if (!activeWorkspace || !activeProject) return;
if (!workspaceSlug || !projectId) return;
const payload: Partial<IIssue> = {
...formData,
target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null,
// description: formData.description ? JSON.parse(formData.description) : null,
};
if (!data) {
await issuesServices
.createIssues(activeWorkspace.slug, activeProject.id, payload)
.then(async (res) => {
console.log(res);
mutate<IssueResponse>(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id));
if (formData.sprints && formData.sprints !== null) {
await addIssueToCycle(res.id, formData.sprints);
.createIssues(workspaceSlug as string, projectId, payload)
.then((res) => {
mutate<IssueResponse>(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId));
if (formData.cycle && formData.cycle !== null) {
addIssueToCycle(res.id, formData.cycle);
}
handleClose();
if (formData.module && formData.module !== null) {
addIssueToModule(res.id, formData.module);
}
resetForm();
if (!createMore) handleClose();
setToastAlert({
title: "Success",
type: "success",
@ -183,20 +237,31 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
}
})
.catch((err) => {
if (err.detail) {
setToastAlert({
title: "Join the project.",
type: "error",
message: "Click select to join from projects page to start making changes",
});
}
Object.keys(err).map((key) => {
setError(key as keyof IIssue, { message: err[key].join(", ") });
const message = err[key];
if (!message) return;
setError(key as keyof IIssue, {
message: Array.isArray(message) ? message.join(", ") : message,
});
});
});
} else {
await issuesServices
.updateIssue(activeWorkspace.slug, activeProject.id, data.id, payload)
.then(async (res) => {
console.log(res);
.updateIssue(workspaceSlug as string, projectId, data.id, payload)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId),
(prevData) => {
return {
...(prevData as IssueResponse),
@ -205,14 +270,13 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
return issue;
}),
};
},
false
}
);
if (formData.sprints && formData.sprints !== null) {
await addIssueToCycle(res.id, formData.sprints);
if (formData.cycle && formData.cycle !== null) {
addIssueToCycle(res.id, formData.cycle);
}
handleClose();
resetForm();
if (!createMore) handleClose();
setToastAlert({
title: "Success",
type: "success",
@ -227,42 +291,24 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
}
};
useEffect(() => {
if (data) setIsOpen(true);
}, [data, setIsOpen]);
useEffect(() => {
reset({
...defaultValues,
...watch(),
...data,
project: activeProject?.id ?? projectId,
...prePopulateData,
});
}, [data, prePopulateData, reset, projectId, activeProject, isOpen, watch]);
useEffect(() => {
return () => setMostSimilarIssue(undefined);
}, []);
return (
<>
{activeProject && (
{projectId && (
<>
<CreateUpdateStateModal
isOpen={isStateModalOpen}
handleClose={() => setIsStateModalOpen(false)}
projectId={activeProject?.id}
projectId={projectId}
/>
<CreateUpdateCycleModal
isOpen={isCycleModalOpen}
setIsOpen={setIsCycleModalOpen}
projectId={activeProject?.id}
projectId={projectId}
/>
</>
)}
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" onClose={handleClose}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -286,23 +332,53 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform rounded-lg bg-white p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
<SelectProject control={control} />
<SelectProject
control={control}
activeProject={activeProject ?? ""}
setActiveProject={setActiveProject}
/>
<h3 className="text-lg font-medium leading-6 text-gray-900">
{data ? "Update" : "Create"} Issue
</h3>
</div>
{watch("parent") && watch("parent") !== "" ? (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-gray-100 p-2 text-xs">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: issues?.results.find(
(i) => i.id === watch("parent")
)?.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-gray-600">
{projects?.find((p) => p.id === activeProject)?.identifier}-
{issues?.results.find((i) => i.id === watch("parent"))?.sequence_id}
</span>
<span className="truncate font-medium">
{issues?.results
.find((i) => i.id === watch("parent"))
?.name.substring(0, 50)}
</span>
<XMarkIcon
className="h-3 w-3 cursor-pointer"
onClick={() => setValue("parent", null)}
/>
</div>
</div>
) : null}
<div className="space-y-3">
<div className="mt-2 space-y-3">
<div>
<TextArea
<Input
id="name"
label="Name"
label="Title"
name="name"
rows={1}
onChange={(e) => {
const value = e.target.value;
const similarIssue = issues?.results.find(
@ -311,19 +387,23 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
setMostSimilarIssue(similarIssue?.id);
}}
className="resize-none"
placeholder="Enter name"
placeholder="Enter title"
autoComplete="off"
error={errors.name}
register={register}
validations={{
required: "Name is required",
maxLength: {
value: 255,
message: "Name should be less than 255 characters",
},
}}
/>
{mostSimilarIssue && (
<div className="flex items-center gap-x-2">
<p className="text-sm text-gray-500">
<Link
href={`/projects/${activeProject?.id}/issues/${mostSimilarIssue}`}
href={`/${workspaceSlug}/projects/${projectId}/issues/${mostSimilarIssue}`}
>
<a target="_blank" type="button" className="inline text-left">
<span>Did you mean </span>
@ -360,37 +440,31 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
)}
</div>
<div>
<TextArea
id="description"
name="description"
label="Description"
placeholder="Enter description"
error={errors.description}
register={register}
/>
{/* <Controller
<label htmlFor={"description"} className="mb-2 text-gray-500">
Description
</label>
<Controller
name="description"
control={control}
render={({ field }) => (
<RichTextEditor {...field} id="issueDescriptionEditor" />
<RemirrorRichTextEditor
{...field}
onBlur={(jsonValue, htmlValue) => {
setValue("description", jsonValue);
setValue("description_html", htmlValue);
}}
placeholder="Enter Your Text..."
/>
)}
/> */}
/>
</div>
<div>
{/* <Input
id="target_date"
label="Target Date"
name="target_date"
type="date"
placeholder="Enter name"
autoComplete="off"
error={errors.target_date}
register={register}
/> */}
</div>
<div className="flex items-center flex-wrap gap-2">
<div className="flex flex-wrap items-center gap-2">
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
<SelectCycles control={control} setIsOpen={setIsCycleModalOpen} />
<SelectCycles
control={control}
setIsOpen={setIsCycleModalOpen}
activeProject={activeProject ?? ""}
/>
<SelectPriority control={control} />
<SelectAssignee control={control} />
<SelectLabels control={control} />
@ -404,7 +478,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
onChange={(e: any) => {
onChange(e.target.value);
}}
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300"
className="cursor-pointer rounded-md border px-2 py-[3px] text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
)}
/>
@ -415,7 +489,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
issues={issues?.results ?? []}
/>
<Menu as="div" className="relative inline-block">
<Menu.Button className="grid place-items-center p-1 hover:bg-gray-100 border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
<Menu.Button className="grid cursor-pointer place-items-center rounded-md border p-1 py-0.5 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<EllipsisHorizontalIcon className="h-5 w-5" />
</Menu.Button>
@ -428,22 +502,40 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
<div className="p-1">
<Menu.Item as="div">
<button
type="button"
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap "
onClick={() => setParentIssueListModalOpen(true)}
>
{watch("parent") && watch("parent") !== ""
? `${activeProject?.identifier}-${
issues?.results.find((i) => i.id === watch("parent"))
?.sequence_id
}`
: "Select Parent Issue"}
</button>
</Menu.Item>
<Menu.Items className="absolute right-0 z-50 mt-2 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{watch("parent") && watch("parent") !== "" ? (
<>
<Menu.Item as="div">
<button
type="button"
className="whitespace-nowrap p-2 text-left text-xs text-gray-900 hover:bg-indigo-50"
onClick={() => setParentIssueListModalOpen(true)}
>
Change parent issue
</button>
</Menu.Item>
<Menu.Item as="div">
<button
type="button"
className="whitespace-nowrap p-2 text-left text-xs text-gray-900 hover:bg-indigo-50"
onClick={() => setValue("parent", null)}
>
Remove parent issue
</button>
</Menu.Item>
</>
) : (
<Menu.Item as="div">
<button
type="button"
className="whitespace-nowrap p-2 text-left text-xs text-gray-900 hover:bg-indigo-50"
onClick={() => setParentIssueListModalOpen(true)}
>
Select Parent Issue
</button>
</Menu.Item>
)}
</div>
</Menu.Items>
</Transition>
@ -452,25 +544,49 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
</div>
</div>
</div>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<Button
theme="secondary"
onClick={() => {
handleClose();
resetForm();
}}
<div className="mt-5 flex items-center justify-between gap-2">
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setCreateMore((prevData) => !prevData)}
>
Discard
</Button>
<Button type="submit" disabled={isSubmitting}>
{data
? isSubmitting
? "Updating Issue..."
: "Update Issue"
: isSubmitting
? "Creating Issue..."
: "Create Issue"}
</Button>
<span className="text-xs">Create more</span>
<button
type="button"
className={`pointer-events-none relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent ${
createMore ? "bg-theme" : "bg-gray-300"
} transition-colors duration-300 ease-in-out focus:outline-none`}
role="switch"
aria-checked="false"
>
<span className="sr-only">Create more</span>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-3 w-3 ${
createMore ? "translate-x-3" : "translate-x-0"
} transform rounded-full bg-white shadow ring-0 transition duration-300 ease-in-out`}
></span>
</button>
</div>
<div className="flex items-center gap-2">
<Button
theme="secondary"
onClick={() => {
handleClose();
resetForm();
}}
>
Discard
</Button>
<Button type="submit" disabled={isSubmitting}>
{data
? isSubmitting
? "Updating Issue..."
: "Update Issue"
: isSubmitting
? "Creating Issue..."
: "Create Issue"}
</Button>
</div>
</div>
</form>
</Dialog.Panel>

View file

@ -1,12 +1,12 @@
import React from "react";
// swr
import { useRouter } from "next/router";
import useSWR from "swr";
// react hook form
import { Controller } from "react-hook-form";
// service
import projectServices from "lib/services/project.service";
// hooks
import useUser from "lib/hooks/useUser";
// fetch keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
// types
@ -21,12 +21,13 @@ type Props = {
};
const SelectAssignee: React.FC<Props> = ({ control }) => {
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: people } = useSWR(
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
activeWorkspace && activeProject
? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id)
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
: null
);
@ -51,6 +52,7 @@ const SelectAssignee: React.FC<Props> = ({ control }) => {
value={value}
onChange={onChange}
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
assignee
/>
)}
/>

View file

@ -1,95 +1,71 @@
import React from "react";
// swr
import useSWR from "swr";
// react hook form
import { Controller } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// hooks
import useUser from "lib/hooks/useUser";
// components
import CreateUpdateSprintsModal from "components/project/cycles/create-update-cycle-modal";
// services
import cycleServices from "lib/services/cycles.service";
// constants
import { CYCLE_LIST } from "constants/fetch-keys";
// ui
import { CustomListbox } from "ui";
// icons
import { CheckIcon, ChevronDownIcon, PlusIcon } from "@heroicons/react/20/solid";
import { PlusIcon } from "@heroicons/react/20/solid";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// types
import type { IIssue } from "types";
import type { Control } from "react-hook-form";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
type Props = {
control: Control<IIssue, any>;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
activeProject: string;
};
const SelectSprint: React.FC<Props> = ({ control, setIsOpen }) => {
const { cycles } = useUser();
const SelectCycle: React.FC<Props> = ({ control, setIsOpen, activeProject }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: cycles } = useSWR(
workspaceSlug && activeProject ? CYCLE_LIST(activeProject) : null,
workspaceSlug && activeProject
? () => cycleServices.getCycles(workspaceSlug as string, activeProject)
: null
);
return (
<>
<Controller
control={control}
name="sprints"
render={({ field: { value, onChange } }) => (
<Listbox as="div" value={value} onChange={onChange}>
{({ open }) => (
<>
<div className="relative">
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
<ArrowPathIcon className="h-3 w-3 text-gray-500" />
<span className="block truncate">
{cycles?.find((i) => i.id.toString() === value?.toString())?.name ?? "Cycle"}
</span>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="py-1">
{cycles?.map((cycle) => (
<Listbox.Option
key={cycle.id}
value={cycle.id}
className={({ active }) =>
`text-gray-900 cursor-pointer select-none p-2 ${
active ? "bg-indigo-50" : ""
}`
}
>
{({ active, selected }) => (
<>
<span className={`block ${selected && "font-semibold"}`}>
{cycle.name}
</span>
</>
)}
</Listbox.Option>
))}
</div>
<button
type="button"
className="relative select-none py-2 pl-3 pr-9 flex items-center gap-x-2 text-gray-400 hover:text-gray-500"
onClick={() => setIsOpen(true)}
>
<span>
<PlusIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
<span>
<span className="block truncate">Create cycle</span>
</span>
</button>
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)}
/>
</>
<Controller
control={control}
name="cycle"
render={({ field: { value, onChange } }) => (
<CustomListbox
title={cycles?.find((i) => i.id.toString() === value?.toString())?.name ?? "Cycle"}
options={cycles?.map((cycle) => {
return { value: cycle.id, display: cycle.name };
})}
value={value}
optionsFontsize="sm"
onChange={onChange}
icon={<ArrowPathIcon className="h-3 w-3 text-gray-500" />}
footerOption={
<button
type="button"
className="relative flex select-none items-center gap-x-2 py-2 pl-3 pr-9 text-gray-400 hover:text-gray-500"
onClick={() => setIsOpen(true)}
>
<span>
<PlusIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
<span>
<span className="block truncate">Create cycle</span>
</span>
</button>
}
/>
)}
/>
);
};
export default SelectSprint;
export default SelectCycle;

View file

@ -1,24 +1,23 @@
import React, { useEffect, useState } from "react";
// swr
import { useRouter } from "next/router";
import useSWR from "swr";
// react hook form
import { useForm, Controller } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
import type { Control } from "react-hook-form";
// services
import issuesServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
// fetching keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
// icons
import { CheckIcon, PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
// ui
import { Button, Input } from "ui";
// types
import type { Control } from "react-hook-form";
import type { IIssue, IIssueLabels } from "types";
import { Input, CustomListbox } from "ui";
// icons
import { TagIcon } from "@heroicons/react/24/outline";
// types
import type { IIssue, IIssueLabels } from "types";
type Props = {
control: Control<IIssue, any>;
@ -29,21 +28,22 @@ const defaultValues: Partial<IIssueLabels> = {
};
const SelectLabels: React.FC<Props> = ({ control }) => {
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const [isOpen, setIsOpen] = useState(false);
const { data: issueLabels, mutate: issueLabelsMutate } = useSWR<IIssueLabels[]>(
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
activeProject && activeWorkspace
? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id)
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(workspaceSlug as string) : null,
workspaceSlug && projectId
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const onSubmit = async (data: IIssueLabels) => {
if (!activeProject || !activeWorkspace || isSubmitting) return;
if (!projectId || !workspaceSlug || isSubmitting) return;
await issuesServices
.createIssueLabel(activeWorkspace.slug, activeProject.id, data)
.createIssueLabel(workspaceSlug as string, projectId as string, data)
.then((response) => {
issueLabelsMutate((prevData) => [...(prevData ?? []), response], false);
setIsOpen(false);
@ -71,119 +71,66 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
control={control}
name="labels_list"
render={({ field: { value, onChange } }) => (
<Listbox
<CustomListbox
title="Labels"
options={issueLabels?.map((label) => {
return { value: label.id, display: label.name, color: label.colour };
})}
value={value}
onChange={(data: any) => {
const valueCopy = [...(value ?? [])];
if (valueCopy.some((i) => i === data)) onChange(valueCopy.filter((i) => i !== data));
else onChange([...valueCopy, data]);
}}
>
{({ open }) => (
<>
<div className="relative">
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
<TagIcon className="h-3 w-3 text-gray-500" />
<span className="block truncate">
{value && value.length > 0
? value.map((id) => issueLabels?.find((i) => i.id === id)?.name).join(", ")
: "Labels"}
</span>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
optionsFontsize="sm"
onChange={onChange}
icon={<TagIcon className="h-3 w-3 text-gray-500" />}
footerOption={
<div className="relative min-w-[12rem] cursor-default select-none p-2">
{isOpen ? (
<div className="flex items-center gap-x-1">
<Input
id="name"
name="name"
type="text"
placeholder="Title"
className="w-full"
autoComplete="off"
register={register}
validations={{
required: true,
}}
/>
<button
type="button"
className="grid h-8 w-12 place-items-center rounded-md bg-green-600 text-white"
disabled={isSubmitting}
onClick={handleSubmit(onSubmit)}
>
<PlusIcon className="h-4 w-4" />
</button>
<button
type="button"
className="grid h-8 w-12 place-items-center rounded-md bg-red-600 text-white"
onClick={() => setIsOpen(false)}
>
<XMarkIcon className="h-4 w-4" aria-hidden="true" />
</button>
</div>
) : (
<button
type="button"
className="flex items-center gap-x-2 text-gray-400 hover:text-gray-500"
onClick={() => setIsOpen(true)}
>
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="py-1">
{issueLabels?.map((label) => (
<Listbox.Option
key={label.id}
className={({ active }) =>
`${
active ? "bg-indigo-50" : ""
} flex items-center gap-2 text-gray-900 cursor-pointer select-none w-full p-2`
}
value={label.id}
>
{({ selected, active }) => (
<>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
style={{ backgroundColor: label.colour }}
></span>
<span
className={`${
selected || (value ?? []).some((i) => i === label.id)
? "font-semibold"
: "font-normal"
} block`}
>
{label.name}
</span>
</>
)}
</Listbox.Option>
))}
</div>
<div className="cursor-default select-none relative p-2 min-w-[12rem]">
{isOpen ? (
<div className="flex items-center gap-x-1">
<Input
id="name"
name="name"
type="text"
placeholder="Title"
className="w-full"
autoComplete="off"
register={register}
validations={{
required: true,
}}
/>
<button
type="button"
className="bg-green-600 text-white h-8 w-12 rounded-md grid place-items-center"
disabled={isSubmitting}
onClick={handleSubmit(onSubmit)}
>
<PlusIcon className="h-4 w-4" />
</button>
<button
type="button"
className="bg-red-600 text-white h-8 w-12 rounded-md grid place-items-center"
onClick={() => setIsOpen(false)}
>
<XMarkIcon className="h-4 w-4" aria-hidden="true" />
</button>
</div>
) : (
<button
type="button"
className="flex items-center gap-x-2 text-gray-400 hover:text-gray-500"
onClick={() => setIsOpen(true)}
>
<span>
<PlusIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
<span>
<span className="block truncate">Create label</span>
</span>
</button>
)}
</div>
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
<span>
<PlusIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
<span>
<span className="block truncate">Create label</span>
</span>
</button>
)}
</div>
}
/>
)}
></Controller>
/>
);
};

View file

@ -1,17 +1,18 @@
import React from "react";
// react hook form
import { Controller } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import { CheckIcon } from "@heroicons/react/20/solid";
// constants
import { PRIORITIES } from "constants/";
import { capitalizeFirstLetter } from "constants/common";
// icons
import { ChartBarIcon } from "@heroicons/react/24/outline";
// ui
import { CustomListbox } from "ui";
// types
import type { IIssue } from "types";
import type { Control } from "react-hook-form";
import { ChartBarIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon } from "constants/global";
type Props = {
control: Control<IIssue, any>;
@ -23,58 +24,22 @@ const SelectPriority: React.FC<Props> = ({ control }) => {
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<>
<div className="relative">
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
<ChartBarIcon className="h-3 w-3 text-gray-500" />
<span className="block capitalize">
{value && value !== "" ? value : "Priority"}
</span>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 w-full w-[5rem] bg-white shadow-lg max-h-28 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-xs">
<div className="py-1">
{PRIORITIES.map((priority) => (
<Listbox.Option
key={priority}
className={({ active }) =>
`${
active ? "bg-indigo-50" : ""
} text-gray-900 cursor-pointer select-none p-2`
}
value={priority}
>
{({ selected, active }) => (
<>
<span
className={`block capitalize ${
selected ? "font-medium" : "font-normal"
}`}
>
{priority}
</span>
</>
)}
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
<CustomListbox
title="State"
options={PRIORITIES?.map((priority) => {
return {
value: priority,
display: capitalizeFirstLetter(priority ?? "none"),
icon: getPriorityIcon(priority),
};
})}
value={value}
optionsFontsize="sm"
onChange={onChange}
icon={getPriorityIcon(value)}
/>
)}
></Controller>
/>
);
};

View file

@ -1,24 +1,39 @@
// react
import React from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// react hook form
import { Controller } from "react-hook-form";
import type { Control } from "react-hook-form";
// services
import projectService from "lib/services/project.service";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// hooks
import useUser from "lib/hooks/useUser";
// icons
import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
// ui
import { Spinner } from "ui";
// fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys";
// types
import type { Control } from "react-hook-form";
import type { IIssue } from "types";
type Props = {
control: Control<IIssue, any>;
activeProject: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
};
const SelectProject: React.FC<Props> = ({ control }) => {
const { projects, setActiveProject } = useUser();
const SelectProject: React.FC<Props> = ({ control, setActiveProject }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: projects } = useSWR(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
() => (workspaceSlug ? projectService.getProjects(workspaceSlug as string) : null)
);
return (
<>
@ -28,15 +43,15 @@ const SelectProject: React.FC<Props> = ({ control }) => {
render={({ field: { value, onChange } }) => (
<Listbox
value={value}
onChange={(value) => {
onChange(value);
setActiveProject(projects?.find((i) => i.id === value));
onChange={(val) => {
onChange(val);
setActiveProject(val);
}}
>
{({ open }) => (
<>
<div className="relative">
<Listbox.Button className="flex items-center gap-1 bg-white relative border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<Listbox.Button className="relative flex cursor-pointer items-center gap-1 rounded-md border bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<ClipboardDocumentListIcon className="h-3 w-3" />
<span className="block truncate">
{projects?.find((i) => i.id === value)?.identifier ?? "Project"}
@ -50,7 +65,7 @@ const SelectProject: React.FC<Props> = ({ control }) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<Listbox.Options className="absolute z-10 mt-1 max-h-28 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{projects ? (
projects.length > 0 ? (
@ -60,7 +75,7 @@ const SelectProject: React.FC<Props> = ({ control }) => {
className={({ active }) =>
`${
active ? "bg-indigo-50" : ""
} text-gray-900 cursor-pointer select-none p-2`
} cursor-pointer select-none p-2 text-gray-900`
}
value={project.id}
>

View file

@ -1,16 +1,24 @@
// react
import React from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// react hook form
import { Controller } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
// services
import stateService from "lib/services/state.service";
// constants
import { STATE_LIST } from "constants/fetch-keys";
// icons
import { PlusIcon } from "@heroicons/react/20/solid";
// ui
import { CustomListbox } from "ui";
// icons
import { Squares2X2Icon } from "@heroicons/react/24/outline";
// types
import type { Control } from "react-hook-form";
import type { IIssue } from "types";
import { Squares2X2Icon } from "@heroicons/react/24/outline";
type Props = {
control: Control<IIssue, any>;
@ -18,7 +26,15 @@ type Props = {
};
const SelectState: React.FC<Props> = ({ control, setIsOpen }) => {
const { states } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
return (
<Controller
@ -37,7 +53,7 @@ const SelectState: React.FC<Props> = ({ control, setIsOpen }) => {
footerOption={
<button
type="button"
className="select-none relative py-2 pl-3 pr-9 flex items-center gap-x-2 text-gray-400 hover:text-gray-500"
className="relative flex select-none items-center gap-x-2 py-2 pl-3 pr-9 text-gray-400 hover:text-gray-500"
onClick={() => setIsOpen(true)}
>
<span>

View file

@ -3,16 +3,12 @@ import React from "react";
import { useRouter } from "next/router";
import Image from "next/image";
// swr
import useSWR from "swr";
// constants
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { addSpaceIfCamelCase, timeAgo } from "constants/common";
// services
import issuesServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
import { KeyedMutator } from "swr";
// common
import { addSpaceIfCamelCase, renderShortNumericDateFormat, timeAgo } from "constants/common";
// ui
import { Spinner } from "ui";
import { Loader } from "ui";
// icons
import {
CalendarDaysIcon,
@ -21,47 +17,115 @@ import {
Squares2X2Icon,
UserIcon,
} from "@heroicons/react/24/outline";
// types
import { IssueResponse, IState } from "types";
import { BlockedIcon, BlockerIcon, TagIcon, UserGroupIcon } from "ui/icons";
import { IIssueActivity, IIssueComment } from "types";
// components
import CommentCard from "components/project/issues/issue-detail/comment/issue-comment-card";
// services
import issuesServices from "lib/services/issues.service";
const activityIcons: {
[key: string]: JSX.Element;
const activityDetails: {
[key: string]: {
message?: string;
icon: JSX.Element;
};
} = {
state: <Squares2X2Icon className="h-3.5 w-3.5" />,
priority: <ChartBarIcon className="h-3.5 w-3.5" />,
name: <ChatBubbleBottomCenterTextIcon className="h-3.5 w-3.5" />,
description: <ChatBubbleBottomCenterTextIcon className="h-3.5 w-3.5" />,
target_date: <CalendarDaysIcon className="h-3.5 w-3.5" />,
parent: <UserIcon className="h-3.5 w-3.5" />,
assignee: {
message: "removed the assignee",
icon: <UserGroupIcon className="h-4 w-4" />,
},
assignees: {
message: "added a new assignee",
icon: <UserGroupIcon className="h-4 w-4" />,
},
blocks: {
message: "marked this issue being blocked by",
icon: <BlockedIcon height="16" width="16" />,
},
blocking: {
message: "marked this issue is blocking",
icon: <BlockerIcon height="16" width="16" />,
},
labels: {
icon: <TagIcon height="16" width="16" />,
},
state: {
message: "set the state to",
icon: <Squares2X2Icon className="h-4 w-4" />,
},
priority: {
message: "set the priority to",
icon: <ChartBarIcon className="h-4 w-4" />,
},
name: {
message: "set the name to",
icon: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
},
description: {
message: "updated the description.",
icon: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
},
target_date: {
message: "set the due date to",
icon: <CalendarDaysIcon className="h-4 w-4" />,
},
parent: {
message: "set the parent to",
icon: <UserIcon className="h-4 w-4" />,
},
};
const IssueActivitySection: React.FC = () => {
const defaultValues: Partial<IIssueComment> = {
comment_html: "",
comment_json: "",
};
const IssueActivitySection: React.FC<{
issueActivities: IIssueActivity[];
mutate: KeyedMutator<IIssueActivity[]>;
}> = ({ issueActivities, mutate }) => {
const router = useRouter();
const { issueId, projectId } = router.query;
let { workspaceSlug, projectId, issueId } = router.query;
const { activeWorkspace, states, issues } = useUser();
const onCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !issueId) return;
await issuesServices
.patchIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
comment.id,
comment
)
.then((response) => {
mutate();
});
};
const { data: issueActivities } = useSWR<any[]>(
activeWorkspace && projectId && issueId ? PROJECT_ISSUES_ACTIVITY : null,
activeWorkspace && projectId && issueId
? () =>
issuesServices.getIssueActivities(
activeWorkspace.slug,
projectId as string,
issueId as string
)
: null
);
const onCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !issueId) return;
await issuesServices
.deleteIssueComment(
workspaceSlug as string,
projectId as string,
issueId as string,
commentId
)
.then((response) => {
mutate();
console.log(response);
});
};
return (
<>
{issueActivities ? (
<div className="space-y-3">
<div className="space-y-4">
{issueActivities.map((activity, index) => {
if (activity.field !== "updated_by")
if ("field" in activity && activity.field !== "updated_by") {
return (
<div key={activity.id} className="relative flex gap-x-2 w-full">
<div key={activity.id} className="relative flex w-full items-center gap-x-2">
{issueActivities.length > 1 && index !== issueActivities.length - 1 ? (
<span
className="absolute top-5 left-2.5 h-full w-0.5 bg-gray-200"
@ -69,89 +133,114 @@ const IssueActivitySection: React.FC = () => {
/>
) : null}
{activity.field ? (
<div className="relative z-10 flex-shrink-0 -ml-1">
<div
className={`h-7 w-7 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
>
{activityIcons[activity.field as keyof typeof activityIcons]}
<div className="relative z-10 -ml-1 flex-shrink-0">
<div className="grid h-8 w-8 place-items-center bg-white">
{activityDetails[activity.field as keyof typeof activityDetails]?.icon}
</div>
</div>
) : (
<div className="relative z-10 flex-shrink-0 border-2 border-white rounded-full h-[34px] -ml-1.5">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
<Image
src={activity.actor_detail.avatar}
alt={activity.actor_detail.name}
height={30}
width={30}
className="rounded-full"
/>
) : (
<div
className={`h-8 w-8 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
>
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
<div className="relative z-10 -ml-4 flex-shrink-0 rounded-full border-2 border-white">
<div className="grid h-12 w-12 place-items-center">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
<Image
src={activity.actor_detail.avatar}
alt={activity.actor_detail.name}
height={30}
width={30}
className="rounded-full"
/>
) : (
<div
className={`grid h-8 w-8 place-items-center rounded-full border-2 border-white bg-gray-700 text-white`}
>
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
</div>
)}
<div className="w-full text-xs">
<div className={`${activity.field ? "ml-1.5" : ""} w-full text-xs`}>
<p>
<span className="font-medium">
{activity.actor_detail.first_name} {activity.actor_detail.last_name}
</span>
<span> {activity.verb} </span>
{activity.verb !== "created" ? (
<span>{activity.field ?? "commented"}</span>
) : (
" this issue"
)}
<span>
{" "}
{activity.field === "labels"
? activity.new_value !== ""
? "added a new label"
: "removed the label"
: activity.field === "blocking"
? activity.new_value !== ""
? "marked this issue is blocking"
: "removed the issue from blocking"
: activity.field === "blocks"
? activity.new_value !== ""
? "marked this issue being blocked by"
: "removed blocker"
: activityDetails[activity.field as keyof typeof activityDetails]
?.message}{" "}
</span>
<span className="font-medium">
{activity.verb === "created" ? (
<span className="text-gray-600">created this issue.</span>
) : activity.field === "description" ? null : activity.field === "state" ? (
activity.new_value ? (
addSpaceIfCamelCase(activity.new_value)
) : (
"None"
)
) : activity.field === "labels" ||
activity.field === "blocking" ||
activity.field === "blocks" ? (
activity.new_value !== "" ? (
activity.new_value
) : (
activity.old_value
)
) : activity.field === "assignee" ? (
activity.old_value
) : activity.field === "target_date" ? (
renderShortNumericDateFormat(activity.new_value as string)
) : activity.field === "description" ? (
""
) : (
activity.new_value ?? "None"
)}
</span>
<span className="ml-2 text-gray-500">{timeAgo(activity.created_at)}</span>
</p>
<div className="w-full mt-2">
{activity.verb !== "created" && (
<div>
<div>
<span className="text-gray-500">From: </span>
{activity.field === "state"
? activity.old_value
? addSpaceIfCamelCase(
states?.find((s) => s.id === activity.old_value)?.name ?? ""
)
: "None"
: activity.field === "parent"
? activity.old_value
? issues?.results.find((i) => i.id === activity.old_value)?.name
: "None"
: activity.old_value ?? "None"}
</div>
<div>
<span className="text-gray-500">To: </span>
{activity.field === "state"
? activity.new_value
? addSpaceIfCamelCase(
states?.find((s) => s.id === activity.new_value)?.name ?? ""
)
: "None"
: activity.field === "parent"
? activity.new_value
? issues?.results.find((i) => i.id === activity.new_value)?.name
: "None"
: activity.new_value ?? "None"}
</div>
</div>
)}
</div>
</div>
</div>
);
} else if ("comment_json" in activity) {
return (
<CommentCard
key={activity.id}
comment={activity as any}
onSubmit={onCommentUpdate}
handleCommentDeletion={onCommentDelete}
/>
);
}
})}
</div>
) : (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
<Loader className="space-y-4">
<div className="space-y-2">
<Loader.Item height="30px" width="40%"></Loader.Item>
<Loader.Item height="15px" width="60%"></Loader.Item>
</div>
<div className="space-y-2">
<Loader.Item height="30px" width="40%"></Loader.Item>
<Loader.Item height="15px" width="60%"></Loader.Item>
</div>
<div className="space-y-2">
<Loader.Item height="30px" width="40%"></Loader.Item>
<Loader.Item height="15px" width="60%"></Loader.Item>
</div>
</Loader>
)}
</>
);

View file

@ -1,15 +1,12 @@
// react
import React, { useState } from "react";
// swr
import { mutate } from "swr";
// react hook form
import { useForm } from "react-hook-form";
// headless ui
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { Combobox, Dialog, Transition } from "@headlessui/react";
// services
import issuesServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
// icons
import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
// commons
@ -25,15 +22,20 @@ type Props = {
parent: IIssue | undefined;
};
type FormInput = {
issue_ids: string[];
cycleId: string;
};
const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
const [query, setQuery] = useState("");
const { activeWorkspace, activeProject, issues } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const filteredIssues: IIssue[] =
query === ""
@ -41,23 +43,18 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
[];
const {
formState: { errors, isSubmitting },
reset,
} = useForm<FormInput>();
const handleCommandPaletteClose = () => {
setIsOpen(false);
setQuery("");
};
const addAsSubIssue = (issueId: string) => {
if (activeWorkspace && activeProject) {
if (workspaceSlug && projectId) {
issuesServices
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parent?.id })
.patchIssue(workspaceSlug as string, projectId as string, issueId, { parent: parent?.id })
.then((res) => {
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
(prevData) => ({
...(prevData as IssueResponse),
results: (prevData?.results ?? []).map((p) =>
@ -106,7 +103,7 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
/>
@ -139,7 +136,7 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
}}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2",
"flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
@ -149,7 +146,7 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
}}
>
<span
className={`h-1.5 w-1.5 block rounded-full`}
className={`block h-1.5 w-1.5 rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}

View file

@ -1,160 +0,0 @@
import React, { useEffect, useState } from "react";
// next
import Image from "next/image";
// headless ui
import { Menu } from "@headlessui/react";
// react hook form
import { useForm } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
// common
import { timeAgo } from "constants/common";
// ui
import { TextArea } from "ui";
// icon
import { CheckIcon, EllipsisHorizontalIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { IIssueComment } from "types";
type Props = {
comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void;
};
const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
const { user } = useUser();
const [isEditing, setIsEditing] = useState(false);
const {
register,
formState: { isSubmitting },
handleSubmit,
setFocus,
} = useForm<IIssueComment>({
defaultValues: comment,
});
const onEnter = (formData: IIssueComment) => {
if (isSubmitting) return;
setIsEditing(false);
onSubmit(formData);
};
useEffect(() => {
isEditing && setFocus("comment");
}, [isEditing, setFocus]);
return (
<div key={comment.id}>
<div className="w-full h-full flex justify-between">
<div className="flex gap-x-2 w-full">
<div className="flex-shrink-0">
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
<Image
src={comment.actor_detail.avatar}
alt={comment.actor_detail.name}
height={30}
width={30}
className="rounded"
/>
) : (
<div
className={`h-8 w-8 bg-gray-500 text-white border-2 border-white grid place-items-center rounded-full`}
>
{comment.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
<div className="w-full">
<div>
{isEditing ? (
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
<TextArea
id="comment"
name="comment"
register={register}
validations={{
required: true,
}}
autoComplete="off"
mode="transparent"
className="w-full"
/>
<div className="flex self-end gap-1">
<button
type="submit"
disabled={isSubmitting}
className="group bg-green-100 hover:bg-green-500 border border-green-500 duration-300 p-2 rounded shadow-md"
>
<CheckIcon className="h-3 w-3 text-green-500 group-hover:text-white duration-300" />
</button>
<button
type="button"
className="group bg-red-100 hover:bg-red-500 border border-red-500 duration-300 p-2 rounded shadow-md"
onClick={() => setIsEditing(false)}
>
<XMarkIcon className="h-3 w-3 text-red-500 group-hover:text-white duration-300" />
</button>
</div>
</form>
) : (
<>
{comment.comment.split("\n").map((item, index) => (
<p key={index} className="text-sm">
{item}
</p>
))}
</>
)}
</div>
<p className="text-xs text-gray-500 flex items-center gap-2 mt-1">
<span>
{comment.actor_detail.first_name} {comment.actor_detail.last_name}
</span>
<span>{timeAgo(comment.created_at)}</span>
</p>
</div>
</div>
{user?.id === comment.actor && (
<div className="relative">
<Menu>
<Menu.Button>
<EllipsisHorizontalIcon className="w-5 h-5 text-gray-500" />
</Menu.Button>
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24 -top-20">
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="w-full text-left py-2 pl-2"
type="button"
onClick={() => setIsEditing(true)}
>
Edit
</button>
</div>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="w-full text-left py-2 pl-2"
type="button"
onClick={() => {
handleCommentDeletion(comment.id);
}}
>
Delete
</button>
</div>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
)}
</div>
</div>
);
};
export default CommentCard;

View file

@ -1,155 +0,0 @@
import React from "react";
// router
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// react hook form
import { useForm } from "react-hook-form";
// services
import issuesServices from "lib/services/issues.service";
// fetch keys
import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys";
// hooks
import useUser from "lib/hooks/useUser";
// components
import CommentCard from "components/project/issues/issue-detail/comment/IssueCommentCard";
// ui
import { TextArea, Button, Spinner } from "ui";
// types
import type { IIssueComment } from "types";
const defaultValues: Partial<IIssueComment> = {
comment: "",
};
const IssueCommentSection: React.FC = () => {
const {
register,
handleSubmit,
setValue,
formState: { errors, isSubmitting },
reset,
} = useForm<IIssueComment>({ defaultValues });
const router = useRouter();
let { issueId, projectId } = router.query;
const { activeWorkspace } = useUser();
const { data: comments, mutate } = useSWR<IIssueComment[]>(
activeWorkspace && projectId && issueId ? PROJECT_ISSUES_COMMENTS(issueId as string) : null,
activeWorkspace && projectId && issueId
? () =>
issuesServices.getIssueComments(
activeWorkspace.slug,
projectId as string,
issueId as string
)
: null
);
const onSubmit = async (formData: IIssueComment) => {
if (!activeWorkspace || !projectId || !issueId || isSubmitting) return;
await issuesServices
.createIssueComment(activeWorkspace.slug, projectId as string, issueId as string, formData)
.then((response) => {
console.log(response);
mutate((prevData) => [response, ...(prevData ?? [])]);
reset(defaultValues);
})
.catch((error) => {
console.log(error);
});
};
const onCommentUpdate = async (comment: IIssueComment) => {
if (!activeWorkspace || !projectId || !issueId || isSubmitting) return;
await issuesServices
.patchIssueComment(
activeWorkspace.slug,
projectId as string,
issueId as string,
comment.id,
comment
)
.then((response) => {
mutate((prevData) => {
const updatedComments = prevData?.map((c) => {
if (c.id === comment.id) {
return comment;
}
return c;
});
return updatedComments;
});
});
};
const onCommentDelete = async (commentId: string) => {
if (!activeWorkspace || !projectId || !issueId || isSubmitting) return;
await issuesServices
.deleteIssueComment(activeWorkspace.slug, projectId as string, issueId as string, commentId)
.then((response) => {
mutate((prevData) => (prevData ?? []).filter((c) => c.id !== commentId));
console.log(response);
});
};
return (
<div className="space-y-5">
{comments ? (
comments.length > 0 ? (
<div className="space-y-5">
{comments.map((comment) => (
<CommentCard
key={comment.id}
comment={comment}
onSubmit={onCommentUpdate}
handleCommentDeletion={onCommentDelete}
/>
))}
</div>
) : (
<p className="text-sm">No comments yet. Be the first to comment.</p>
)
) : (
<div className="w-full flex justify-center">
<Spinner />
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-start gap-2 border rounded-md p-2">
<TextArea
id="comment"
name="comment"
register={register}
validations={{
required: true,
}}
mode="transparent"
error={errors.comment}
placeholder="Enter your comment"
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) {
e.preventDefault();
const value = e.currentTarget.value;
const start = e.currentTarget.selectionStart;
const end = e.currentTarget.selectionEnd;
setValue("comment", `${value.substring(0, start)}\r ${value.substring(end)}`);
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
isSubmitting || handleSubmit(onSubmit)();
}
}}
/>
<Button type="submit" className="whitespace-nowrap" disabled={isSubmitting}>
{isSubmitting ? "Adding comment..." : "Add comment"}
</Button>
</div>
</form>
</div>
);
};
export default IssueCommentSection;

View file

@ -0,0 +1,134 @@
// react
import React, { useEffect, useState, useMemo } from "react";
// next
import Image from "next/image";
import dynamic from "next/dynamic";
// react-hook-form
import { useForm } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { CustomMenu } from "ui";
// icons
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { IIssueComment } from "types";
// common
import { timeAgo } from "constants/common";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false });
type Props = {
comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void;
};
const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
const { user } = useUser();
const [isEditing, setIsEditing] = useState(false);
const {
formState: { isSubmitting },
handleSubmit,
setFocus,
setValue,
} = useForm<IIssueComment>({
defaultValues: comment,
});
const onEnter = (formData: IIssueComment) => {
if (isSubmitting) return;
setIsEditing(false);
onSubmit(formData);
};
useEffect(() => {
isEditing && setFocus("comment");
}, [isEditing, setFocus]);
return (
<div className="-ml-1 flex h-full w-full justify-between">
<div className="flex w-full gap-x-4">
<div className="flex-shrink-0">
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
<Image
src={comment.actor_detail.avatar}
alt={comment.actor_detail.name}
height={30}
width={30}
className="rounded-full"
/>
) : (
<div
className={`grid h-8 w-8 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{comment.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
<div className="w-full space-y-1">
<p className="flex items-center gap-2 text-xs text-gray-500">
<span>
{comment.actor_detail.first_name} {comment.actor_detail.last_name}
</span>
<span>{timeAgo(comment.created_at)}</span>
</p>
<div>
{isEditing ? (
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
<RemirrorRichTextEditor
value={comment.comment_html}
onBlur={(jsonValue, htmlValue) => {
setValue("comment_json", jsonValue);
setValue("comment_html", htmlValue);
}}
placeholder="Enter Your comment..."
/>
<div className="flex gap-1 self-end">
<button
type="submit"
disabled={isSubmitting}
className="group rounded border border-green-500 bg-green-100 p-2 shadow-md duration-300 hover:bg-green-500"
>
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
</button>
<button
type="button"
className="group rounded border border-red-500 bg-red-100 p-2 shadow-md duration-300 hover:bg-red-500"
onClick={() => setIsEditing(false)}
>
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
</button>
</div>
</form>
) : (
<>
<RemirrorRichTextEditor
value={comment.comment_html}
editable={false}
onBlur={() => {}}
/>
</>
)}
</div>
</div>
</div>
{user?.id === comment.actor && (
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
handleCommentDeletion(comment.id);
}}
>
Delete
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
);
};
export default CommentCard;

View file

@ -0,0 +1,115 @@
import React, { useMemo } from "react";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import type { KeyedMutator } from "swr";
import { useForm, Controller } from "react-hook-form";
// services
import issuesServices from "lib/services/issues.service";
// ui
import { Loader } from "ui";
// types
import type { IIssueActivity, IIssueComment } from "types";
// common
import { debounce } from "constants/common";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader className="mb-5">
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
const defaultValues: Partial<IIssueComment> = {
comment_html: "",
comment_json: "",
};
const AddIssueComment: React.FC<{
mutate: KeyedMutator<IIssueActivity[]>;
}> = ({ mutate }) => {
const {
handleSubmit,
control,
setValue,
formState: { isSubmitting },
reset,
} = useForm<IIssueComment>({ defaultValues });
const router = useRouter();
let { workspaceSlug, projectId, issueId } = router.query;
const onSubmit = async (formData: IIssueComment) => {
if (
!workspaceSlug ||
!projectId ||
!issueId ||
isSubmitting ||
!formData.comment_html ||
!formData.comment_json
)
return;
await issuesServices
.createIssueComment(workspaceSlug as string, projectId as string, issueId as string, formData)
.then(() => {
mutate();
reset(defaultValues);
})
.catch((error) => {
console.error(error);
});
};
const updateDescription = useMemo(
() =>
debounce((key: any, val: any) => {
setValue(key, val);
}, 1000),
[setValue]
);
const updateDescriptionHTML = useMemo(
() =>
debounce((key: any, val: any) => {
setValue(key, val);
}, 1000),
[setValue]
);
return (
<div className="space-y-5">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="rounded-md p-2 pt-3">
<Controller
name="comment_html"
control={control}
render={({ field: { value } }) => (
<RemirrorRichTextEditor
value={value}
onBlur={(jsonValue, htmlValue) => {
setValue("comment_json", jsonValue);
setValue("comment_html", htmlValue);
}}
placeholder="Enter Your comment..."
/>
)}
/>
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-gray-300 p-2 px-4 text-sm text-black hover:bg-gray-300"
>
{isSubmitting ? "Adding..." : "Comment"}
</button>
</div>
</form>
</div>
);
};
export default AddIssueComment;

View file

@ -1,30 +1,33 @@
import React, { useState } from "react";
// swr
import useSWR from "swr";
import dynamic from "next/dynamic";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// react hook form
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { useForm, Controller, UseFormWatch } from "react-hook-form";
import { TwitterPicker } from "react-color";
// services
import issuesServices from "lib/services/issues.service";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// fetching keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
// commons
import { copyTextToClipboard } from "constants/common";
// components
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
import SelectState from "components/project/issues/issue-detail/issue-detail-sidebar/select-state";
import SelectPriority from "components/project/issues/issue-detail/issue-detail-sidebar/select-priority";
import SelectParent from "components/project/issues/issue-detail/issue-detail-sidebar/select-parent";
import SelectCycle from "components/project/issues/issue-detail/issue-detail-sidebar/select-cycle";
import SelectAssignee from "components/project/issues/issue-detail/issue-detail-sidebar/select-assignee";
import SelectBlocker from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocker";
import SelectBlocked from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocked";
// headless ui
import { Popover, Listbox, Transition } from "@headlessui/react";
// ui
import { Input, Button, Spinner } from "ui";
import { Popover } from "@headlessui/react";
// icons
import {
TagIcon,
ChevronDownIcon,
ClipboardDocumentIcon,
LinkIcon,
CalendarDaysIcon,
TrashIcon,
@ -33,16 +36,11 @@ import {
} from "@heroicons/react/24/outline";
// types
import type { Control } from "react-hook-form";
import type { ICycle, IIssue, IIssueLabels, NestedKeyOf } from "types";
import { TwitterPicker } from "react-color";
import { positionEditorElement } from "components/lexical/helpers/editor";
import SelectState from "./select-state";
import SelectPriority from "./select-priority";
import SelectParent from "./select-parent";
import SelectCycle from "./select-cycle";
import SelectAssignee from "./select-assignee";
import SelectBlocker from "./select-blocker";
import SelectBlocked from "./select-blocked";
import type { ICycle, IIssue, IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// common
import { copyTextToClipboard } from "constants/common";
type Props = {
control: Control<IIssue, any>;
@ -63,19 +61,28 @@ const IssueDetailSidebar: React.FC<Props> = ({
watch: watchIssue,
}) => {
const [createLabelForm, setCreateLabelForm] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { activeWorkspace, activeProject, issues } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
activeProject && activeWorkspace
? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id)
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const {
register,
@ -89,49 +96,51 @@ const IssueDetailSidebar: React.FC<Props> = ({
});
const handleNewLabel = (formData: any) => {
if (!activeWorkspace || !activeProject || isSubmitting) return;
if (!workspaceSlug || !projectId || isSubmitting) return;
issuesServices
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
.createIssueLabel(workspaceSlug as string, projectId as string, formData)
.then((res) => {
console.log(res);
reset(defaultValues);
issueLabelMutate((prevData) => [...(prevData ?? []), res], false);
submitChanges({ labels_list: [res.id] });
submitChanges({ labels_list: [...(issueDetail?.labels ?? []), res.id] });
setCreateLabelForm(false);
});
};
const handleCycleChange = (cycleDetail: ICycle) => {
if (activeWorkspace && activeProject && issueDetail) {
submitChanges({ cycle: cycleDetail.id, cycle_detail: cycleDetail });
issuesServices
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleDetail.id, {
issues: [issueDetail.id],
})
.then(() => {
submitChanges({});
});
}
if (!workspaceSlug || !projectId || !issueDetail) return;
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
issuesServices.addIssueToCycle(workspaceSlug as string, projectId as string, cycleDetail.id, {
issues: [issueDetail.id],
});
};
return (
<>
<ConfirmIssueDeletion
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetail}
/>
<div className="h-full w-full divide-y-2 divide-gray-100">
<div className="flex justify-between items-center pb-3">
<div className="flex items-center justify-between pb-3">
<h4 className="text-sm font-medium">
{activeProject?.identifier}-{issueDetail?.sequence_id}
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
</h4>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
className="rounded-md border p-2 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
`https://app.plane.so/${workspaceSlug}/projects/${issueDetail?.project_detail?.id}/issues/${issueDetail?.id}`
)
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
title: "Issue link copied to clipboard",
});
})
.catch(() => {
@ -146,28 +155,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
</button>
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() =>
copyTextToClipboard(issueDetail?.id ?? "")
.then(() => {
setToastAlert({
type: "success",
title: "Copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-2 hover:bg-red-50 text-red-500 border border-red-500 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-50 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() => setDeleteIssueModal(true)}
>
<TrashIcon className="h-3.5 w-3.5" />
@ -196,14 +184,14 @@ const IssueDetailSidebar: React.FC<Props> = ({
issueDetail?.parent_detail ? (
<button
type="button"
className="flex items-center gap-2 bg-gray-100 px-3 py-2 text-xs rounded"
className="flex items-center gap-2 rounded bg-gray-100 px-3 py-2 text-xs"
onClick={() => submitChanges({ parent: null })}
>
{issueDetail.parent_detail?.name}
<XMarkIcon className="h-3 w-3" />
</button>
) : (
<div className="inline-block bg-gray-100 px-3 py-2 text-xs rounded">
<div className="inline-block rounded bg-gray-100 px-3 py-2 text-xs">
No parent selected
</div>
)
@ -216,13 +204,13 @@ const IssueDetailSidebar: React.FC<Props> = ({
watch={watchIssue}
/>
<SelectBlocked
issueDetail={issueDetail}
submitChanges={submitChanges}
issuesList={issues?.results.filter((i) => i.id !== issueDetail?.id) ?? []}
watch={watchIssue}
/>
<div className="flex items-center py-2 flex-wrap">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<CalendarDaysIcon className="flex-shrink-0 h-4 w-4" />
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
<p>Due date</p>
</div>
<div className="sm:basis-1/2">
@ -238,7 +226,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
submitChanges({ target_date: e.target.value });
onChange(e.target.value);
}}
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
className="w-full cursor-pointer rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
)}
/>
@ -246,17 +234,22 @@ const IssueDetailSidebar: React.FC<Props> = ({
</div>
</div>
<div className="py-1">
<SelectCycle control={control} handleCycleChange={handleCycleChange} />
<SelectCycle
issueDetail={issueDetail}
control={control}
handleCycleChange={handleCycleChange}
watch={watchIssue}
/>
</div>
</div>
<div className="pt-3 space-y-3">
<div className="flex justify-between items-start">
<div className="flex items-center gap-x-2 text-sm basis-1/2">
<TagIcon className="w-4 h-4" />
<div className="space-y-3 pt-3">
<div className="flex items-start justify-between">
<div className="flex basis-1/2 items-center gap-x-2 text-sm">
<TagIcon className="h-4 w-4" />
<p>Label</p>
</div>
<div className="basis-1/2">
<div className="flex gap-1 flex-wrap">
<div className="flex flex-wrap gap-1">
{watchIssue("labels_list")?.map((label) => {
const singleLabel = issueLabels?.find((l) => l.id === label);
@ -265,7 +258,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
return (
<span
key={singleLabel.id}
className="group flex items-center gap-1 border rounded-2xl text-xs px-1 py-0.5 hover:bg-red-50 hover:border-red-500 cursor-pointer"
className="group flex cursor-pointer items-center gap-1 rounded-2xl border px-1 py-0.5 text-xs hover:border-red-500 hover:bg-red-50"
onClick={() => {
const updatedLabels = watchIssue("labels_list")?.filter((l) => l !== label);
submitChanges({
@ -274,7 +267,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
}}
>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: singleLabel.colour ?? "green" }}
></span>
{singleLabel.name}
@ -297,7 +290,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
<>
<Listbox.Label className="sr-only">Label</Listbox.Label>
<div className="relative">
<Listbox.Button className="flex items-center gap-2 border rounded-2xl text-xs px-2 py-0.5 hover:bg-gray-100 cursor-pointer">
<Listbox.Button className="flex cursor-pointer items-center gap-2 rounded-2xl border px-2 py-0.5 text-xs hover:bg-gray-100">
Select Label
</Listbox.Button>
@ -308,7 +301,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{issueLabels ? (
issueLabels.length > 0 ? (
@ -318,12 +311,12 @@ const IssueDetailSidebar: React.FC<Props> = ({
className={({ active, selected }) =>
`${
active || selected ? "bg-indigo-50" : ""
} flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate`
} relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={label.id}
>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: label.colour ?? "green" }}
></span>
{label.name}
@ -346,7 +339,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
/>
<button
type="button"
className="flex items-center gap-1 border rounded-2xl text-xs px-2 py-0.5 hover:bg-gray-100 cursor-pointer"
className="flex cursor-pointer items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs hover:bg-gray-100"
onClick={() => setCreateLabelForm((prevData) => !prevData)}
>
{createLabelForm ? (
@ -369,11 +362,11 @@ const IssueDetailSidebar: React.FC<Props> = ({
{({ open }) => (
<>
<Popover.Button
className={`bg-white flex items-center gap-1 rounded-md p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
className={`flex items-center gap-1 rounded-md bg-white p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
>
{watch("colour") && watch("colour") !== "" && (
<span
className="w-5 h-5 rounded"
className="h-5 w-5 rounded"
style={{
backgroundColor: watch("colour") ?? "green",
}}
@ -391,7 +384,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute z-10 transform right-0 mt-1 px-2 max-w-xs sm:px-0">
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
<Controller
name="colour"
control={controlLabel}
@ -418,18 +411,16 @@ const IssueDetailSidebar: React.FC<Props> = ({
}}
autoComplete="off"
/>
<Button type="submit" theme="danger" onClick={() => setCreateLabelForm(false)}>
<XMarkIcon className="h-4 w-4 text-white" />
</Button>
<Button type="submit" theme="success" disabled={isSubmitting}>
+
<PlusIcon className="h-4 w-4 text-white" />
</Button>
</form>
)}
</div>
</div>
<ConfirmIssueDeletion
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetail}
/>
</>
);
};

View file

@ -1,10 +1,10 @@
// react
import React from "react";
// next
import Image from "next/image";
// swr
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { Control, Controller } from "react-hook-form";
// services
import workspaceService from "lib/services/workspace.service";
@ -29,17 +29,18 @@ type Props = {
};
const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
const { activeWorkspace } = useUser();
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: people } = useSWR(
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
return (
<div className="flex items-center py-2 flex-wrap">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserGroupIcon className="flex-shrink-0 h-4 w-4" />
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p>
</div>
<div className="sm:basis-1/2">
@ -58,14 +59,14 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="w-full flex items-center gap-1 text-xs cursor-pointer">
<Listbox.Button className="flex w-full cursor-pointer items-center gap-1 text-xs">
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden truncate sm:block text-left"
"hidden truncate text-left sm:block"
)}
>
<div className="flex items-center gap-1 text-xs cursor-pointer">
<div className="flex cursor-pointer items-center gap-1 text-xs">
{value && Array.isArray(value) ? (
<>
{value.length > 0 ? (
@ -82,7 +83,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
}`}
>
{person && person.avatar && person.avatar !== "" ? (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={person.avatar}
height="100%"
@ -93,7 +94,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
</div>
) : (
<div
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
className={`grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 text-white`}
>
{person?.first_name.charAt(0)}
</div>
@ -102,7 +103,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
);
})
) : (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={User}
height="100%"
@ -128,7 +129,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options className="absolute z-10 left-0 mt-1 w-auto bg-white shadow-lg max-h-48 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<Listbox.Options className="absolute left-0 z-10 mt-1 max-h-48 w-auto overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{people ? (
people.length > 0 ? (
@ -138,7 +139,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
className={({ active, selected }) =>
`${
active || selected ? "bg-indigo-50" : ""
} flex items-center gap-2 text-gray-900 cursor-pointer select-none p-2 truncate`
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.member.id}
>
@ -153,7 +154,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
/>
</div>
) : (
<div className="flex-shrink-0 h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name.charAt(0)
: option.member.email.charAt(0)}

View file

@ -1,40 +1,58 @@
// react
import React, { useState } from "react";
// react-hook-form
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
// services
import issuesService from "lib/services/issues.service";
// constants
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { FolderIcon, MagnifyingGlassIcon, FlagIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { BlockedIcon } from "ui/icons";
// types
import { IIssue, IssueResponse } from "types";
import { IIssue } from "types";
// constants
import { classNames } from "constants/common";
import issuesService from "lib/services/issues.service";
type FormInput = {
issue_ids: string[];
};
type Props = {
issueDetail: IIssue | undefined;
submitChanges: (formData: Partial<IIssue>) => void;
issuesList: IIssue[];
watch: UseFormWatch<IIssue>;
};
const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
const SelectBlocked: React.FC<Props> = ({ submitChanges, issuesList, watch }) => {
const [query, setQuery] = useState("");
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
const { activeWorkspace, activeProject, issues, mutateIssues } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { register, handleSubmit, reset, watch: watchIssues } = useForm<FormInput>();
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
: null
);
const { register, handleSubmit, reset, watch: watchBlocked } = useForm<FormInput>();
const handleClose = () => {
setIsBlockedModalOpen(false);
@ -51,86 +69,50 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
return;
}
data.issue_ids.map((issue) => {
if (!activeWorkspace || !activeProject || !issueDetail) return;
const currentBlockers =
issues?.results
.find((i) => i.id === issue)
?.blocker_issues.map((b) => b.blocker_issue_detail?.id ?? "") ?? [];
issuesService
.patchIssue(activeWorkspace.slug, activeProject.id, issue, {
blockers_list: [...currentBlockers, issueDetail.id],
})
.then((response) => {
mutateIssues((prevData) => ({
...(prevData as IssueResponse),
results: (prevData?.results ?? []).map((issue) => {
if (issue.id === issueDetail.id) {
return { ...issue, ...response };
}
return issue;
}),
}));
})
.catch((error) => {
console.log(error);
});
});
if (!Array.isArray(data.issue_ids)) data.issue_ids = [data.issue_ids];
const newBlocked = [...watch("blocked_list"), ...data.issue_ids];
submitChanges({ blocks_list: newBlocked });
handleClose();
};
const removeBlocked = (issueId: string) => {
if (!activeWorkspace || !activeProject || !issueDetail) return;
const currentBlockers =
issues?.results
.find((i) => i.id === issueId)
?.blocker_issues.map((b) => b.blocker_issue_detail?.id ?? "") ?? [];
const updatedBlockers = currentBlockers.filter((b) => b !== issueDetail.id);
issuesService
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, {
blockers_list: updatedBlockers,
})
.then((response) => {
mutateIssues((prevData) => ({
...(prevData as IssueResponse),
results: (prevData?.results ?? []).map((issue) => {
if (issue.id === issueDetail.id) {
return { ...issue, ...response };
}
return issue;
}),
}));
})
.catch((error) => {
console.log(error);
});
};
return (
<div className="flex items-start py-2 flex-wrap">
<div className="flex flex-wrap items-start py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<FlagIcon className="flex-shrink-0 h-4 w-4" />
<BlockedIcon height={16} width={16} />
<p>Blocked by</p>
</div>
<div className="sm:basis-1/2 space-y-1">
<div className="flex gap-1 flex-wrap">
<div className="space-y-1 sm:basis-1/2">
<div className="flex flex-wrap gap-1">
{watch("blocked_list") && watch("blocked_list").length > 0
? watch("blocked_list").map((issue) => (
<span
key={issue}
className="group flex items-center gap-1 border rounded-2xl text-xs px-1.5 py-0.5 text-red-500 hover:bg-red-50 border-red-500 cursor-pointer"
onClick={() => removeBlocked(issue)}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-white px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500 hover:bg-red-50"
onClick={() => {
const updatedBlocked: string[] = watch("blocked_list").filter(
(i) => i !== issue
);
submitChanges({
blocks_list: updatedBlocked,
});
}}
>
{`${activeProject?.identifier}-${
issues?.results.find((i) => i.id === issue)?.sequence_id
}`}
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
<Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${
issues?.results.find((i) => i.id === issue)?.id
}`}
>
<a className="flex items-center gap-1">
<BlockedIcon height={10} width={10} />
{`${
issues?.results.find((i) => i.id === issue)?.project_detail?.identifier
}-${issues?.results.find((i) => i.id === issue)?.sequence_id}`}
</a>
</Link>
<span className="opacity-0 duration-300 group-hover:opacity-100">
<XMarkIcon className="h-2 w-2" />
</span>
</span>
))
: null}
@ -173,7 +155,7 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
/>
@ -201,40 +183,40 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
htmlFor={`blocked-issue-${issue.id}`}
value={{
name: issue.name,
url: `/projects/${issue.project}/issues/${issue.id}`,
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
}}
className={({ active }) =>
classNames(
"flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2",
"flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
>
{({ active }) => (
<>
<div className="flex items-center gap-2">
<input
type="checkbox"
{...register("issue_ids")}
id={`issue-${issue.id}`}
value={issue.id}
/>
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
<span>{issue.name}</span>
</div>
</>
)}
<div className="flex items-center gap-2">
<input
type="checkbox"
{...register("issue_ids")}
id={`blocked-issue-${issue.id}`}
value={issue.id}
/>
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{
issues?.results.find((i) => i.id === issue.id)
?.project_detail?.identifier
}
-{issue.sequence_id}
</span>
<span>{issue.name}</span>
</div>
</Combobox.Option>
);
}
@ -258,15 +240,15 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
)}
</Combobox>
<div className="flex justify-end items-center gap-2 p-3">
<Button onClick={handleSubmit(onSubmit)} size="sm">
Add selected issues
</Button>
<div className="flex items-center justify-end gap-2 p-3">
<div>
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
Close
</Button>
</div>
<Button onClick={handleSubmit(onSubmit)} size="sm">
Add selected issues
</Button>
</div>
</form>
</Dialog.Panel>
@ -276,7 +258,7 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
</Transition.Root>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() => setIsBlockedModalOpen(true)}
>
Select issues

View file

@ -1,16 +1,24 @@
// react
import React, { useState } from "react";
// react-hook-form
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
// constants
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// services
import issuesServices from "lib/services/issues.service";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { FlagIcon, FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { BlockerIcon, LayerDiagonalIcon } from "ui/icons";
// types
import { IIssue } from "types";
// constants
@ -30,9 +38,20 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
const [query, setQuery] = useState("");
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
const { activeProject, issues } = useUser();
const { setToastAlert } = useToast();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const { register, handleSubmit, reset } = useForm<FormInput>();
const handleClose = () => {
@ -49,36 +68,54 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
});
return;
}
if (!Array.isArray(data.issue_ids)) data.issue_ids = [data.issue_ids];
const newBlockers = [...watch("blockers_list"), ...data.issue_ids];
submitChanges({ blockers_list: newBlockers });
handleClose();
};
return (
<div className="flex items-start py-2 flex-wrap">
<div className="flex flex-wrap items-start py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<FlagIcon className="flex-shrink-0 h-4 w-4" />
<BlockerIcon height={16} width={16} />
<p>Blocking</p>
</div>
<div className="sm:basis-1/2 space-y-1">
<div className="flex gap-1 flex-wrap">
<div className="space-y-1 sm:basis-1/2">
<div className="flex flex-wrap gap-1">
{watch("blockers_list") && watch("blockers_list").length > 0
? watch("blockers_list").map((issue) => (
<span
<div
key={issue}
className="group flex items-center gap-1 border rounded-2xl text-xs px-1.5 py-0.5 text-yellow-500 hover:bg-yellow-50 border-yellow-500 cursor-pointer"
onClick={() => {
const updatedBlockers = watch("blockers_list").filter((i) => i !== issue);
submitChanges({
blockers_list: updatedBlockers,
});
}}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-white px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500 hover:bg-yellow-50"
>
{`${activeProject?.identifier}-${
issues?.results.find((i) => i.id === issue)?.sequence_id
}`}
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
</span>
<Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${
issues?.results.find((i) => i.id === issue)?.id
}`}
>
<a className="flex items-center gap-1">
<BlockerIcon height={10} width={10} />
{`${
issues?.results.find((i) => i.id === issue)?.project_detail?.identifier
}-${issues?.results.find((i) => i.id === issue)?.sequence_id}`}
</a>
</Link>
<span
className="opacity-0 duration-300 group-hover:opacity-100"
onClick={() => {
const updatedBlockers: string[] = watch("blockers_list").filter(
(i) => i !== issue
);
submitChanges({
blockers_list: updatedBlockers,
});
}}
>
<XMarkIcon className="h-2 w-2" />
</span>
</div>
))
: null}
</div>
@ -120,7 +157,7 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
/>
@ -130,65 +167,73 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{issuesList.length > 0 && (
<>
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
Select blocker issues
</h2>
)}
<ul className="text-sm text-gray-700">
{issuesList.map((issue) => {
if (
!watch("blockers_list").includes(issue.id) &&
!watch("blocked_list").includes(issue.id)
) {
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={{
name: issue.name,
url: `/projects/${issue.project}/issues/${issue.id}`,
}}
className={({ active }) =>
classNames(
"flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
>
{({ active }) => (
<>
<div className="flex items-center gap-2">
<input
type="checkbox"
{...register("issue_ids")}
id={`issue-${issue.id}`}
value={issue.id}
/>
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
<span>{issue.name}</span>
</div>
</>
)}
</Combobox.Option>
);
}
})}
</ul>
</li>
</>
{issuesList.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
Select blocker issues
</h2>
)}
<ul className="text-sm text-gray-700">
{issuesList.map((issue) => {
if (
!watch("blockers_list").includes(issue.id) &&
!watch("blocked_list").includes(issue.id)
)
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`blocker-issue-${issue.id}`}
value={{
name: issue.name,
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
}}
className={({ active }) =>
classNames(
"flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
>
<div className="flex items-center gap-2">
<input
type="checkbox"
{...register("issue_ids")}
id={`blocker-issue-${issue.id}`}
value={issue.id}
/>
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{
issues?.results.find((i) => i.id === issue.id)
?.project_detail?.identifier
}
-{issue.sequence_id}
</span>
<span>{issue.name}</span>
</div>
</Combobox.Option>
);
})}
</ul>
</li>
) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" />
<h3 className="text-gray-500">
No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-gray-100 px-2 py-1">
Ctrl/Command + I
</pre>
.
</h3>
</div>
)}
</Combobox.Options>
@ -205,15 +250,15 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
)}
</Combobox>
<div className="flex justify-end items-center gap-2 p-3">
<Button onClick={handleSubmit(onSubmit)} size="sm">
Add selected issues
</Button>
<div className="flex items-center justify-end gap-2 p-3">
<div>
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
Close
</Button>
</div>
<Button onClick={handleSubmit(onSubmit)} size="sm">
Add selected issues
</Button>
</div>
</form>
</Dialog.Panel>
@ -223,7 +268,7 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
</Transition.Root>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() => setIsBlockerModalOpen(true)}
>
Select issues

View file

@ -1,33 +1,67 @@
// react
import React from "react";
// react-hook-form
import { Control, Controller } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { Control, Controller, UseFormWatch } from "react-hook-form";
// constants
import { CYCLE_ISSUES, CYCLE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// services
import issuesService from "lib/services/issues.service";
import cyclesService from "lib/services/cycles.service";
// ui
import { Spinner, CustomSelect } from "ui";
// icons
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// types
import { ICycle, IIssue } from "types";
import { CycleIssueResponse, ICycle, IIssue, IssueResponse } from "types";
// common
import { classNames } from "constants/common";
type Props = {
issueDetail: IIssue | undefined;
control: Control<IIssue, any>;
handleCycleChange: (cycle: ICycle) => void;
watch: UseFormWatch<IIssue>;
};
const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
const { cycles } = useUser();
const SelectCycle: React.FC<Props> = ({ issueDetail, control, handleCycleChange }) => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { data: cycles } = useSWR(
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => cyclesService.getCycles(workspaceSlug as string, projectId as string)
: null
);
const removeIssueFromCycle = (bridgeId: string, cycleId: string) => {
if (!workspaceSlug || !projectId) return;
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
issuesService
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId)
.then((res) => {
console.log(res);
mutate(CYCLE_ISSUES(cycleId));
})
.catch((e) => {
console.log(e);
});
};
return (
<div className="flex items-center py-2 flex-wrap">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<ArrowPathIcon className="flex-shrink-0 h-4 w-4" />
<ArrowPathIcon className="h-4 w-4 flex-shrink-0" />
<p>Cycle</p>
</div>
<div className="sm:basis-1/2">
<div className="space-y-1 sm:basis-1/2">
<Controller
control={control}
name="issue_cycle"
@ -38,24 +72,34 @@ const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden truncate sm:block text-left"
"hidden truncate text-left sm:block"
)}
>
{value ? cycles?.find((c) => c.id === value.cycle_detail.id)?.name : "None"}
{value ? value?.cycle_detail?.name : "None"}
</span>
}
value={value}
onChange={(value: any) => {
handleCycleChange(cycles?.find((c) => c.id === value) as any);
value === null
? removeIssueFromCycle(
issueDetail?.issue_cycle?.id ?? "",
issueDetail?.issue_cycle?.cycle ?? ""
)
: handleCycleChange(cycles?.find((c) => c.id === value) as any);
}}
>
{cycles ? (
cycles.length > 0 ? (
cycles.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
{option.name}
<>
<CustomSelect.Option value={null} className="capitalize">
<>None</>
</CustomSelect.Option>
))
{cycles.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
{option.name}
</CustomSelect.Option>
))}
</>
) : (
<div className="text-center">No cycles found</div>
)

View file

@ -0,0 +1,85 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Control, Controller } from "react-hook-form";
// constants
import { MODULE_LIST } from "constants/fetch-keys";
// services
import modulesService from "lib/services/modules.service";
// ui
import { Spinner, CustomSelect } from "ui";
// icons
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IModule } from "types";
// common
import { classNames } from "constants/common";
type Props = {
control: Control<IIssue, any>;
handleModuleChange: (module: IModule) => void;
};
const SelectModule: React.FC<Props> = ({ control, handleModuleChange }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: modules } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => modulesService.getModules(workspaceSlug as string, projectId as string)
: null
);
return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<RectangleGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Module</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="issue_module"
render={({ field: { value } }) => (
<CustomSelect
label={
<span
className={classNames(
value ? "" : "text-gray-900",
"hidden truncate text-left sm:block"
)}
>
{value ? modules?.find((m) => m.id === value?.module_detail.id)?.name : "None"}
</span>
}
value={value}
onChange={(value: any) => {
handleModuleChange(modules?.find((m) => m.id === value) as any);
}}
>
{modules ? (
modules.length > 0 ? (
modules.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
{option.name}
</CustomSelect.Option>
))
) : (
<div className="text-center">No cycles found</div>
)
) : (
<Spinner />
)}
</CustomSelect>
)}
/>
</div>
</div>
);
};
export default SelectModule;

View file

@ -1,9 +1,14 @@
// react
import React, { useState } from "react";
// react-hook-form
import { useRouter } from "next/router";
import useSWR from "swr";
import { Control, Controller, UseFormWatch } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
// fetch keys
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// services
import issuesServices from "lib/services/issues.service";
// components
import IssuesListModal from "components/project/issues/issues-list-modal";
// icons
@ -28,12 +33,22 @@ const SelectParent: React.FC<Props> = ({
}) => {
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
const { activeProject, issues } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
return (
<div className="flex items-center py-2 flex-wrap">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserIcon className="flex-shrink-0 h-4 w-4" />
<UserIcon className="h-4 w-4 flex-shrink-0" />
<p>Parent</p>
</div>
<div className="sm:basis-1/2">
@ -57,13 +72,13 @@ const SelectParent: React.FC<Props> = ({
/>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() => setIsParentModalOpen(true)}
>
{watch("parent") && watch("parent") !== ""
? `${activeProject?.identifier}-${
issues?.results.find((i) => i.id === watch("parent"))?.sequence_id
}`
? `${
issues?.results.find((i) => i.id === watch("parent"))?.project_detail?.identifier
}-${issues?.results.find((i) => i.id === watch("parent"))?.sequence_id}`
: "Select issue"}
</button>
</div>

View file

@ -2,17 +2,17 @@
import React from "react";
// react-hook-form
import { Control, Controller, UseFormWatch } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CustomSelect } from "ui";
// icons
import { ChevronDownIcon, ChartBarIcon } from "@heroicons/react/24/outline";
import { ChartBarIcon } from "@heroicons/react/24/outline";
// types
import { IIssue } from "types";
// constants
// common
import { classNames } from "constants/common";
import { PRIORITIES } from "constants/";
import CustomSelect from "ui/custom-select";
// constants
import { getPriorityIcon } from "constants/global";
import { PRIORITIES } from "constants/";
type Props = {
control: Control<IIssue, any>;
@ -22,9 +22,9 @@ type Props = {
const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
return (
<div className="flex items-center py-2 flex-wrap">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<ChartBarIcon className="flex-shrink-0 h-4 w-4" />
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
<p>Priority</p>
</div>
<div className="sm:basis-1/2">
@ -37,7 +37,7 @@ const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
<span
className={classNames(
value ? "" : "text-gray-900",
"text-left capitalize flex items-center gap-2"
"flex items-center gap-2 text-left capitalize"
)}
>
{getPriorityIcon(
@ -58,7 +58,7 @@ const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
<CustomSelect.Option key={option} value={option} className="capitalize">
<>
{getPriorityIcon(option, "text-sm")}
{option}
{option ?? "None"}
</>
</CustomSelect.Option>
))}

View file

@ -1,16 +1,21 @@
// react-hook-form
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Control, Controller } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// services
import stateService from "lib/services/state.service";
// icons
import { Squares2X2Icon } from "@heroicons/react/24/outline";
// constants
import { classNames } from "constants/common";
import { STATE_LIST } from "constants/fetch-keys";
// types
import { IIssue } from "types";
import { classNames } from "constants/common";
import { CustomMenu, Spinner } from "ui";
import React from "react";
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import CustomSelect from "ui/custom-select";
// ui
import { Spinner, CustomSelect } from "ui";
type Props = {
control: Control<IIssue, any>;
@ -18,12 +23,20 @@ type Props = {
};
const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
const { states } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
return (
<div className="flex items-center py-2 flex-wrap">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<Squares2X2Icon className="flex-shrink-0 h-4 w-4" />
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
<p>State</p>
</div>
<div className="sm:basis-1/2">
@ -42,7 +55,7 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
{value ? (
<>
<span
className="h-2 w-2 rounded-full flex-shrink-0"
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: states?.find((option) => option.id === value)?.color,
}}
@ -66,7 +79,7 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
<>
{option.color && (
<span
className="h-2 w-2 rounded-full flex-shrink-0"
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: option.color }}
></span>
)}

View file

@ -1,15 +1,14 @@
// react
import React, { useState } from "react";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
// constants
import { classNames } from "constants/common";
// types
import { IIssue } from "types";
import { classNames } from "constants/common";
import useUser from "lib/hooks/useUser";
import { LayerDiagonalIcon } from "ui/icons";
type Props = {
isOpen: boolean;
@ -35,8 +34,6 @@ const IssuesListModal: React.FC<Props> = ({
const [query, setQuery] = useState("");
const [values, setValues] = useState<string[]>([]);
const { activeProject } = useUser();
const handleClose = () => {
onClose();
setQuery("");
@ -51,7 +48,7 @@ const IssuesListModal: React.FC<Props> = ({
return (
<>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-10" onClose={handleClose}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -77,21 +74,14 @@ const IssuesListModal: React.FC<Props> = ({
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
{multiple ? (
<>
<Combobox
value={value}
onChange={(val) => {
// setValues(val);
console.log(val);
}}
multiple
>
<Combobox value={value} onChange={(val) => {}} multiple>
<div className="relative m-1">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
displayValue={() => ""}
@ -116,7 +106,7 @@ const IssuesListModal: React.FC<Props> = ({
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2",
"flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
@ -125,13 +115,13 @@ const IssuesListModal: React.FC<Props> = ({
<>
<input type="checkbox" checked={selected} readOnly />
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>{" "}
{issue.id}
</>
@ -155,7 +145,7 @@ const IssuesListModal: React.FC<Props> = ({
</div>
)}
</Combobox>
<div className="flex justify-end items-center gap-2 p-3">
<div className="flex items-center justify-end gap-2 p-3">
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
Cancel
</Button>
@ -172,7 +162,7 @@ const IssuesListModal: React.FC<Props> = ({
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(e) => setQuery(e.target.value)}
displayValue={() => ""}
@ -183,7 +173,7 @@ const IssuesListModal: React.FC<Props> = ({
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{filteredIssues.length > 0 && (
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
@ -197,30 +187,39 @@ const IssuesListModal: React.FC<Props> = ({
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2",
"flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
onClick={() => handleClose()}
>
{({ selected }) => (
<>
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>{" "}
{issue.name}
</>
)}
<>
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>{" "}
{issue.name}
</>
</Combobox.Option>
))}
</ul>
</li>
) : (
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
<LayerDiagonalIcon height="56" width="56" />
<h3 className="text-gray-500">
No issues found. Create a new issue with{" "}
<pre className="inline rounded bg-gray-100 px-2 py-1">
Ctrl/Command + I
</pre>
.
</h3>
</div>
)}
</Combobox.Options>

View file

@ -1,33 +1,19 @@
// react
import React, { useState } from "react";
// next
import Link from "next/link";
import Image from "next/image";
// swr
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Disclosure, Listbox, Menu, Transition } from "@headlessui/react";
// ui
import { CustomMenu, Spinner } from "ui";
// icons
import {
ChevronDownIcon,
PlusIcon,
CalendarDaysIcon,
EllipsisHorizontalIcon,
} from "@heroicons/react/24/outline";
import User from "public/user.png";
// components
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
// types
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
// services
import workspaceService from "lib/services/workspace.service";
import { Disclosure, Listbox, Transition } from "@headlessui/react";
// hooks
import useUser from "lib/hooks/useUser";
// fetch keys
import { PRIORITIES } from "constants/";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// services
import stateService from "lib/services/state.service";
import issuesService from "lib/services/issues.service";
import projectService from "lib/services/project.service";
import workspaceService from "lib/services/workspace.service";
// constants
import {
addSpaceIfCamelCase,
@ -35,10 +21,26 @@ import {
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
import { PRIORITIES } from "constants/";
import { getPriorityIcon } from "constants/global";
import {
PROJECT_DETAILS,
PROJECT_ISSUES_LIST,
STATE_LIST,
WORKSPACE_MEMBERS,
} from "constants/fetch-keys";
// ui
import { CustomMenu, CustomSelect, Spinner } from "ui";
// icons
import User from "public/user.png";
import { ChevronDownIcon, PlusIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
// components
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
// types
import { IIssue, IProject, IssueResponse, IWorkspaceMember, NestedKeyOf } from "types";
// types
type Props = {
properties: Properties;
groupedByIssues: any;
selectedGroup: NestedKeyOf<IIssue> | null;
setSelectedIssue: any;
@ -47,7 +49,6 @@ type Props = {
};
const ListView: React.FC<Props> = ({
properties,
groupedByIssues,
selectedGroup,
setSelectedIssue,
@ -59,13 +60,39 @@ const ListView: React.FC<Props> = ({
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
>(undefined);
const { activeWorkspace, activeProject, states } = useUser();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: issues } = useSWR<IssueResponse>(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
: null
);
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const { data: people } = useSWR<IWorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
return (
<>
<CreateUpdateIssuesModal
@ -74,23 +101,23 @@ const ListView: React.FC<Props> = ({
prePopulateData={{
...preloadedData,
}}
projectId={activeProject?.id as string}
projectId={projectId as string}
/>
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => (
<Disclosure key={singleGroup} as="div" defaultOpen>
{({ open }) => (
<div className="bg-white rounded-lg">
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
<div className="rounded-lg bg-white">
<div className="rounded-t-lg bg-gray-100 px-4 py-3">
<Disclosure.Button>
<div className="flex items-center gap-x-2">
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
/>
</span>
{selectedGroup !== null ? (
<h2 className="font-medium leading-5 capitalize">
<h2 className="font-medium capitalize leading-5">
{singleGroup === null || singleGroup === "null"
? selectedGroup === "priority" && "No priority"
: selectedGroup === "created_by"
@ -101,7 +128,7 @@ const ListView: React.FC<Props> = ({
) : (
<h2 className="font-medium leading-5">All Issues</h2>
)}
<p className="text-gray-500 text-sm">
<p className="text-sm text-gray-500">
{groupedByIssues[singleGroup as keyof IIssue].length}
</p>
</div>
@ -136,34 +163,36 @@ const ListView: React.FC<Props> = ({
};
});
const totalChildren = issues?.results.filter(
(i) => i.parent === issue.id
).length;
return (
<div
key={issue.id}
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
className="flex items-center justify-between gap-2 px-4 py-3 text-sm"
>
<div className="flex items-center gap-2">
<span
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
<Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
>
<a className="group relative flex items-center gap-2">
{properties.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
{projectDetails?.identifier}-{issue.sequence_id}
</span>
)}
<span>{issue.name}</span>
{/* <div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
<h5 className="font-medium mb-1">Name</h5>
<div>{issue.name}</div>
</div> */}
</a>
</Link>
</div>
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<Listbox
as="div"
@ -177,7 +206,7 @@ const ListView: React.FC<Props> = ({
<>
<div>
<Listbox.Button
className={`rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
className={`flex cursor-pointer items-center gap-x-2 rounded px-2 py-1 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600"
: issue.priority === "high"
@ -189,7 +218,12 @@ const ListView: React.FC<Props> = ({
: "bg-gray-100"
}`}
>
{issue.priority ?? "None"}
{getPriorityIcon(
issue.priority && issue.priority !== ""
? issue.priority ?? ""
: "None",
"text-sm"
)}
</Listbox.Button>
<Transition
@ -199,26 +233,27 @@ const ListView: React.FC<Props> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-36 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{PRIORITIES?.map((priority) => (
<Listbox.Option
key={priority}
className={({ active }) =>
classNames(
active ? "bg-indigo-50" : "bg-white",
"cursor-pointer capitalize select-none px-3 py-2"
"flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize"
)
}
value={priority}
>
{priority}
{getPriorityIcon(priority, "text-sm")}
{priority ?? "None"}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">
Priority
</h5>
<div
@ -242,77 +277,43 @@ const ListView: React.FC<Props> = ({
</Listbox>
)}
{properties.state && (
<Listbox
as="div"
<CustomSelect
label={
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
></span>
{addSpaceIfCamelCase(issue.state_detail.name)}
</>
}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data }, issue.id);
}}
className="group relative flex-shrink-0"
maxHeight="md"
noChevron
>
{({ open }) => (
<>
<div>
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
></span>
{addSpaceIfCamelCase(issue.state_detail.name)}
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
{states?.map((state) => (
<Listbox.Option
key={state.id}
className={({ active }) =>
classNames(
active ? "bg-indigo-50" : "bg-white",
"cursor-pointer select-none px-3 py-2"
)
}
value={state.id}
>
{addSpaceIfCamelCase(state.name)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">State</h5>
<div>{issue.state_detail.name}</div>
</div>
</>
)}
</Listbox>
)}
{properties.start_date && (
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<CalendarDaysIcon className="h-4 w-4" />
{issue.start_date
? renderShortNumericDateFormat(issue.start_date)
: "N/A"}
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">Started at</h5>
<div>
{renderShortNumericDateFormat(issue.start_date ?? "")}
</div>
</div>
</div>
{states?.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: state.color,
}}
></span>
{addSpaceIfCamelCase(state.name)}
</>
</CustomSelect.Option>
))}
</CustomSelect>
)}
{properties.due_date && (
<div
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
className={`group group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
@ -325,8 +326,8 @@ const ListView: React.FC<Props> = ({
{issue.target_date
? renderShortNumericDateFormat(issue.target_date)
: "N/A"}
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1 text-gray-900">
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium text-gray-900">
Target date
</h5>
<div>
@ -347,6 +348,12 @@ const ListView: React.FC<Props> = ({
</div>
</div>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
{totalChildren}{" "}
{totalChildren === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.assignee && (
<Listbox
as="div"
@ -366,7 +373,7 @@ const ListView: React.FC<Props> = ({
<>
<div>
<Listbox.Button>
<div className="flex items-center gap-1 text-xs cursor-pointer">
<div className="flex cursor-pointer items-center gap-1 text-xs">
{assignees.length > 0 ? (
assignees.map((assignee, index: number) => (
<div
@ -376,18 +383,20 @@ const ListView: React.FC<Props> = ({
}`}
>
{assignee.avatar && assignee.avatar !== "" ? (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={assignee.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={assignee?.first_name}
priority={false}
loading="lazy"
/>
</div>
) : (
<div
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
className={`grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 text-white`}
>
{assignee.first_name?.charAt(0)}
</div>
@ -395,13 +404,15 @@ const ListView: React.FC<Props> = ({
</div>
))
) : (
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={User}
height="100%"
width="100%"
className="rounded-full"
alt="No user"
priority={false}
loading="lazy"
/>
</div>
)}
@ -415,7 +426,7 @@ const ListView: React.FC<Props> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{people?.map((person) => (
<Listbox.Option
key={person.id}
@ -447,10 +458,12 @@ const ListView: React.FC<Props> = ({
className="rounded-full"
layout="fill"
objectFit="cover"
priority={false}
loading="lazy"
/>
</div>
) : (
<div className="h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
<div className="grid h-4 w-4 place-items-center rounded-full bg-gray-700 capitalize text-white">
{person.member.first_name &&
person.member.first_name !== ""
? person.member.first_name.charAt(0)
@ -469,8 +482,8 @@ const ListView: React.FC<Props> = ({
</Listbox.Options>
</Transition>
</div>
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
<h5 className="font-medium mb-1">Assigned to</h5>
<div className="absolute bottom-full right-0 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-white p-2 shadow-md group-hover:block">
<h5 className="mb-1 font-medium">Assigned to</h5>
<div>
{issue.assignee_details?.length > 0
? issue.assignee_details
@ -507,10 +520,10 @@ const ListView: React.FC<Props> = ({
);
})
) : (
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
)
) : (
<div className="h-full w-full flex items-center justify-center">
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
)}
@ -520,7 +533,7 @@ const ListView: React.FC<Props> = ({
<div className="p-3">
<button
type="button"
className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium"
className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-gray-100"
onClick={() => {
setIsCreateIssuesModalOpen(true);
if (selectedGroup !== null) {

View file

@ -1,6 +1,7 @@
// react
import React from "react";
// swr
import { useRouter } from "next/router";
import useSWR from "swr";
// hooks
import useUser from "lib/hooks/useUser";
@ -25,11 +26,12 @@ type Props = {
};
const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
const { activeWorkspace } = useUser();
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: states } = useSWR<IState[]>(
activeWorkspace ? STATE_LIST(issue.project) : null,
activeWorkspace ? () => stateServices.getStates(activeWorkspace.slug, issue.project) : null
workspaceSlug ? STATE_LIST(issue.project) : null,
workspaceSlug ? () => stateServices.getStates(workspaceSlug as string, issue.project) : null
);
return (
@ -38,8 +40,8 @@ const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
as="div"
value={issue.state}
onChange={(data: string) => {
if (!activeWorkspace) return;
updateIssues(activeWorkspace.slug, issue.project, issue.id, {
if (!workspaceSlug) return;
updateIssues(workspaceSlug as string, issue.project, issue.id, {
state: data,
state_detail: states?.find((state) => state.id === data),
});
@ -50,7 +52,7 @@ const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
<>
<div>
<Listbox.Button
className="inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 border"
className="inline-flex items-center whitespace-nowrap rounded-full border bg-gray-50 px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100"
style={{
border: `2px solid ${issue.state_detail.color}`,
backgroundColor: `${issue.state_detail.color}20`,
@ -59,7 +61,7 @@ const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
<span
className={classNames(
issue.state ? "" : "text-gray-900",
"hidden capitalize sm:block w-16"
"hidden w-16 capitalize sm:block"
)}
>
{addSpaceIfCamelCase(issue.state_detail.name)}
@ -73,7 +75,7 @@ const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="fixed z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<Listbox.Options className="fixed z-10 mt-1 max-h-28 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{states?.map((state) => (
<Listbox.Option
key={state.id}