Refactoring Phase 1 (#199)

* style: added cta at the bottom of sidebar, added missing icons as well, showing dynamic workspace member count on workspace dropdown

* refractor: running parallel request,

made create/edit label function to async function

* fix: sidebar dropdown content going below kanban items

outside click detection in need help dropdown

* refractor: making parallel api calls

fix: create state input comes at bottom, create state input gets on focus automatically, form is getting submitted on enter click

* refactoring file structure and signin page

* style: changed text and added spinner for signing in loading

* refractor: removed unused type

* fix: my issue cta in profile page sending to 404 page

* fix: added new s3 bucket url in next.config.js file

increased image modal height

* packaging UI components

* eslint config

* eslint fixes

* refactoring changes

* build fixes

* minor fixes

* adding todo comments for reference

* refactor: cleared unused imports and re ordered imports

* refactor: removed unused imports

* fix: added workspace argument to useissues hook

* refactor: removed api-routes file, unnecessary constants

* refactor: created helpers folder, removed unnecessary constants

* refactor: new context for issue view

* refactoring issues page

* build fixes

* refactoring

* refactor: create issue modal

* refactor: module ui

* fix: sub-issues mutation

* fix: create more option in create issue modal

* description form debounce issue

* refactor: global component for assignees list

* fix: link module interface

* fix: priority icons and sub-issues count added

* fix: cycle mutation in issue details page

* fix: remove issue from cycle mutation

* fix: create issue modal in home page

* fix: removed unnecessary props

* fix: updated create issue form status

* fix: settings auth breaking

* refactor: issue details page

Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: venkatesh-soulpage <venkatesh.marreboyina@soulpageit.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
This commit is contained in:
sriram veeraghanta 2023-01-26 23:42:20 +05:30 committed by GitHub
parent 9134b0c543
commit 9075f9441c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
322 changed files with 14149 additions and 21378 deletions

10
.eslintrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
root: true,
// This tells ESLint to load the config from the package `config`
// extends: ["custom"],
settings: {
next: {
rootDir: ["apps/*/"],
},
},
};

View File

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

1
apps/app/.eslintrc.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("config/.eslintrc");

View File

@ -14,7 +14,7 @@ ENV PATH="${PATH}:./pnpm"
COPY ./apps ./apps
COPY ./package.json ./package.json
COPY ./.eslintrc.json ./.eslintrc.json
COPY ./.eslintrc.js ./.eslintrc.js
COPY ./turbo.json ./turbo.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml

View File

@ -1,28 +1,28 @@
import React, { useState } from "react";
// react hook form
import { useForm } from "react-hook-form";
// ui
import { Button, Input } from "ui";
import authenticationService from "lib/services/authentication.service";
// icons
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { Button, Input } from "components/ui";
// services
import authenticationService from "services/authentication.service";
// icons
// types
type SignIn = {
type EmailCodeFormValues = {
email: string;
key?: string;
token?: string;
};
const EmailCodeForm = ({ onSuccess }: any) => {
export const EmailCodeForm = ({ onSuccess }: any) => {
const [codeSent, setCodeSent] = useState(false);
const {
register,
handleSubmit,
setError,
setValue,
formState: { errors, isSubmitting, dirtyFields, isValid, isDirty },
} = useForm<SignIn>({
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailCodeFormValues>({
defaultValues: {
email: "",
key: "",
@ -32,9 +32,8 @@ const EmailCodeForm = ({ onSuccess }: any) => {
reValidateMode: "onChange",
});
const onSubmit = ({ email }: SignIn) => {
const onSubmit = ({ email }: EmailCodeFormValues) => {
console.log(email);
authenticationService
.emailCode({ email })
.then((res) => {
@ -46,15 +45,15 @@ const EmailCodeForm = ({ onSuccess }: any) => {
});
};
const handleSignin = (formData: SignIn) => {
const handleSignin = (formData: EmailCodeFormValues) => {
authenticationService
.magicSignIn(formData)
.then(async (response) => {
await onSuccess(response);
.then((response) => {
onSuccess(response);
})
.catch((error) => {
console.log(error);
setError("token" as keyof SignIn, {
setError("token" as keyof EmailCodeFormValues, {
type: "manual",
message: error.error,
});
@ -127,5 +126,3 @@ const EmailCodeForm = ({ onSuccess }: any) => {
</>
);
};
export default EmailCodeForm;

View File

@ -1,29 +1,26 @@
import React from "react";
// next
import Link from "next/link";
import { useRouter } from "next/router";
// react hook form
import { useForm } from "react-hook-form";
// ui
import { Button, Input } from "ui";
import authenticationService from "lib/services/authentication.service";
import { Button, Input } from "components/ui";
import authenticationService from "services/authentication.service";
// types
type SignIn = {
type EmailPasswordFormValues = {
email: string;
password?: string;
medium?: string;
};
const EmailPasswordForm = ({ onSuccess }: any) => {
export const EmailPasswordForm = ({ onSuccess }: any) => {
const {
register,
handleSubmit,
setError,
setValue,
getValues,
formState: { errors, isSubmitting, dirtyFields, isValid, isDirty },
} = useForm<SignIn>({
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailPasswordFormValues>({
defaultValues: {
email: "",
password: "",
@ -33,11 +30,11 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
reValidateMode: "onChange",
});
const onSubmit = (formData: SignIn) => {
const onSubmit = (formData: EmailPasswordFormValues) => {
authenticationService
.emailLogin(formData)
.then(async (response) => {
await onSuccess(response);
.then((response) => {
onSuccess(response);
})
.catch((error) => {
console.log(error);
@ -45,7 +42,7 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
Object.keys(error.response.data).forEach((key) => {
const err = error.response.data[key];
console.log("err", err);
setError(key as keyof SignIn, {
setError(key as keyof EmailPasswordFormValues, {
type: "manual",
message: Array.isArray(err) ? err.join(", ") : err,
});
@ -85,8 +82,8 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
placeholder="Enter your password"
/>
</div>
<div className="flex items-center justify-between mt-2">
<div className="text-sm ml-auto">
<div className="mt-2 flex items-center justify-between">
<div className="ml-auto text-sm">
<Link href={"/forgot-password"}>
<a className="font-medium text-theme hover:text-indigo-500">Forgot your password?</a>
</Link>
@ -105,5 +102,3 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
</>
);
};
export default EmailPasswordForm;

View File

@ -0,0 +1,46 @@
import { useState, FC } from "react";
import { KeyIcon } from "@heroicons/react/24/outline";
// components
import { EmailCodeForm, EmailPasswordForm } from "components/account";
export interface EmailSignInFormProps {
handleSuccess: () => void;
}
export const EmailSignInForm: FC<EmailSignInFormProps> = (props) => {
const { handleSuccess } = props;
// states
const [useCode, setUseCode] = useState(true);
return (
<>
{useCode ? (
<EmailCodeForm onSuccess={handleSuccess} />
) : (
<EmailPasswordForm onSuccess={handleSuccess} />
)}
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">or</span>
</div>
</div>
<div className="mt-6 flex w-full flex-col items-stretch gap-y-2">
<button
type="button"
className="flex w-full items-center rounded border border-gray-300 px-3 py-2 text-sm duration-300 hover:bg-gray-100"
onClick={() => setUseCode((prev) => !prev)}
>
<KeyIcon className="h-[25px] w-[25px]" />
<span className="w-full text-center font-medium">
{useCode ? "Continue with Password" : "Continue with Code"}
</span>
</button>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,51 @@
import { useEffect, useState, FC } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
// images
import githubImage from "/public/logos/github.png";
const { NEXT_PUBLIC_GITHUB_ID } = process.env;
export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>;
}
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const { handleSignIn } = props;
// router
const {
query: { code },
} = useRouter();
// states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
useEffect(() => {
if (code) {
handleSignIn(code.toString());
}
}, [code, handleSignIn]);
useEffect(() => {
const origin =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
setLoginCallBackURL(`${origin}/signin` as any);
}, []);
return (
<Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}`}
>
<button className="flex w-full items-center rounded bg-black px-3 py-2 text-sm text-white opacity-90 duration-300 hover:opacity-100">
<Image
src={githubImage}
height={25}
width={25}
className="flex-shrink-0"
alt="GitHub Logo"
/>
<span className="w-full text-center font-medium">Continue with GitHub</span>
</button>
</Link>
);
};

View File

@ -4,12 +4,13 @@ import Script from "next/script";
export interface IGoogleLoginButton {
text?: string;
onSuccess?: (res: any) => void;
onFailure?: (res: any) => void;
handleSignIn: React.Dispatch<any>;
styles?: CSSProperties;
}
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
const { handleSignIn } = props;
const googleSignInButton = useRef<HTMLDivElement>(null);
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
@ -17,7 +18,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
callback: props.onSuccess as any,
callback: handleSignIn,
});
window?.google?.accounts.id.renderButton(
googleSignInButton.current,
@ -32,7 +33,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
);
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true);
}, [props.onSuccess, gsiScriptLoaded]);
}, [handleSignIn, gsiScriptLoaded]);
useEffect(() => {
if (window?.google?.accounts?.id) {
@ -46,7 +47,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
return (
<>
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
<div className="w-full" id="googleSignInButton" ref={googleSignInButton}></div>
<div className="w-full" id="googleSignInButton" ref={googleSignInButton} />
</>
);
};

View File

@ -0,0 +1,5 @@
export * from "./google-login";
export * from "./email-code-form";
export * from "./email-password-form";
export * from "./github-login-button";
export * from "./email-signin-form";

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// icons
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
@ -32,29 +32,27 @@ type BreadcrumbItemProps = {
icon?: any;
};
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => {
return (
<>
{link ? (
<Link href={link}>
<a className="border-r-2 border-gray-300 px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon ?? null}
{title}
</p>
</a>
</Link>
) : (
<div className="px-3 text-sm">
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => (
<>
{link ? (
<Link href={link}>
<a className="border-r-2 border-gray-300 px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon}
{icon ?? null}
{title}
</p>
</div>
)}
</>
);
};
</a>
</Link>
) : (
<div className="px-3 text-sm">
<p className={`${icon ? "flex items-center gap-2" : ""}`}>
{icon}
{title}
</p>
</div>
)}
</>
);
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;

View File

@ -1,40 +1,39 @@
// TODO: Refactor this component: into a different file, use this file to export the components
import React, { useState, useCallback, useEffect } from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// hooks
import useTheme from "lib/hooks/useTheme";
import useToast from "lib/hooks/useToast";
import useUser from "lib/hooks/useUser";
// services
import userService from "lib/services/user.service";
// components
import ShortcutsModal from "components/command-palette/shortcuts";
import { CreateProjectModal } from "components/project";
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// constants
import { USER_ISSUE } from "constants/fetch-keys";
// ui
import { Button } from "ui";
// icons
import {
FolderIcon,
RectangleStackIcon,
ClipboardDocumentListIcon,
MagnifyingGlassIcon,
} from "@heroicons/react/24/outline";
import useTheme from "hooks/use-theme";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// services
import userService from "services/user.service";
// components
import ShortcutsModal from "components/command-palette/shortcuts";
import { CreateProjectModal } from "components/project";
import { CreateUpdateIssueModal } from "components/issues/modal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal";
// headless ui
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
// common
import { classNames, copyTextToClipboard } from "constants/common";
// ui
import { Button } from "components/ui";
// icons
// fetch-keys
import { USER_ISSUE } from "constants/fetch-keys";
const CommandPalette: React.FC = () => {
const [query, setQuery] = useState("");
@ -173,10 +172,9 @@ const CommandPalette: React.FC = () => {
/>
</>
)}
<CreateUpdateIssuesModal
<CreateUpdateIssueModal
isOpen={isIssueModalOpen}
setIsOpen={setIsIssueModalOpen}
projectId={projectId as string}
handleClose={() => setIsIssueModalOpen(false)}
/>
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen}
@ -188,7 +186,7 @@ const CommandPalette: React.FC = () => {
afterLeave={() => setQuery("")}
appear
>
<Dialog as="div" className="relative z-10" onClose={handleCommandPaletteClose}>
<Dialog as="div" className="relative z-20" onClose={handleCommandPaletteClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -201,7 +199,7 @@ const CommandPalette: React.FC = () => {
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -228,7 +226,7 @@ const CommandPalette: React.FC = () => {
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..."
autoComplete="off"
onChange={(event) => setQuery(event.target.value)}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
@ -255,10 +253,9 @@ const CommandPalette: React.FC = () => {
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",
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
active ? "bg-gray-500 bg-opacity-5 text-gray-900" : ""
)
}`
}
>
{({ active }) => (
@ -307,19 +304,17 @@ const CommandPalette: React.FC = () => {
onClick: action.onClick,
}}
className={({ active }) =>
classNames(
"flex cursor-default select-none items-center rounded-md px-3 py-2",
`flex cursor-default select-none items-center rounded-md px-3 py-2 ${
active ? "bg-gray-500 bg-opacity-5 text-gray-900" : ""
)
}`
}
>
{({ active }) => (
<>
<action.icon
className={classNames(
"h-6 w-6 flex-none text-gray-900 text-opacity-40",
className={`h-6 w-6 flex-none text-gray-900 text-opacity-40 ${
active ? "text-opacity-100" : ""
)}
}`}
aria-hidden="true"
/>
<span className="ml-3 flex-auto truncate">{action.name}</span>

View File

@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
// ui
import { Input } from "ui";
import { Input } from "components/ui";
type Props = {
isOpen: boolean;
@ -52,7 +52,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" onClose={setIsOpen}>
<Dialog as="div" className="relative z-20" onClose={setIsOpen}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -65,7 +65,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
@ -79,10 +79,10 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white p-5">
<div className="sm:flex sm:items-start">
<div className="flex flex-col gap-y-4 text-center sm:text-left w-full">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 flex justify-between"
className="flex justify-between text-lg font-medium leading-6 text-gray-900"
>
<span>Keyboard Shortcuts</span>
<span>
@ -103,11 +103,11 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="flex flex-col gap-y-3 w-full">
<div className="flex w-full flex-col gap-y-3">
{filteredShortcuts.length > 0 ? (
filteredShortcuts.map(({ title, shortcuts }) => (
<div key={title} className="w-full flex flex-col">
<p className="font-medium mb-4">{title}</p>
<div key={title} className="flex w-full flex-col">
<p className="mb-4 font-medium">{title}</p>
<div className="flex flex-col gap-y-3">
{shortcuts.map(({ keys, description }, index) => (
<div key={index} className="flex justify-between">
@ -115,7 +115,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="flex items-center gap-x-1">
{keys.split(",").map((key, index) => (
<span key={index} className="flex items-center gap-1">
<kbd className="bg-gray-200 text-sm px-1 rounded">
<kbd className="rounded bg-gray-200 px-1 text-sm">
{key}
</kbd>
</span>

View File

@ -0,0 +1,109 @@
import React from "react";
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, NestedKeyOf } from "types";
type Props = {
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
groupedByIssues: {
[key: string]: IIssue[];
};
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
createdBy: string | null;
bgColor: string;
addIssueToState: () => void;
provided?: DraggableProvided;
};
const BoardHeader: React.FC<Props> = ({
isCollapsed,
setIsCollapsed,
provided,
groupedByIssues,
selectedGroup,
groupTitle,
createdBy,
bgColor,
addIssueToState,
}) => (
<div
className={`flex justify-between p-3 pb-0 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
{provided && (
<button
type="button"
{...provided.dragHandleProps}
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="mt-[-0.7rem] h-4 w-4 text-gray-600" />
</button>
)}
<div
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}`,
backgroundColor: `${bgColor}20`,
}}
>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
>
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
</div>
);
export default BoardHeader;

View File

@ -1,5 +1,3 @@
const SingleBoard = () => {
return <></>;
};
const SingleBoard = () => <></>;
export default SingleBoard;

View File

@ -10,40 +10,32 @@ import { DraggableStateSnapshot } from "react-beautiful-dnd";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// constants
import { PRIORITIES } from "constants/";
import { PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys";
import { getPriorityIcon } from "constants/global";
// services
import issuesService from "lib/services/issues.service";
import stateService from "lib/services/state.service";
import projectService from "lib/services/project.service";
// icons
import { TrashIcon } from "@heroicons/react/24/outline";
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import User from "public/user.png";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
import projectService from "services/project.service";
// components
import { AssigneesList } from "components/ui/avatar";
// helpers
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IssueResponse, IWorkspaceMember, Properties } from "types";
import { IIssue, IssueResponse, IUserLite, IWorkspaceMember, Properties } from "types";
// common
import {
addSpaceIfCamelCase,
classNames,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
import { PROJECT_DETAILS } from "constants/fetch-keys";
import { PRIORITIES } from "constants/";
import { PROJECT_ISSUES_LIST, STATE_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import { getPriorityIcon } from "constants/global";
type Props = {
issue: IIssue;
properties: Properties;
snapshot?: DraggableStateSnapshot;
assignees: {
avatar: string | undefined;
first_name: string | undefined;
email: string | undefined;
}[];
assignees: Partial<IUserLite>[] | (Partial<IUserLite> | undefined)[];
people: IWorkspaceMember[] | undefined;
handleDeleteIssue?: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, childIssueId: string) => void;
partialUpdateIssue: any;
};
const SingleBoardIssue: React.FC<Props> = ({
@ -157,10 +149,9 @@ const SingleBoardIssue: React.FC<Props> = ({
<Listbox.Option
key={priority}
className={({ active }) =>
classNames(
active ? "bg-indigo-50" : "bg-white",
"flex cursor-pointer select-none items-center gap-2 px-3 py-2 capitalize"
)
`flex cursor-pointer select-none items-center gap-2 px-3 py-2 capitalize ${
active ? "bg-indigo-50" : "bg-white"
}`
}
value={priority}
>
@ -193,7 +184,7 @@ const SingleBoardIssue: React.FC<Props> = ({
style={{
backgroundColor: issue.state_detail.color,
}}
></span>
/>
{addSpaceIfCamelCase(issue.state_detail.name)}
</Listbox.Button>
@ -209,10 +200,9 @@ const SingleBoardIssue: React.FC<Props> = ({
<Listbox.Option
key={state.id}
className={({ active }) =>
classNames(
active ? "bg-indigo-50" : "bg-white",
"flex cursor-pointer select-none items-center gap-2 px-3 py-2"
)
`flex cursor-pointer select-none items-center gap-2 px-3 py-2 ${
active ? "bg-indigo-50" : "bg-white"
}`
}
value={state.id}
>
@ -221,7 +211,7 @@ const SingleBoardIssue: React.FC<Props> = ({
style={{
backgroundColor: state.color,
}}
></span>
/>
{addSpaceIfCamelCase(state.name)}
</Listbox.Option>
))}
@ -261,11 +251,10 @@ const SingleBoardIssue: React.FC<Props> = ({
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) {
newData.splice(newData.indexOf(data), 1);
} else {
newData.push(data);
}
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: newData }, issue.id);
}}
className="group relative flex-shrink-0"
@ -275,48 +264,7 @@ const SingleBoardIssue: React.FC<Props> = ({
<div>
<Listbox.Button>
<div className="flex cursor-pointer items-center gap-1 text-xs">
{assignees.length > 0 ? (
assignees.map((assignee, index: number) => (
<div
key={index}
className={`relative z-[1] h-5 w-5 rounded-full ${
index !== 0 ? "-ml-2.5" : ""
}`}
>
{assignee.avatar && assignee.avatar !== "" ? (
<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="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
{assignee.first_name && assignee.first_name !== ""
? assignee.first_name.charAt(0)
: assignee?.email?.charAt(0)}
</div>
)}
</div>
))
) : (
<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>
)}
<AssigneesList users={assignees} length={3} />
</div>
</Listbox.Button>
@ -332,19 +280,20 @@ const SingleBoardIssue: React.FC<Props> = ({
<Listbox.Option
key={person.id}
className={({ active }) =>
classNames(
active ? "bg-indigo-50" : "bg-white",
"cursor-pointer select-none p-2"
)
`cursor-pointer select-none p-2 ${
active ? "bg-indigo-50" : "bg-white"
}`
}
value={person.member.id}
>
<div
className={`flex items-center gap-x-1 ${
assignees.includes({
avatar: person.member.avatar,
id: person.member.last_name,
first_name: person.member.first_name,
last_name: person.member.last_name,
email: person.member.email,
avatar: person.member.avatar,
})
? "font-medium"
: "font-normal"

View File

@ -7,23 +7,21 @@ import useSWR, { mutate } from "swr";
// react hook form
import { SubmitHandler, useForm } from "react-hook-form";
// services
import issuesServices from "lib/services/issues.service";
import projectService from "lib/services/project.service";
// hooks
import useToast from "lib/hooks/useToast";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { FolderIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import issuesServices from "services/issues.service";
import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
// headless ui
// ui
import { Button } from "components/ui";
// icons
import { LayerDiagonalIcon } from "components/icons";
// types
import { IIssue, IssueResponse } from "types";
// fetch keys
import { PROJECT_ISSUES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// common
import { classNames } from "constants/common";
import { LayerDiagonalIcon } from "ui/icons";
type FormInput = {
issue_ids: string[];
@ -62,7 +60,12 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
const { setToastAlert } = useToast();
const { register, handleSubmit, reset } = useForm<FormInput>();
const {
register,
handleSubmit,
reset,
formState: { isSubmitting },
} = useForm<FormInput>();
const filteredIssues: IIssue[] =
query === ""
@ -99,17 +102,15 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
});
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
(prevData) => {
return {
...(prevData as IssueResponse),
count: (prevData?.results ?? []).filter(
(p) => !data.issue_ids.some((id) => p.id === id)
).length,
results: (prevData?.results ?? []).filter(
(p) => !data.issue_ids.some((id) => p.id === id)
),
};
},
(prevData) => ({
...(prevData as IssueResponse),
count: (prevData?.results ?? []).filter(
(p) => !data.issue_ids.some((id) => p.id === id)
).length,
results: (prevData?.results ?? []).filter(
(p) => !data.issue_ids.some((id) => p.id === id)
),
}),
false
);
})
@ -120,140 +121,142 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
};
return (
<>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-10" onClose={handleClose}>
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
</Transition.Child>
<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">
<form>
<Combobox>
<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 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<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">
<form>
<Combobox>
<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 outline-none focus:ring-0 sm:text-sm"
placeholder="Search..."
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<Combobox.Options
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
Select issues to delete
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`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={`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">
{projectDetails?.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>
{query !== "" && filteredIssues.length === 0 && (
<div className="py-14 px-6 text-center sm:px-14">
<FolderIcon
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
aria-hidden="true"
/>
<p className="mt-4 text-sm text-gray-900">
We couldn{"'"}t find any issue with that term. Please try again.
</p>
<Combobox.Options
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
Select issues to delete
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={{
name: issue.name,
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
}}
className={({ active }) =>
`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={`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">
{projectDetails?.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>
</Combobox.Options>
{filteredIssues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3">
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
Close
</Button>
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
Delete selected issues
</Button>
{query !== "" && filteredIssues.length === 0 && (
<div className="py-14 px-6 text-center sm:px-14">
<FolderIcon
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
aria-hidden="true"
/>
<p className="mt-4 text-sm text-gray-900">
We couldn{"'"}t find any issue with that term. Please try again.
</p>
</div>
)}
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
</Combobox>
{filteredIssues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3">
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
Close
</Button>
<Button
onClick={handleSubmit(handleDelete)}
theme="danger"
size="sm"
disabled={isSubmitting}
>
{isSubmitting ? "Deleting..." : "Delete selected issues"}
</Button>
</div>
)}
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -6,22 +6,19 @@ import useSWR from "swr";
// react-hook-form
import { Controller, SubmitHandler, useForm } from "react-hook-form";
// hooks
import useToast from "lib/hooks/useToast";
// services
import projectService from "lib/services/project.service";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
import useToast from "hooks/use-toast";
// services
import projectService from "services/project.service";
// headless ui
// ui
import { Button } from "components/ui";
import { LayerDiagonalIcon } from "components/icons";
// types
import { IIssue } from "types";
// fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys";
// common
import { classNames } from "constants/common";
import { LayerDiagonalIcon } from "ui/icons";
type FormInput = {
issues: string[];
@ -32,7 +29,7 @@ type Props = {
handleClose: () => void;
type: string;
issues: IIssue[];
handleOnSubmit: (data: FormInput) => void;
handleOnSubmit: any;
};
const ExistingIssuesListModal: React.FC<Props> = ({
@ -73,7 +70,7 @@ const ExistingIssuesListModal: React.FC<Props> = ({
},
});
const onSubmit: SubmitHandler<FormInput> = (data) => {
const onSubmit: SubmitHandler<FormInput> = async (data) => {
if (!data.issues || data.issues.length === 0) {
setToastAlert({
title: "Error",
@ -83,7 +80,7 @@ const ExistingIssuesListModal: React.FC<Props> = ({
return;
}
handleOnSubmit(data);
await handleOnSubmit(data);
handleClose();
};
@ -149,38 +146,35 @@ const ExistingIssuesListModal: React.FC<Props> = ({
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => {
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue.id}
className={({ active }) =>
classNames(
"flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
>
{({ selected }) => (
<>
<input type="checkbox" checked={selected} readOnly />
<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">
{projectDetails?.identifier}-{issue.sequence_id}
</span>
{issue.name}
</>
)}
</Combobox.Option>
);
})}
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue.id}
className={({ active }) =>
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
}`
}
>
{({ selected }) => (
<>
<input type="checkbox" checked={selected} readOnly />
<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">
{projectDetails?.identifier}-{issue.sequence_id}
</span>
{issue.name}
</>
)}
</Combobox.Option>
))}
</ul>
</li>
) : (

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useState } from "react";
import NextImage from "next/image";
import { useRouter } from "next/router";
@ -8,11 +8,11 @@ import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// services
import fileServices from "lib/services/file.service";
import fileServices from "services/file.service";
// icon
import { UserCircleIcon } from "ui/icons";
import { UserCircleIcon } from "components/icons";
// ui
import { Button } from "ui";
import { Button } from "components/ui";
type TImageUploadModalProps = {
value?: string | null;
@ -70,7 +70,7 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-10" onClose={handleClose}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -83,7 +83,7 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
@ -103,13 +103,13 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
<div className="flex items-center gap-3">
<div
{...getRootProps()}
className={`relative block w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
className={`relative block h-80 w-full rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
(image === null && isDragActive) || !value
? "border-2 border-dashed border-gray-300 hover:border-gray-400"
: ""
}`}
>
{value && value !== "" ? (
{image !== null || (value && value !== null && value !== "") ? (
<>
<button
type="button"
@ -121,7 +121,7 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
<NextImage
layout="fill"
objectFit="cover"
src={image ? URL.createObjectURL(image) : value}
src={image ? URL.createObjectURL(image) : value ? value : ""}
alt="image"
/>
</>

View File

@ -1,59 +1,99 @@
// next
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// services
import issuesService from "lib/services/issues.service";
import issuesService from "services/issues.service";
import workspaceService from "services/workspace.service";
import stateService from "services/state.service";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CustomMenu } from "ui";
import { CustomMenu, CustomSelect, AssigneesList, Avatar } from "components/ui";
// components
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
// icons
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IssueResponse, Properties } from "types";
import { IIssue, IWorkspaceMember, Properties } from "types";
// fetch-keys
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// common
import {
addSpaceIfCamelCase,
findHowManyDaysLeft,
renderShortNumericDateFormat,
} from "constants/common";
CYCLE_ISSUES,
MODULE_ISSUES,
PROJECT_ISSUES_LIST,
STATE_LIST,
WORKSPACE_MEMBERS,
} from "constants/fetch-keys";
// constants
import { getPriorityIcon } from "constants/global";
import { PRIORITIES } from "constants/";
type Props = {
type?: string;
typeId?: string;
issue: IIssue;
properties: Properties;
editIssue: () => void;
handleDeleteIssue: () => void;
removeIssue: () => void;
removeIssue?: () => void;
};
const SingleListIssue: React.FC<Props> = ({
type,
typeId,
issue,
properties,
editIssue,
handleDeleteIssue,
removeIssue,
}) => {
const router = useRouter();
let { workspaceSlug, projectId } = router.query;
const [deleteIssue, setDeleteIssue] = useState<IIssue | undefined>();
const { data: issues } = useSWR<IssueResponse>(
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const totalChildren = issues?.results.filter((i) => i.parent === issue.id).length;
const { data: people } = useSWR<IWorkspaceMember[]>(
workspaceSlug ? WORKSPACE_MEMBERS : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const partialUpdateIssue = (formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId) return;
issuesService
.patchIssue(workspaceSlug as string, projectId as string, issue.id, formData)
.then((res) => {
if (typeId) {
mutate(CYCLE_ISSUES(typeId ?? ""));
mutate(MODULE_ISSUES(typeId ?? ""));
}
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
})
.catch((error) => {
console.log(error);
});
};
return (
<>
<div key={issue.id} className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<ConfirmIssueDeletion
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={deleteIssue}
/>
<div className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<span
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
@ -74,55 +114,117 @@ const SingleListIssue: React.FC<Props> = ({
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<div
className={`group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded px-2 py-1 text-xs 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"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
<Listbox
as="div"
value={issue.priority}
onChange={(data: string) => {
partialUpdateIssue({ priority: data });
}}
className="group relative flex-shrink-0"
>
{/* {getPriorityIcon(issue.priority ?? "")} */}
{issue.priority ?? "None"}
<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
className={`capitalize ${
issue.priority === "urgent"
? "text-red-600"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"
? "text-yellow-500"
: issue.priority === "low"
? "text-green-500"
: ""
}`}
>
{issue.priority ?? "None"}
</div>
</div>
</div>
{({ open }) => (
<>
<div>
<Listbox.Button
className={`flex cursor-pointer items-center gap-x-2 rounded px-2 py-0.5 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"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<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 }) =>
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
active ? "bg-indigo-50" : "bg-white"
}`
}
value={priority}
>
{getPriorityIcon(priority, "text-sm")}
{priority ?? "None"}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<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
className={`capitalize ${
issue.priority === "urgent"
? "text-red-600"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"
? "text-yellow-500"
: issue.priority === "low"
? "text-green-500"
: ""
}`}
>
{issue.priority ?? "None"}
</div>
</div>
</>
)}
</Listbox>
)}
{properties.state && (
<div className="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">
<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)}
<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">State</h5>
<div>{issue?.state_detail.name}</div>
</div>
</div>
<CustomSelect
label={
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
{addSpaceIfCamelCase(issue.state_detail.name)}
</>
}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data });
}}
maxHeight="md"
noChevron
>
{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,
}}
/>
{addSpaceIfCamelCase(state.name)}
</>
</CustomSelect.Option>
))}
</CustomSelect>
)}
{properties.due_date && (
<div
@ -150,18 +252,88 @@ const SingleListIssue: React.FC<Props> = ({
</div>
</div>
)}
{properties.sub_issue_count && projectId && (
{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"}
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.assignee && (
<Listbox
as="div"
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: newData });
}}
className="group relative flex-shrink-0"
>
{({ open }) => (
<>
<div>
<Listbox.Button>
<div className="flex cursor-pointer items-center gap-1 text-xs">
<AssigneesList userIds={issue.assignees ?? []} />
</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<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}
className={({ active, selected }) =>
`flex items-center gap-x-1 cursor-pointer select-none p-2 ${
active ? "bg-indigo-50" : ""
} ${
selected || issue.assignees?.includes(person.member.id)
? "bg-indigo-50 font-medium"
: "font-normal"
}`
}
value={person.member.id}
>
<Avatar user={person.member} />
<p>
{person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email}
</p>
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
<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.map((assignee) => assignee.first_name).join(", ")
: "No one"}
</div>
</div>
</>
)}
</Listbox>
)}
{type && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={() => editIssue()}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => removeIssue()}>
<>Remove from {type}</>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleDeleteIssue()}>
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
{type !== "issue" && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => setDeleteIssue(issue)}>
Delete permanently
</CustomMenu.MenuItem>
</CustomMenu>

View File

@ -5,9 +5,9 @@ import { useRouter } from "next/router";
// layouts
import DefaultLayout from "layouts/default-layout";
// hooks
import useUser from "lib/hooks/useUser";
import useUser from "hooks/use-user";
// icons
import { LockIcon } from "ui/icons";
import { LockIcon } from "components/icons";
type TNotAuthorizedViewProps = {
actionButton?: React.ReactNode;

View File

@ -1,60 +1,84 @@
import React from "react";
import { useRouter } from "next/router";
// hooks
import useIssuesProperties from "lib/hooks/useIssuesProperties";
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// ui
import { CustomMenu } from "ui";
import { CustomMenu } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline";
import { Squares2X2Icon } from "@heroicons/react/20/solid";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { IIssue, NestedKeyOf, Properties } from "types";
import { IIssue, Properties } from "types";
// common
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
// constants
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
type Props = {
groupByProperty: NestedKeyOf<IIssue> | null;
setGroupByProperty: (property: NestedKeyOf<IIssue> | null) => void;
orderBy: NestedKeyOf<IIssue> | null;
setOrderBy: (property: NestedKeyOf<IIssue> | null) => void;
filterIssue: "activeIssue" | "backlogIssue" | null;
setFilterIssue: (property: "activeIssue" | "backlogIssue" | null) => void;
resetFilterToDefault: () => void;
setNewFilterDefaultView: () => void;
issues?: IIssue[];
};
const View: React.FC<Props> = ({
groupByProperty,
setGroupByProperty,
orderBy,
setOrderBy,
filterIssue,
setFilterIssue,
resetFilterToDefault,
setNewFilterDefaultView,
}) => {
const View: React.FC<Props> = ({ issues }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const {
issueView,
setIssueViewToList,
setIssueViewToKanban,
groupByProperty,
setGroupByProperty,
setOrderBy,
setFilterIssue,
orderBy,
filterIssue,
resetFilterToDefault,
setNewFilterDefaultView,
} = useIssueView(issues ?? []);
const [properties, setProperties] = useIssuesProperties(
workspaceSlug as string,
projectId as string
);
return (
<>
<div className="flex items-center gap-2">
<div className="flex items-center gap-x-1">
{issues && (
<>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
issueView === "list" ? "bg-gray-200" : ""
}`}
onClick={() => setIssueViewToList()}
>
<ListBulletIcon className="h-4 w-4" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200 ${
issueView === "kanban" ? "bg-gray-200" : ""
}`}
onClick={() => setIssueViewToKanban()}
>
<Squares2X2Icon className="h-4 w-4" />
</button>
</>
)}
</div>
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={classNames(
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
"group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none"
)}
className={`group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
}`}
>
<span>View</span>
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
@ -71,82 +95,85 @@ const View: React.FC<Props> = ({
>
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
<div className="relative divide-y-2">
<div className="space-y-4 pb-3">
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Group by</h4>
<CustomMenu
label={
groupByOptions.find((option) => option.key === groupByProperty)?.name ??
"Select"
}
width="lg"
>
{groupByOptions.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Order by</h4>
<CustomMenu
label={
orderByOptions.find((option) => option.key === orderBy)?.name ?? "Select"
}
width="lg"
>
{orderByOptions.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
{issues && (
<div className="space-y-4 pb-3">
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Group by</h4>
<CustomMenu
label={
groupByOptions.find((option) => option.key === groupByProperty)?.name ??
"Select"
}
width="lg"
>
{groupByOptions.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setOrderBy(option.key)}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
))}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Order by</h4>
<CustomMenu
label={
orderByOptions.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="lg"
>
{orderByOptions.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setOrderBy(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Issue type</h4>
<CustomMenu
label={
filterIssueOptions.find((option) => option.key === filterIssue)?.name ??
"Select"
}
width="lg"
>
{filterIssueOptions.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setFilterIssue(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
<div className="relative flex justify-end gap-x-3">
<button
type="button"
className="text-xs"
onClick={() => resetFilterToDefault()}
>
Reset to default
</button>
<button
type="button"
className="text-xs font-medium text-theme"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div>
</div>
<div className="flex items-center justify-between">
<h4 className="text-sm text-gray-600">Issue type</h4>
<CustomMenu
label={
filterIssueOptions.find((option) => option.key === filterIssue)?.name ??
"Select"
}
width="lg"
>
{filterIssueOptions.map((option) => (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setFilterIssue(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
<div className="relative flex justify-end gap-x-3">
<button
type="button"
className="text-xs"
onClick={() => resetFilterToDefault()}
>
Reset to default
</button>
<button
type="button"
className="text-xs font-medium text-theme"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div>
</div>
)}
<div className="space-y-2 py-3">
<h4 className="text-sm text-gray-600">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2">
@ -172,7 +199,7 @@ const View: React.FC<Props> = ({
</>
)}
</Popover>
</>
</div>
);
};

View File

@ -0,0 +1,135 @@
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
// components
import { Button, Input, TextArea, CustomSelect } from "components/ui";
// types
import type { ICycle } from "types";
const defaultValues: Partial<ICycle> = {
name: "",
description: "",
status: "draft",
start_date: new Date().toString(),
end_date: new Date().toString(),
};
export interface CycleFormProps {
handleFormSubmit: (values: Partial<ICycle>) => void;
handleFormCancel?: () => void;
initialData?: Partial<ICycle>;
}
export const CycleForm: FC<CycleFormProps> = (props) => {
const { handleFormSubmit, handleFormCancel = () => {}, initialData = null } = props;
// form handler
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
control,
} = useForm<ICycle>({
defaultValues: initialData || defaultValues,
});
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5">
<div className="space-y-3">
<div>
<Input
id="name"
label="Name"
name="name"
type="name"
placeholder="Enter name"
autoComplete="off"
error={errors.name}
register={register}
validations={{
required: "Name is required",
}}
/>
</div>
<div>
<TextArea
id="description"
name="description"
label="Description"
placeholder="Enter description"
error={errors.description}
register={register}
/>
</div>
<div>
<h6 className="text-gray-500">Status</h6>
<Controller
name="status"
control={control}
render={({ field }) => (
<CustomSelect
{...field}
label={<span className="capitalize">{field.value ?? "Select Status"}</span>}
input
>
{[
{ label: "Draft", value: "draft" },
{ label: "Started", value: "started" },
{ label: "Completed", value: "completed" },
].map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex gap-x-2">
<div className="w-full">
<Input
id="start_date"
label="Start Date"
name="start_date"
type="date"
placeholder="Enter start date"
error={errors.start_date}
register={register}
validations={{
required: "Start date is required",
}}
/>
</div>
<div className="w-full">
<Input
id="end_date"
label="End Date"
name="end_date"
type="date"
placeholder="Enter end date"
error={errors.end_date}
register={register}
validations={{
required: "End date is required",
}}
/>
</div>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<Button theme="secondary" onClick={handleFormCancel}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{initialData
? isSubmitting
? "Updating Cycle..."
: "Update Cycle"
: isSubmitting
? "Creating Cycle..."
: "Create Cycle"}
</Button>
</div>
</form>
);
};

View File

@ -0,0 +1,3 @@
export * from "./modal";
export * from "./select";
export * from "./form";

View File

@ -0,0 +1,112 @@
import { Fragment } from "react";
import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
// services
import cycleService from "services/cycles.service";
// components
import { CycleForm } from "components/cycles";
// helpers
import { renderDateFormat } from "helpers/date-time.helper";
// types
import type { ICycle } from "types";
// fetch keys
import { CYCLE_LIST } from "constants/fetch-keys";
export interface CycleModalProps {
isOpen: boolean;
handleClose: () => void;
projectId: string;
workspaceSlug: string;
initialData?: ICycle;
}
export const CycleModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, initialData, projectId, workspaceSlug } = props;
const createCycle = (payload: Partial<ICycle>) => {
cycleService
.createCycle(workspaceSlug as string, projectId, payload)
.then((res) => {
mutate(CYCLE_LIST(projectId));
handleClose();
})
.catch((err) => {
// TODO: Handle this ERROR.
// Object.keys(err).map((key) => {
// setError(key as keyof typeof defaultValues, {
// message: err[key].join(", "),
// });
// });
});
};
const updateCycle = (cycleId: string, payload: Partial<ICycle>) => {
cycleService
.updateCycle(workspaceSlug, projectId, cycleId, payload)
.then((res) => {
mutate(CYCLE_LIST(projectId));
handleClose();
})
.catch((err) => {
// TODO: Handle this ERROR.
// Object.keys(err).map((key) => {
// setError(key as keyof typeof defaultValues, {
// message: err[key].join(", "),
// });
// });
});
};
const handleFormSubmit = (formValues: Partial<ICycle>) => {
if (workspaceSlug && projectId) {
const payload = {
...formValues,
start_date: formValues.start_date ? renderDateFormat(formValues.start_date) : null,
end_date: formValues.end_date ? renderDateFormat(formValues.end_date) : null,
};
if (initialData) {
updateCycle(initialData.id, payload);
} else {
createCycle(payload);
}
}
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
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 overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
{initialData ? "Update" : "Create"} Cycle
</Dialog.Title>
<CycleForm handleFormSubmit={handleFormSubmit} handleFormCancel={handleClose} />
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,131 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import { PlusIcon, ArrowPathIcon } from "@heroicons/react/24/outline";
// services
import cycleServices from "services/cycles.service";
// components
import { CycleModal } from "components/cycles";
// fetch-keys
import { CYCLE_LIST } from "constants/fetch-keys";
export type IssueCycleSelectProps = {
projectId: string;
value: any;
onChange: (value: any) => void;
multiple?: boolean;
};
export const CycleSelect: React.FC<IssueCycleSelectProps> = ({
projectId,
value,
onChange,
multiple = false,
}) => {
// states
const [isCycleModalActive, setCycleModalActive] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: cycles } = useSWR(
workspaceSlug && projectId ? CYCLE_LIST(projectId) : null,
workspaceSlug && projectId
? () => cycleServices.getCycles(workspaceSlug as string, projectId)
: null
);
const options = cycles?.map((cycle) => ({ value: cycle.id, display: cycle.name }));
const openCycleModal = () => {
setCycleModalActive(true);
};
const closeCycleModal = () => {
setCycleModalActive(false);
};
return (
<>
<CycleModal
isOpen={isCycleModalActive}
handleClose={closeCycleModal}
projectId={projectId}
workspaceSlug={workspaceSlug as string}
/>
<Listbox as="div" className="relative" value={value} onChange={onChange} multiple={multiple}>
{({ open }) => (
<>
<Listbox.Button
className={`flex cursor-pointer items-center 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`}
>
<ArrowPathIcon className="h-3 w-3 text-gray-500" />
<div className="flex items-center gap-2 truncate">
{cycles?.find((c) => c.id === value)?.name ?? "Cycles"}
</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className={`absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none`}
>
<div className="py-1">
{options ? (
options.length > 0 ? (
options.map((option) => (
<Listbox.Option
key={option.value}
className={({ selected, active }) =>
`${
selected ||
(Array.isArray(value)
? value.includes(option.value)
: value === option.value)
? "bg-indigo-50 font-medium"
: ""
} ${
active ? "bg-indigo-50" : ""
} relative cursor-pointer select-none p-2 text-gray-900`
}
value={option.value}
>
<span className={` flex items-center gap-2 truncate`}>
{option.display}
</span>
</Listbox.Option>
))
) : (
<p className="text-center text-sm text-gray-500">No options</p>
)
) : (
<p className="text-center text-sm text-gray-500">Loading...</p>
)}
<button
type="button"
className="relative w-full flex select-none items-center gap-x-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900"
onClick={openCycleModal}
>
<PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" />
<span>Create cycle</span>
</button>
</div>
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
</>
);
};

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from "react";
// react beautiful dnd
import { Droppable } from "react-beautiful-dnd";
import type { DroppableProps } from "react-beautiful-dnd";
import { Droppable, DroppableProps } from "react-beautiful-dnd";
const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
const [enabled, setEnabled] = useState(false);

View File

@ -1,16 +1,15 @@
import React, { useEffect, useState, useRef } from "react";
// headless ui
import { Tab, Transition, Popover } from "@headlessui/react";
// hooks
import useOutsideClickDetector from "lib/hooks/useOutsideClickDetector";
// common
import { getRandomEmoji } from "constants/common";
// emoji
// types
import { Props } from "./types";
// emojis
import emojis from "./emojis.json";
// helpers
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
// types
import { Props } from "./types";
import { getRandomEmoji } from "helpers/functions.helper";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const tabOptions = [
{
@ -43,7 +42,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
}, [value, onChange]);
return (
<Popover className="relative" ref={ref}>
<Popover className="relative z-[1]" ref={ref}>
<Popover.Button
className="rounded-md border border-gray-300 p-2 outline-none sm:text-sm"
onClick={() => setIsOpen((prev) => !prev)}

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) => {
return (
export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const AttachmentIcon: React.FC<Props> = ({ width, height, className }) =>
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -24,4 +23,3 @@ export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", clas
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -24,4 +23,3 @@ export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", clas
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -15,4 +14,3 @@ export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
<path d="M10.6002 21C10.4169 21 10.2752 20.9417 10.1752 20.825C10.0752 20.7083 10.0419 20.5583 10.0752 20.375L11.0002 13.95H7.3502C7.16686 13.95 7.03353 13.8667 6.9502 13.7C6.86686 13.5333 6.86686 13.375 6.9502 13.225L12.8752 3.325C12.9252 3.24167 13.0085 3.16667 13.1252 3.1C13.2419 3.03333 13.3585 3 13.4752 3C13.6585 3 13.8002 3.05833 13.9002 3.175C14.0002 3.29167 14.0335 3.44167 14.0002 3.625L13.0752 10.025H16.6752C16.8585 10.025 16.996 10.1083 17.0877 10.275C17.1794 10.4417 17.1835 10.6 17.1002 10.75L11.2002 20.675C11.1502 20.7583 11.0669 20.8333 10.9502 20.9C10.8335 20.9667 10.7169 21 10.6002 21V21Z" />
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24"
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const CancelIcon: React.FC<Props> = ({ width, height, className }) => {
return (
export const CancelIcon: React.FC<Props> = ({ width, height, className }) => (
<svg
width={width}
height={height}
@ -15,4 +14,3 @@ export const CancelIcon: React.FC<Props> = ({ width, height, className }) => {
<path d="M7.725 16.275C7.875 16.425 8.05 16.5 8.25 16.5C8.45 16.5 8.625 16.425 8.775 16.275L12 13.05L15.25 16.3C15.3833 16.4333 15.5542 16.4958 15.7625 16.4875C15.9708 16.4792 16.1417 16.4083 16.275 16.275C16.425 16.125 16.5 15.95 16.5 15.75C16.5 15.55 16.425 15.375 16.275 15.225L13.05 12L16.3 8.75C16.4333 8.61667 16.4958 8.44583 16.4875 8.2375C16.4792 8.02917 16.4083 7.85833 16.275 7.725C16.125 7.575 15.95 7.5 15.75 7.5C15.55 7.5 15.375 7.575 15.225 7.725L12 10.95L8.75 7.7C8.61667 7.56667 8.44583 7.50417 8.2375 7.5125C8.02917 7.52083 7.85833 7.59167 7.725 7.725C7.575 7.875 7.5 8.05 7.5 8.25C7.5 8.45 7.575 8.625 7.725 8.775L10.95 12L7.7 15.25C7.56667 15.3833 7.50417 15.5542 7.5125 15.7625C7.52083 15.9708 7.59167 16.1417 7.725 16.275ZM12 22C10.5833 22 9.26667 21.7458 8.05 21.2375C6.83333 20.7292 5.775 20.025 4.875 19.125C3.975 18.225 3.27083 17.1667 2.7625 15.95C2.25417 14.7333 2 13.4167 2 12C2 10.6 2.25417 9.29167 2.7625 8.075C3.27083 6.85833 3.975 5.8 4.875 4.9C5.775 4 6.83333 3.29167 8.05 2.775C9.26667 2.25833 10.5833 2 12 2C13.4 2 14.7083 2.25833 15.925 2.775C17.1417 3.29167 18.2 4 19.1 4.9C20 5.8 20.7083 6.85833 21.225 8.075C21.7417 9.29167 22 10.6 22 12C22 13.4167 21.7417 14.7333 21.225 15.95C20.7083 17.1667 20 18.225 19.1 19.125C18.2 20.025 17.1417 20.7292 15.925 21.2375C14.7083 21.7458 13.4 22 12 22ZM12 20.5C14.3333 20.5 16.3333 19.6667 18 18C19.6667 16.3333 20.5 14.3333 20.5 12C20.5 9.66667 19.6667 7.66667 18 6C16.3333 4.33333 14.3333 3.5 12 3.5C9.66667 3.5 7.66667 4.33333 6 6C4.33333 7.66667 3.5 9.66667 3.5 12C3.5 14.3333 4.33333 16.3333 6 18C7.66667 19.6667 9.66667 20.5 12 20.5Z" />
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const ClipboardIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const ClipboardIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const ClipboardIcon: React.FC<Props> = ({ width = "24", height = "24", cl
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const CommentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const CommentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -15,4 +14,3 @@ export const CommentIcon: React.FC<Props> = ({ width = "24", height = "24", clas
<path d="M2 16.1V3.05C2 2.81667 2.10833 2.58333 2.325 2.35C2.54167 2.11667 2.76667 2 3 2H15.975C16.225 2 16.4583 2.1125 16.675 2.3375C16.8917 2.5625 17 2.8 17 3.05V11.95C17 12.1833 16.8917 12.4167 16.675 12.65C16.4583 12.8833 16.225 13 15.975 13H6L2.65 16.35C2.53333 16.4667 2.39583 16.4958 2.2375 16.4375C2.07917 16.3792 2 16.2667 2 16.1ZM3.5 3.5V11.5V3.5ZM7.025 18C6.79167 18 6.5625 17.8833 6.3375 17.65C6.1125 17.4167 6 17.1833 6 16.95V14.5H18.5V6H21C21.2333 6 21.4583 6.11667 21.675 6.35C21.8917 6.58333 22 6.825 22 7.075V21.075C22 21.2417 21.9208 21.3542 21.7625 21.4125C21.6042 21.4708 21.4667 21.4417 21.35 21.325L18.025 18H7.025ZM15.5 3.5H3.5V11.5H15.5V3.5Z" />
</svg>
);
};

View File

@ -7,8 +7,7 @@ export const CompletedCycleIcon: React.FC<Props> = ({
height = "24",
className,
color = "black",
}) => {
return (
}) => (
<svg xmlns="http://www.w3.org/2000/svg" height={height} width={width} className={className}>
<path
d="m21.65 36.6-6.9-6.85 2.1-2.1 4.8 4.7 9.2-9.2 2.1 2.15ZM6 44V7h6.25V4h3.25v3h17V4h3.25v3H42v37Zm3-3h30V19.5H9Zm0-24.5h30V10H9Zm0 0V10v6.5Z"
@ -16,4 +15,3 @@ export const CompletedCycleIcon: React.FC<Props> = ({
/>
</svg>
);
};

View File

@ -7,8 +7,7 @@ export const CurrentCycleIcon: React.FC<Props> = ({
height = "24",
className,
color = "black",
}) => {
return (
}) => (
<svg xmlns="http://www.w3.org/2000/svg" height={height} width={width} className={className}>
<path
d="M15.3 28.3q-.85 0-1.425-.575-.575-.575-.575-1.425 0-.85.575-1.425.575-.575 1.425-.575.85 0 1.425.575.575.575.575 1.425 0 .85-.575 1.425-.575.575-1.425.575Zm8.85 0q-.85 0-1.425-.575-.575-.575-.575-1.425 0-.85.575-1.425.575-.575 1.425-.575.85 0 1.425.575.575.575.575 1.425 0 .85-.575 1.425-.575.575-1.425.575Zm8.5 0q-.85 0-1.425-.575-.575-.575-.575-1.425 0-.85.575-1.425.575-.575 1.425-.575.85 0 1.425.575.575.575.575 1.425 0 .85-.575 1.425-.575.575-1.425.575ZM6 44V7h6.25V4h3.25v3h17V4h3.25v3H42v37Zm3-3h30V19.5H9Zm0-24.5h30V10H9Zm0 0V10v6.5Z"
@ -16,4 +15,3 @@ export const CurrentCycleIcon: React.FC<Props> = ({
/>
</svg>
);
};

View File

@ -7,8 +7,7 @@ export const CyclesIcon: React.FC<Props> = ({
height = "24",
className,
color = "black",
}) => {
return (
}) => (
<svg
width={width}
height={height}
@ -33,4 +32,3 @@ export const CyclesIcon: React.FC<Props> = ({
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -12,7 +11,7 @@ export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", clas
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_282_229)">
<g clipPath="url(#clip0_282_229)">
<path d="M16.9312 3.64157C15.6346 3.04643 14.2662 2.62206 12.8604 2.37907C12.8476 2.37657 12.8343 2.37821 12.8225 2.38375C12.8106 2.38929 12.8009 2.39845 12.7946 2.4099C12.6196 2.7224 12.4246 3.1299 12.2879 3.45157C10.7724 3.22139 9.23088 3.22139 7.7154 3.45157C7.5633 3.09515 7.39165 2.7474 7.20123 2.4099C7.19467 2.39871 7.18486 2.38977 7.1731 2.38426C7.16135 2.37876 7.1482 2.37695 7.1354 2.37907C5.72944 2.62155 4.36101 3.04595 3.06457 3.64157C3.05359 3.64617 3.04429 3.65402 3.0379 3.66407C0.444567 7.53823 -0.266266 11.3166 0.0829005 15.0474C0.0837487 15.0567 0.0864772 15.0656 0.0909192 15.0738C0.0953611 15.082 0.101423 15.0892 0.108734 15.0949C1.6184 16.2134 3.30716 17.0672 5.1029 17.6199C5.11556 17.6236 5.12903 17.6233 5.14153 17.6191C5.15403 17.615 5.16497 17.6071 5.1729 17.5966C5.55895 17.072 5.90069 16.5162 6.19457 15.9349C6.19866 15.9269 6.20103 15.9182 6.2015 15.9093C6.20198 15.9003 6.20056 15.8914 6.19733 15.8831C6.1941 15.8747 6.18914 15.8671 6.18278 15.8609C6.17641 15.8546 6.16878 15.8497 6.1604 15.8466C5.62159 15.6404 5.09995 15.3918 4.6004 15.1032C4.59124 15.0979 4.58354 15.0905 4.57797 15.0815C4.5724 15.0725 4.56914 15.0622 4.56848 15.0517C4.56782 15.0411 4.56978 15.0306 4.57418 15.021C4.57859 15.0113 4.58531 15.003 4.59373 14.9966C4.69893 14.9179 4.80229 14.8367 4.90373 14.7532C4.91261 14.746 4.92331 14.7414 4.93464 14.74C4.94597 14.7385 4.95748 14.7402 4.9679 14.7449C8.24123 16.2391 11.7846 16.2391 15.0196 14.7449C15.0301 14.74 15.0418 14.7382 15.0533 14.7397C15.0648 14.7412 15.0756 14.7459 15.0846 14.7532C15.1846 14.8349 15.2896 14.9182 15.3954 14.9966C15.4037 15.0029 15.4104 15.0111 15.4148 15.0205C15.4193 15.03 15.4213 15.0404 15.4208 15.0508C15.4203 15.0612 15.4173 15.0714 15.412 15.0804C15.4067 15.0894 15.3993 15.0969 15.3904 15.1024C14.892 15.3937 14.3699 15.6424 13.8296 15.8457C13.8212 15.849 13.8135 15.8539 13.8071 15.8603C13.8008 15.8666 13.7958 15.8743 13.7926 15.8827C13.7894 15.8911 13.788 15.9001 13.7884 15.9091C13.7889 15.9181 13.7913 15.9269 13.7954 15.9349C14.0954 16.5166 14.4387 17.0699 14.8162 17.5957C14.824 17.6064 14.8349 17.6145 14.8475 17.6186C14.86 17.6228 14.8736 17.623 14.8862 17.6191C16.685 17.0681 18.3765 16.2142 19.8879 15.0941C19.8953 15.0889 19.9014 15.0822 19.906 15.0744C19.9106 15.0667 19.9135 15.058 19.9146 15.0491C20.3312 10.7349 19.2162 6.9874 16.9571 3.66573C16.9518 3.65453 16.9426 3.64564 16.9312 3.64073V3.64157ZM6.68373 12.7749C5.6979 12.7749 4.88623 11.8707 4.88623 10.7591C4.88623 9.64823 5.6829 8.74323 6.68373 8.74323C7.69207 8.74323 8.49707 9.65657 8.48123 10.7599C8.48123 11.8707 7.68457 12.7749 6.68373 12.7749ZM13.3296 12.7749C12.3437 12.7749 11.5321 11.8707 11.5321 10.7591C11.5321 9.64823 12.3279 8.74323 13.3296 8.74323C14.3379 8.74323 15.1429 9.65657 15.1271 10.7599C15.1271 11.8707 14.3387 12.7749 13.3296 12.7749Z" />
</g>
<defs>
@ -22,4 +21,3 @@ export const DiscordIcon: React.FC<Props> = ({ width = "24", height = "24", clas
</defs>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const DocumentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const DocumentIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -15,4 +14,3 @@ export const DocumentIcon: React.FC<Props> = ({ width = "24", height = "24", cla
<path d="M7.27051 14.792H12.7288C12.9094 14.792 13.0587 14.733 13.1768 14.6149C13.2948 14.4969 13.3538 14.3475 13.3538 14.167C13.3538 13.9864 13.2948 13.8371 13.1768 13.7191C13.0587 13.601 12.9094 13.542 12.7288 13.542H7.27051C7.08995 13.542 6.94065 13.601 6.82259 13.7191C6.70454 13.8371 6.64551 13.9864 6.64551 14.167C6.64551 14.3475 6.70454 14.4969 6.82259 14.6149C6.94065 14.733 7.08995 14.792 7.27051 14.792ZM7.27051 11.2503H12.7288C12.9094 11.2503 13.0587 11.1913 13.1768 11.0732C13.2948 10.9552 13.3538 10.8059 13.3538 10.6253C13.3538 10.4448 13.2948 10.2955 13.1768 10.1774C13.0587 10.0594 12.9094 10.0003 12.7288 10.0003H7.27051C7.08995 10.0003 6.94065 10.0594 6.82259 10.1774C6.70454 10.2955 6.64551 10.4448 6.64551 10.6253C6.64551 10.8059 6.70454 10.9552 6.82259 11.0732C6.94065 11.1913 7.08995 11.2503 7.27051 11.2503ZM4.58301 18.3337C4.24967 18.3337 3.95801 18.2087 3.70801 17.9587C3.45801 17.7087 3.33301 17.417 3.33301 17.0837V2.91699C3.33301 2.58366 3.45801 2.29199 3.70801 2.04199C3.95801 1.79199 4.24967 1.66699 4.58301 1.66699H11.583C11.7497 1.66699 11.9129 1.70171 12.0726 1.77116C12.2323 1.8406 12.3677 1.93088 12.4788 2.04199L16.2913 5.85449C16.4025 5.9656 16.4927 6.10102 16.5622 6.26074C16.6316 6.42046 16.6663 6.58366 16.6663 6.75033V17.0837C16.6663 17.417 16.5413 17.7087 16.2913 17.9587C16.0413 18.2087 15.7497 18.3337 15.4163 18.3337H4.58301ZM11.4788 6.16699V2.91699H4.58301V17.0837H15.4163V6.79199H12.1038C11.9233 6.79199 11.774 6.73296 11.6559 6.61491C11.5379 6.49685 11.4788 6.34755 11.4788 6.16699ZM4.58301 2.91699V6.79199V2.91699V17.0837V2.91699Z" />
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const EditIcon: React.FC<Props> = ({ width, height, className }) => {
return (
export const EditIcon: React.FC<Props> = ({ width, height, className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const EditIcon: React.FC<Props> = ({ width, height, className }) => {
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const EllipsisHorizontalIcon: React.FC<Props> = ({ width, height, className }) => {
return (
export const EllipsisHorizontalIcon: React.FC<Props> = ({ width, height, className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const EllipsisHorizontalIcon: React.FC<Props> = ({ width, height, classNa
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -12,10 +11,10 @@ export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", class
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_282_232)">
<g clipPath="url(#clip0_282_232)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M10 0C4.475 0 0 4.475 0 10C0 14.425 2.8625 18.1625 6.8375 19.4875C7.3375 19.575 7.525 19.275 7.525 19.0125C7.525 18.775 7.5125 17.9875 7.5125 17.15C5 17.6125 4.35 16.5375 4.15 15.975C4.0375 15.6875 3.55 14.8 3.125 14.5625C2.775 14.375 2.275 13.9125 3.1125 13.9C3.9 13.8875 4.4625 14.625 4.65 14.925C5.55 16.4375 6.9875 16.0125 7.5625 15.75C7.65 15.1 7.9125 14.6625 8.2 14.4125C5.975 14.1625 3.65 13.3 3.65 9.475C3.65 8.3875 4.0375 7.4875 4.675 6.7875C4.575 6.5375 4.225 5.5125 4.775 4.1375C4.775 4.1375 5.6125 3.875 7.525 5.1625C8.325 4.9375 9.175 4.825 10.025 4.825C10.875 4.825 11.725 4.9375 12.525 5.1625C14.4375 3.8625 15.275 4.1375 15.275 4.1375C15.825 5.5125 15.475 6.5375 15.375 6.7875C16.0125 7.4875 16.4 8.375 16.4 9.475C16.4 13.3125 14.0625 14.1625 11.8375 14.4125C12.2 14.725 12.5125 15.325 12.5125 16.2625C12.5125 17.6 12.5 18.675 12.5 19.0125C12.5 19.275 12.6875 19.5875 13.1875 19.4875C15.1726 18.8173 16.8976 17.5414 18.1197 15.8395C19.3418 14.1375 19.9994 12.0952 20 10C20 4.475 15.525 0 10 0Z"
fill="#858E96"
/>
@ -27,4 +26,3 @@ export const GithubIcon: React.FC<Props> = ({ width = "24", height = "24", class
</defs>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -21,4 +20,3 @@ export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", cl
/>
</svg>
);
};

View File

@ -7,8 +7,7 @@ export const LayerDiagonalIcon: React.FC<Props> = ({
height = "24",
className,
color = "black",
}) => {
return (
}) => (
<svg
width={width}
height={height}
@ -23,4 +22,3 @@ export const LayerDiagonalIcon: React.FC<Props> = ({
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const LockIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const LockIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -15,4 +14,3 @@ export const LockIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
<path d="M6 22C5.58333 22 5.22917 21.8542 4.9375 21.5625C4.64583 21.2708 4.5 20.9167 4.5 20.5V9.65C4.5 9.23333 4.64583 8.87917 4.9375 8.5875C5.22917 8.29583 5.58333 8.15 6 8.15H7.75V5.75C7.75 4.43333 8.2125 3.3125 9.1375 2.3875C10.0625 1.4625 11.1833 1 12.5 1C13.8167 1 14.9375 1.4625 15.8625 2.3875C16.7875 3.3125 17.25 4.43333 17.25 5.75V8.15H19C19.4167 8.15 19.7708 8.29583 20.0625 8.5875C20.3542 8.87917 20.5 9.23333 20.5 9.65V20.5C20.5 20.9167 20.3542 21.2708 20.0625 21.5625C19.7708 21.8542 19.4167 22 19 22H6ZM6 20.5H19V9.65H6V20.5ZM12.5 17C13.0333 17 13.4875 16.8167 13.8625 16.45C14.2375 16.0833 14.425 15.6417 14.425 15.125C14.425 14.625 14.2375 14.1708 13.8625 13.7625C13.4875 13.3542 13.0333 13.15 12.5 13.15C11.9667 13.15 11.5125 13.3542 11.1375 13.7625C10.7625 14.1708 10.575 14.625 10.575 15.125C10.575 15.6417 10.7625 16.0833 11.1375 16.45C11.5125 16.8167 11.9667 17 12.5 17ZM9.25 8.15H15.75V5.75C15.75 4.85 15.4333 4.08333 14.8 3.45C14.1667 2.81667 13.4 2.5 12.5 2.5C11.6 2.5 10.8333 2.81667 10.2 3.45C9.56667 4.08333 9.25 4.85 9.25 5.75V8.15ZM6 20.5V9.65V20.5Z" />
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const MenuIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const MenuIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const MenuIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const PlusIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const PlusIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const PlusIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
/>
</svg>
);
};

View File

@ -6,8 +6,7 @@ export const QuestionMarkCircleIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
}) => {
return (
}) => (
<svg
width={width}
height={height}
@ -19,4 +18,3 @@ export const QuestionMarkCircleIcon: React.FC<Props> = ({
<path d="M12.1 17.825C12.3667 17.825 12.5917 17.7333 12.775 17.55C12.9583 17.3667 13.05 17.1417 13.05 16.875C13.05 16.6083 12.9583 16.3833 12.775 16.2C12.5917 16.0167 12.3667 15.925 12.1 15.925C11.8333 15.925 11.6083 16.0167 11.425 16.2C11.2417 16.3833 11.15 16.6083 11.15 16.875C11.15 17.1417 11.2417 17.3667 11.425 17.55C11.6083 17.7333 11.8333 17.825 12.1 17.825ZM12.075 7.5C12.6417 7.5 13.1 7.65417 13.45 7.9625C13.8 8.27083 13.975 8.66667 13.975 9.15C13.975 9.48333 13.875 9.8125 13.675 10.1375C13.475 10.4625 13.15 10.8167 12.7 11.2C12.2667 11.5833 11.9208 11.9875 11.6625 12.4125C11.4042 12.8375 11.275 13.225 11.275 13.575C11.275 13.7583 11.3458 13.9042 11.4875 14.0125C11.6292 14.1208 11.7917 14.175 11.975 14.175C12.175 14.175 12.3417 14.1083 12.475 13.975C12.6083 13.8417 12.6917 13.675 12.725 13.475C12.775 13.1417 12.8875 12.8458 13.0625 12.5875C13.2375 12.3292 13.5083 12.05 13.875 11.75C14.375 11.3333 14.7375 10.9167 14.9625 10.5C15.1875 10.0833 15.3 9.61667 15.3 9.1C15.3 8.21667 15.0125 7.50833 14.4375 6.975C13.8625 6.44167 13.1 6.175 12.15 6.175C11.5167 6.175 10.9333 6.3 10.4 6.55C9.86667 6.8 9.425 7.16667 9.075 7.65C8.94167 7.83333 8.8875 8.02083 8.9125 8.2125C8.9375 8.40417 9.01667 8.55 9.15 8.65C9.33333 8.78333 9.52917 8.825 9.7375 8.775C9.94583 8.725 10.1167 8.60833 10.25 8.425C10.4667 8.125 10.7292 7.89583 11.0375 7.7375C11.3458 7.57917 11.6917 7.5 12.075 7.5ZM12 22C10.6 22 9.29167 21.7458 8.075 21.2375C6.85833 20.7292 5.8 20.025 4.9 19.125C4 18.225 3.29167 17.1667 2.775 15.95C2.25833 14.7333 2 13.4167 2 12C2 10.6 2.25833 9.29167 2.775 8.075C3.29167 6.85833 4 5.8 4.9 4.9C5.8 4 6.85833 3.29167 8.075 2.775C9.29167 2.25833 10.6 2 12 2C13.3833 2 14.6833 2.25833 15.9 2.775C17.1167 3.29167 18.175 4 19.075 4.9C19.975 5.8 20.6875 6.85833 21.2125 8.075C21.7375 9.29167 22 10.6 22 12C22 13.4167 21.7375 14.7333 21.2125 15.95C20.6875 17.1667 19.975 18.225 19.075 19.125C18.175 20.025 17.1167 20.7292 15.9 21.2375C14.6833 21.7458 13.3833 22 12 22ZM12 20.5C14.35 20.5 16.3542 19.6667 18.0125 18C19.6708 16.3333 20.5 14.3333 20.5 12C20.5 9.66667 19.6708 7.66667 18.0125 6C16.3542 4.33333 14.35 3.5 12 3.5C9.61667 3.5 7.60417 4.33333 5.9625 6C4.32083 7.66667 3.5 9.66667 3.5 12C3.5 14.3333 4.32083 16.3333 5.9625 18C7.60417 19.6667 9.61667 20.5 12 20.5Z" />
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", clas
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const SignalCellularIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const SignalCellularIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const SignalCellularIcon: React.FC<Props> = ({ width = "24", height = "24
/>
</svg>
);
};

View File

@ -7,8 +7,7 @@ export const TagIcon: React.FC<Props> = ({
height = "24",
className,
color = "black",
}) => {
return (
}) => (
<svg
width={width}
height={height}
@ -23,4 +22,3 @@ export const TagIcon: React.FC<Props> = ({
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const TuneIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const TuneIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const TuneIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
/>
</svg>
);
};

View File

@ -7,8 +7,7 @@ export const UpcomingCycleIcon: React.FC<Props> = ({
height = "24",
className,
color = "black",
}) => {
return (
}) => (
<svg xmlns="http://www.w3.org/2000/svg" height={height} width={width} className={className}>
<path
d="M28.3 44v-3H39V19.5H9v11H6V10q0-1.2.9-2.1Q7.8 7 9 7h3.25V4h3.25v3h17V4h3.25v3H39q1.2 0 2.1.9.9.9.9 2.1v31q0 1.2-.9 2.1-.9.9-2.1.9ZM16 47.3l-2.1-2.1 5.65-5.7H2.5v-3h17.05l-5.65-5.7 2.1-2.1 9.3 9.3ZM9 16.5h30V10H9Zm0 0V10v6.5Z"
@ -16,4 +15,3 @@ export const UpcomingCycleIcon: React.FC<Props> = ({
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const UserGroupIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const UserGroupIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const UserGroupIcon: React.FC<Props> = ({ width = "24", height = "24", cl
/>
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const UserCircleIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const UserCircleIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -15,4 +14,3 @@ export const UserCircleIcon: React.FC<Props> = ({ width = "24", height = "24", c
<path d="M5.55 17.625C6.6 16.8917 7.64167 16.3292 8.675 15.9375C9.70833 15.5458 10.8167 15.35 12 15.35C13.1833 15.35 14.2958 15.5458 15.3375 15.9375C16.3792 16.3292 17.425 16.8917 18.475 17.625C19.2083 16.725 19.7292 15.8167 20.0375 14.9C20.3458 13.9833 20.5 13.0167 20.5 12C20.5 9.58333 19.6875 7.5625 18.0625 5.9375C16.4375 4.3125 14.4167 3.5 12 3.5C9.58333 3.5 7.5625 4.3125 5.9375 5.9375C4.3125 7.5625 3.5 9.58333 3.5 12C3.5 13.0167 3.65833 13.9833 3.975 14.9C4.29167 15.8167 4.81667 16.725 5.55 17.625ZM12 12.75C11.0333 12.75 10.2208 12.4208 9.5625 11.7625C8.90417 11.1042 8.575 10.2917 8.575 9.325C8.575 8.35833 8.90417 7.54583 9.5625 6.8875C10.2208 6.22917 11.0333 5.9 12 5.9C12.9667 5.9 13.7792 6.22917 14.4375 6.8875C15.0958 7.54583 15.425 8.35833 15.425 9.325C15.425 10.2917 15.0958 11.1042 14.4375 11.7625C13.7792 12.4208 12.9667 12.75 12 12.75ZM12 22C10.6333 22 9.34167 21.7375 8.125 21.2125C6.90833 20.6875 5.84583 19.9708 4.9375 19.0625C4.02917 18.1542 3.3125 17.0917 2.7875 15.875C2.2625 14.6583 2 13.3667 2 12C2 10.6167 2.2625 9.32083 2.7875 8.1125C3.3125 6.90417 4.02917 5.84583 4.9375 4.9375C5.84583 4.02917 6.90833 3.3125 8.125 2.7875C9.34167 2.2625 10.6333 2 12 2C13.3833 2 14.6792 2.2625 15.8875 2.7875C17.0958 3.3125 18.1542 4.02917 19.0625 4.9375C19.9708 5.84583 20.6875 6.90417 21.2125 8.1125C21.7375 9.32083 22 10.6167 22 12C22 13.3667 21.7375 14.6583 21.2125 15.875C20.6875 17.0917 19.9708 18.1542 19.0625 19.0625C18.1542 19.9708 17.0958 20.6875 15.8875 21.2125C14.6792 21.7375 13.3833 22 12 22ZM12 20.5C12.9167 20.5 13.8125 20.3667 14.6875 20.1C15.5625 19.8333 16.425 19.3667 17.275 18.7C16.425 18.1 15.5583 17.6417 14.675 17.325C13.7917 17.0083 12.9 16.85 12 16.85C11.1 16.85 10.2083 17.0083 9.325 17.325C8.44167 17.6417 7.575 18.1 6.725 18.7C7.575 19.3667 8.4375 19.8333 9.3125 20.1C10.1875 20.3667 11.0833 20.5 12 20.5ZM12 11.25C12.5667 11.25 13.0292 11.0708 13.3875 10.7125C13.7458 10.3542 13.925 9.89167 13.925 9.325C13.925 8.75833 13.7458 8.29583 13.3875 7.9375C13.0292 7.57917 12.5667 7.4 12 7.4C11.4333 7.4 10.9708 7.57917 10.6125 7.9375C10.2542 8.29583 10.075 8.75833 10.075 9.325C10.075 9.89167 10.2542 10.3542 10.6125 10.7125C10.9708 11.0708 11.4333 11.25 12 11.25Z" />
</svg>
);
};

View File

@ -2,8 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const UserIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => {
return (
export const UserIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
@ -18,4 +17,3 @@ export const UserIcon: React.FC<Props> = ({ width = "24", height = "24", classNa
/>
</svg>
);
};

View File

@ -0,0 +1,74 @@
import { FC, useEffect, useState } from "react";
import dynamic from "next/dynamic";
// types
import { IIssue } from "types";
// components
import { Loader, Input } from "components/ui";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader>
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
// hooks
import useDebounce from "hooks/use-debounce";
export interface IssueDescriptionFormValues {
name: string;
description: any;
description_html: string;
}
export interface IssueDetailsProps {
issue: IIssue;
handleSubmit: (value: IssueDescriptionFormValues) => void;
}
export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleSubmit }) => {
// states
// const [issueFormValues, setIssueFormValues] = useState({
// name: issue.name,
// description: issue?.description,
// description_html: issue?.description_html,
// });
const [issueName, setIssueName] = useState(issue?.name);
const [issueDescription, setIssueDescription] = useState(issue?.description);
const [issueDescriptionHTML, setIssueDescriptionHTML] = useState(issue?.description_html);
// hooks
const formValues = useDebounce(
{ name: issueName, description: issueDescription, description_html: issueDescriptionHTML },
2000
);
const stringFromValues = JSON.stringify(formValues);
useEffect(() => {
handleSubmit(formValues);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSubmit, stringFromValues]);
return (
<div>
<Input
id="name"
placeholder="Enter issue name"
name="name"
autoComplete="off"
value={issueName}
onChange={(e) => setIssueName(e.target.value)}
mode="transparent"
className="text-xl font-medium"
required={true}
/>
<RemirrorRichTextEditor
value={issueDescription}
placeholder="Enter Your Text..."
onJSONChange={(json) => setIssueDescription(json)}
onHTMLChange={(html) => setIssueDescriptionHTML(html)}
/>
</div>
);
};

View File

@ -0,0 +1,382 @@
import { ChangeEvent, FC, useState, useEffect } from "react";
import Link from "next/link";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// components
import {
IssueAssigneeSelect,
IssueLabelSelect,
IssueParentSelect,
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
} from "components/issues/select";
import { CycleSelect as IssueCycleSelect } from "components/cycles/select";
import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
// ui
import { Button, CustomMenu, Input, Loader } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
// helpers
import { cosineSimilarity } from "helpers/string.helper";
// types
import type { IIssue } from "types";
// rich-text-editor
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
loading: () => (
<Loader>
<Loader.Item height="12rem" width="100%" />
</Loader>
),
});
const defaultValues: Partial<IIssue> = {
project: "",
name: "",
description: "",
description_html: "<p></p>",
state: "",
cycle: null,
priority: null,
labels_list: [],
};
export interface IssueFormProps {
handleFormSubmit: (values: Partial<IIssue>) => void;
initialData?: Partial<IIssue>;
issues: IIssue[];
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
status: boolean;
}
export const IssueForm: FC<IssueFormProps> = ({
handleFormSubmit,
initialData,
issues = [],
projectId,
setActiveProject,
createMore,
setCreateMore,
handleClose,
status,
}) => {
// states
const [mostSimilarIssue, setMostSimilarIssue] = useState<IIssue | undefined>();
const [cycleModal, setCycleModal] = useState(false);
const [stateModal, setStateModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
control,
setValue,
} = useForm<IIssue>({
defaultValues,
mode: "all",
reValidateMode: "onChange",
});
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7);
setMostSimilarIssue(similarIssue);
};
const handleDiscard = () => {
reset({ ...defaultValues, project: projectId });
handleClose();
};
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
await handleFormSubmit(formData);
reset({
...defaultValues,
project: projectId,
});
};
useEffect(() => {
reset({
...defaultValues,
...watch(),
project: projectId,
...initialData,
});
}, [initialData, reset, watch, projectId]);
return (
<>
{projectId && (
<>
<CreateUpdateStateModal
isOpen={stateModal}
handleClose={() => setStateModal(false)}
projectId={projectId}
/>
<CreateUpdateCycleModal
isOpen={cycleModal}
setIsOpen={setCycleModal}
projectId={projectId}
/>
</>
)}
<form onSubmit={handleSubmit(handleCreateUpdateIssue)}>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
<Controller
control={control}
name="project"
render={({ field: { value, onChange } }) => (
<IssueProjectSelect
value={value}
onChange={onChange}
setActiveProject={setActiveProject}
/>
)}
/>
<h3 className="text-lg font-medium leading-6 text-gray-900">
{status ? "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.find((i) => i.id === watch("parent"))?.state_detail
.color,
}}
/>
<span className="flex-shrink-0 text-gray-600">
{/* {projects?.find((p) => p.id === projectId)?.identifier}- */}
{issues.find((i) => i.id === watch("parent"))?.sequence_id}
</span>
<span className="truncate font-medium">
{issues.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>
<Input
id="name"
label="Title"
name="name"
onChange={handleTitleChange}
className="resize-none"
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={`/${workspaceSlug}/projects/${projectId}/issues/${mostSimilarIssue}`}
>
<a target="_blank" type="button" className="inline text-left">
<span>Did you mean </span>
<span className="italic">
{mostSimilarIssue?.project_detail.identifier}-
{mostSimilarIssue?.sequence_id}: {mostSimilarIssue?.name}{" "}
</span>
?
</a>
</Link>{" "}
</p>
<button
type="button"
className="text-sm text-blue-500"
onClick={() => {
setMostSimilarIssue(undefined);
}}
>
No
</button>
</div>
)}
</div>
<div>
<label htmlFor={"description"} className="mb-2 text-gray-500">
Description
</label>
<Controller
name="description"
control={control}
render={({ field: { value, onChange } }) => (
<RemirrorRichTextEditor
value={value}
onBlur={(jsonValue, htmlValue) => {
setValue("description", jsonValue);
setValue("description_html", htmlValue);
}}
placeholder="Enter Your Text..."
/>
)}
/>
</div>
<div className="flex flex-wrap items-center gap-2">
<Controller
control={control}
name="state"
render={({ field: { value, onChange } }) => (
<IssueStateSelect
setIsOpen={setStateModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
<Controller
control={control}
name="cycle"
render={({ field: { value, onChange } }) => (
<IssueCycleSelect projectId={projectId} value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="assignees_list"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="labels_list"
render={({ field: { value, onChange } }) => (
<IssueLabelSelect value={value} onChange={onChange} projectId={projectId} />
)}
/>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<input
type="date"
value={value ?? ""}
onChange={(e: any) => {
onChange(e.target.value);
}}
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"
/>
)}
/>
<IssueParentSelect
control={control}
isOpen={parentIssueListModalOpen}
setIsOpen={setParentIssueListModalOpen}
issues={issues ?? []}
/>
<CustomMenu ellipsis>
{watch("parent") && watch("parent") !== "" ? (
<>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setValue("parent", null)}
>
Remove parent issue
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Select Parent Issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
</div>
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setCreateMore((prevData) => !prevData)}
>
<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`}
/>
</button>
</div>
<div className="flex items-center gap-2">
<Button theme="secondary" onClick={handleDiscard}>
Discard
</Button>
<Button type="submit" disabled={isSubmitting}>
{status
? isSubmitting
? "Updating Issue..."
: "Update Issue"
: isSubmitting
? "Creating Issue..."
: "Create Issue"}
</Button>
</div>
</div>
</form>
</>
);
};

View File

@ -0,0 +1,5 @@
export * from "./list-item";
export * from "./description-form";
export * from "./sub-issue-list";
export * from "./form";
export * from "./modal";

View File

@ -0,0 +1,144 @@
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
// components
import { AssigneesList } from "components/ui/avatar";
// icons
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, Properties } from "types";
// constants
import { getPriorityIcon } from "constants/global";
type Props = {
type?: string;
issue: IIssue;
properties: Properties;
editIssue?: () => void;
handleDeleteIssue?: () => void;
removeIssue?: () => void;
};
export const IssueListItem: React.FC<Props> = (props) => {
// const { type, issue, properties, editIssue, handleDeleteIssue, removeIssue } = props;
const { issue, properties } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div key={issue.id} className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<span
className={`block h-1.5 w-1.5 flex-shrink-0 rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties?.key && (
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
)}
<span>{issue.name}</span>
</a>
</Link>
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<div
className={`group relative flex flex-shrink-0 cursor-pointer items-center gap-1 rounded px-2 py-1 text-xs 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"
? "bg-orange-100 text-orange-500"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500"
: issue.priority === "low"
? "bg-green-100 text-green-500"
: "bg-gray-100"
}`}
>
{getPriorityIcon(issue.priority)}
<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
className={`capitalize ${
issue.priority === "urgent"
? "text-red-600"
: issue.priority === "high"
? "text-orange-500"
: issue.priority === "medium"
? "text-yellow-500"
: issue.priority === "low"
? "text-green-500"
: ""
}`}
>
{issue.priority ?? "None"}
</div>
</div>
</div>
)}
{properties.state && (
<div className="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">
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue?.state_detail?.color,
}}
/>
{addSpaceIfCamelCase(issue?.state_detail.name)}
<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">State</h5>
<div>{issue?.state_detail.name}</div>
</div>
</div>
)}
{properties.due_date && (
<div
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()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
}`}
>
<CalendarDaysIcon className="h-4 w-4" />
{issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"}
<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">Due date</h5>
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
<div>
{issue.target_date &&
(issue.target_date < new Date().toISOString()
? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days`
: findHowManyDaysLeft(issue.target_date) <= 3
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
: "Due date")}
</div>
</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">
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.assignee && (
<div className="flex items-center gap-1">
<AssigneesList userIds={issue.assignees ?? []} />
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,265 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import projectService from "services/project.service";
import modulesService from "services/modules.service";
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// components
import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
import { IssueForm } from "components/issues";
// common
import { renderDateFormat } from "helpers/date-time.helper";
// types
import type { IIssue, IssueResponse } from "types";
// fetch keys
import {
PROJECT_ISSUES_DETAILS,
PROJECT_ISSUES_LIST,
CYCLE_ISSUES,
USER_ISSUE,
PROJECTS_LIST,
MODULE_ISSUES,
SUB_ISSUES,
} from "constants/fetch-keys";
export interface IssuesModalProps {
isOpen: boolean;
handleClose: () => void;
data?: IIssue | null;
prePopulateData?: Partial<IIssue>;
isUpdatingSingleIssue?: boolean;
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
isOpen,
handleClose,
data,
prePopulateData,
isUpdatingSingleIssue = false,
}) => {
// states
const [createMore, setCreateMore] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUser();
const { setToastAlert } = useToast();
const { data: issues } = useSWR(
workspaceSlug && activeProject
? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "")
: null,
workspaceSlug && activeProject
? () => issuesService.getIssues(workspaceSlug as string, activeProject ?? "")
: null
);
const { data: projects } = useSWR(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null
);
const { setError } = useForm<IIssue>({
mode: "all",
reValidateMode: "onChange",
});
useEffect(() => {
if (projects && projects.length > 0)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [projectId, projects]);
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !projectId) return;
await issuesService
.addIssueToCycle(workspaceSlug as string, activeProject ?? "", cycleId, {
issues: [issueId],
})
.then((res) => {
mutate(CYCLE_ISSUES(cycleId));
if (isUpdatingSingleIssue) {
mutate<IIssue>(
PROJECT_ISSUES_DETAILS,
(prevData) => ({ ...(prevData as IIssue), sprints: cycleId }),
false
);
} else
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""),
(prevData) => ({
...(prevData as IssueResponse),
results: (prevData?.results ?? []).map((issue) => {
if (issue.id === res.id) return { ...issue, sprints: cycleId };
return issue;
}),
}),
false
);
})
.catch((err) => {
console.log(err);
});
};
const addIssueToModule = async (issueId: string, moduleId: string) => {
if (!workspaceSlug || !projectId) return;
await modulesService
.addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, {
issues: [issueId],
})
.then((res) => {
console.log(res);
mutate(MODULE_ISSUES(moduleId as string));
})
.catch((e) => console.log(e));
};
const createIssue = async (payload: Partial<IIssue>) => {
await issuesService
.createIssues(workspaceSlug as string, activeProject ?? "", payload)
.then((res) => {
mutate<IssueResponse>(PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""));
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) handleClose();
setToastAlert({
title: "Success",
type: "success",
message: `Issue ${data ? "updated" : "created"} successfully`,
});
if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE);
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.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) => {
const message = err[key];
if (!message) return;
setError(key as keyof IIssue, {
message: Array.isArray(message) ? message.join(", ") : message,
});
});
});
};
const updateIssue = async (payload: Partial<IIssue>) => {
await issuesService
.updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else
mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""),
(prevData) => ({
...(prevData as IssueResponse),
results: (prevData?.results ?? []).map((issue) => {
if (issue.id === res.id) return { ...issue, ...res };
return issue;
}),
})
);
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (!createMore) handleClose();
setToastAlert({
title: "Success",
type: "success",
message: "Issue updated successfully",
});
})
.catch((err) => {
Object.keys(err).map((key) => {
setError(key as keyof IIssue, { message: err[key].join(", ") });
});
});
};
const handleFormSubmit = async (formData: Partial<IIssue>) => {
if (workspaceSlug && activeProject) {
const payload: Partial<IIssue> = {
...formData,
target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null,
};
if (!data) await createIssue(payload);
else await updateIssue(payload);
}
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
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 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
issues={issues?.results ?? []}
handleFormSubmit={handleFormSubmit}
initialData={prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={handleClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,166 @@
import { useState, FC, Fragment } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Transition, Combobox } from "@headlessui/react";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
// service
import projectServices from "services/project.service";
// types
import type { IProjectMember } from "types";
// fetch keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
export type IssueAssigneeSelectProps = {
projectId: string;
value: string[];
onChange: (value: string[]) => void;
};
type AssigneeAvatarProps = {
user: IProjectMember | undefined;
};
export const AssigneeAvatar: FC<AssigneeAvatarProps> = ({ user }) => {
if (!user) return <></>;
if (user.member.avatar && user.member.avatar !== "") {
return (
<div className="relative h-4 w-4">
<Image
src={user.member.avatar}
alt="avatar"
className="rounded-full"
layout="fill"
objectFit="cover"
/>
</div>
);
} else
return (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{user.member.first_name && user.member.first_name !== ""
? user.member.first_name.charAt(0)
: user.member.email.charAt(0)}
</div>
);
};
export const IssueAssigneeSelect: FC<IssueAssigneeSelectProps> = ({
projectId,
value = [],
onChange,
}) => {
// states
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug } = router.query;
// fetching project members
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options = people?.map((person) => ({
value: person.member.id,
display:
person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email,
}));
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox
as="div"
value={value}
onChange={(val) => onChange(val)}
className="relative flex-shrink-0"
multiple
>
{({ open }: any) => (
<>
<Combobox.Label className="sr-only">Assignees</Combobox.Label>
<Combobox.Button
className={`flex cursor-pointer items-center 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`}
>
<UserIcon className="h-3 w-3 text-gray-500" />
<span
className={`hidden truncate sm:block ${
value === null || value === undefined ? "" : "text-gray-900"
}`}
>
{Array.isArray(value)
? value
.map((v) => options?.find((option) => option.value === v)?.display)
.join(", ") || "Assignees"
: options?.find((option) => option.value === value)?.display || "Assignees"}
</span>
</Combobox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.value}
>
{people && (
<>
<AssigneeAvatar
user={people?.find((p) => p.member.id === option.value)}
/>
{option.display}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-xs text-gray-500 px-2">No assignees found</p>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
);
};

View File

@ -0,0 +1,6 @@
export * from "./assignee";
export * from "./label";
export * from "./parent-issue";
export * from "./priority";
export * from "./project";
export * from "./state";

View File

@ -0,0 +1,206 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Combobox, Transition } from "@headlessui/react";
// icons
import { TagIcon } from "@heroicons/react/24/outline";
// services
import issuesServices from "services/issues.service";
// types
import type { IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = {
value: string[];
onChange: (value: string[]) => void;
projectId: string;
};
const defaultValues: Partial<IIssueLabels> = {
name: "",
};
export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }) => {
// states
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug } = router.query;
const [isOpen, setIsOpen] = useState(false);
const { data: issueLabels, mutate: issueLabelsMutate } = useSWR<IIssueLabels[]>(
projectId ? PROJECT_ISSUE_LABELS(projectId) : null,
workspaceSlug && projectId
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId)
: null
);
const onSubmit = async (data: IIssueLabels) => {
if (!projectId || !workspaceSlug || isSubmitting) return;
await issuesServices
.createIssueLabel(workspaceSlug as string, projectId as string, data)
.then((response) => {
issueLabelsMutate((prevData) => [...(prevData ?? []), response], false);
setIsOpen(false);
reset(defaultValues);
})
.catch((error) => {
console.log(error);
});
};
const {
register,
handleSubmit,
formState: { isSubmitting },
setFocus,
reset,
} = useForm<IIssueLabels>({ defaultValues });
useEffect(() => {
isOpen && setFocus("name");
}, [isOpen, setFocus]);
const options = issueLabels?.map((label) => ({
value: label.id,
display: label.name,
color: label.colour,
}));
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
return (
<>
<Combobox
as="div"
value={value}
onChange={(val) => onChange(val)}
className="relative flex-shrink-0"
multiple
>
{({ open }: any) => (
<>
<Combobox.Label className="sr-only">Labels</Combobox.Label>
<Combobox.Button
className={`flex cursor-pointer items-center 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`}
>
<TagIcon className="h-3 w-3 text-gray-500" />
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
{Array.isArray(value)
? value
.map((v) => options?.find((option) => option.value === v)?.display)
.join(", ") || "Labels"
: options?.find((option) => option.value === value)?.display || "Labels"}
</span>
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.value}
>
{issueLabels && (
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: option.color,
}}
/>
{option.display}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-xs text-gray-500 px-2">No labels found</p>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
{/* <div className="cursor-default select-none p-2 hover:bg-indigo-50 hover:text-gray-900">
{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 place-items-center text-green-600"
disabled={isSubmitting}
onClick={handleSubmit(onSubmit)}
>
<PlusIcon className="h-4 w-4" />
</button>
<button
type="button"
className="grid place-items-center text-red-600"
onClick={() => setIsOpen(false)}
>
<XMarkIcon className="h-4 w-4" aria-hidden="true" />
</button>
</div>
) : (
<button
type="button"
className="flex items-center gap-2 w-full"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-4 w-4 text-gray-400" aria-hidden="true" />
<span className="text-xs whitespace-nowrap">Create label</span>
</button>
)}
</div> */}
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
</>
);
};

View File

@ -0,0 +1,28 @@
import React from "react";
import { Controller, Control } from "react-hook-form";
// components
import IssuesListModal from "components/project/issues/issues-list-modal";
// types
import type { IIssue } from "types";
type Props = {
control: Control<IIssue, any>;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
issues: IIssue[];
};
export const IssueParentSelect: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => (
<Controller
control={control}
name="parent"
render={({ field: { onChange } }) => (
<IssuesListModal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
onChange={onChange}
issues={issues}
/>
)}
/>
);

View File

@ -0,0 +1,54 @@
import React from "react";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// constants
import { getPriorityIcon } from "constants/global";
import { PRIORITIES } from "constants/";
type Props = {
value: string | null;
onChange: (value: string) => void;
};
export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
<Listbox as="div" className="relative" value={value} onChange={onChange}>
{({ open }) => (
<>
<Listbox.Button className="flex cursor-pointer items-center 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">
<span className="text-gray-500 grid place-items-center">{getPriorityIcon(value)}</span>
<div className="flex items-center gap-2 capitalize">{value ?? "Priority"}</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{PRIORITIES.map((priority) => (
<Listbox.Option
key={priority}
className={({ selected, active }) =>
`${selected ? "bg-indigo-50 font-medium" : ""} ${
active ? "bg-indigo-50" : ""
} relative cursor-pointer select-none p-2 text-gray-900`
}
value={priority}
>
<span className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)}
{priority ?? "None"}
</span>
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
);

View File

@ -0,0 +1,104 @@
import { FC, Fragment } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
// services
import projectService from "services/project.service";
// fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys";
export interface IssueProjectSelectProps {
value: string;
onChange: (value: string) => void;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
}
export const IssueProjectSelect: FC<IssueProjectSelectProps> = ({
value,
onChange,
setActiveProject,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// Fetching Projects List
const { data: projects } = useSWR(
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
() => (workspaceSlug ? projectService.getProjects(workspaceSlug as string) : null)
);
return (
<>
<Listbox
value={value}
onChange={(val) => {
onChange(val);
setActiveProject(val);
}}
>
{({ open }) => (
<>
<div className="relative">
<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"}
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<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 ? (
projects.map((project) => (
<Listbox.Option
key={project.id}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} cursor-pointer select-none p-2 text-gray-900`
}
value={project.id}
>
{({ selected }) => (
<>
<span
className={`${
selected ? "font-medium" : "font-normal"
} block truncate`}
>
{project.name}
</span>
</>
)}
</Listbox.Option>
))
) : (
<p className="text-gray-400">No projects found!</p>
)
) : (
<div className="text-sm text-gray-500 px-2">Loading...</div>
)}
</div>
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
</>
);
};

View File

@ -0,0 +1,138 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import stateService from "services/state.service";
// headless ui
import { Squares2X2Icon, PlusIcon } from "@heroicons/react/24/outline";
// icons
import { Combobox, Transition } from "@headlessui/react";
// fetch keys
import { STATE_LIST } from "constants/fetch-keys";
type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
value: string;
onChange: (value: string) => void;
projectId: string;
};
export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => {
// states
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId)
: null
);
const options = states?.map((state) => ({
value: state.id,
display: state.name,
color: state.color,
}));
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox
as="div"
value={value}
onChange={(val: any) => onChange(val)}
className="relative flex-shrink-0"
>
{({ open }: any) => (
<>
<Combobox.Label className="sr-only">State</Combobox.Label>
<Combobox.Button
className={`flex cursor-pointer items-center 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`}
>
<Squares2X2Icon className="h-3 w-3 text-gray-500" />
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
{value && value !== "" ? (
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: options?.find((option) => option.value === value)?.color,
}}
/>
) : null}
{options?.find((option) => option.value === value)?.display || "State"}
</span>
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.value}
>
{states && (
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: option.color,
}}
/>
{option.display}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-xs text-gray-500 px-2">No states found</p>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
<button
type="button"
className="flex select-none w-full items-center gap-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3 w-3 text-gray-400" aria-hidden="true" />
<span className="text-xs whitespace-nowrap">Create state</span>
</button>
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
);
};

View File

@ -0,0 +1,168 @@
import { FC, useState } from "react";
import Link from "next/link";
import { Disclosure, Transition } from "@headlessui/react";
import { ChevronRightIcon, PlusIcon } from "@heroicons/react/24/outline";
// components
import { CustomMenu } from "components/ui";
import { CreateUpdateIssueModal } from "components/issues";
import AddAsSubIssue from "components/project/issues/issue-detail/add-as-sub-issue";
// types
import { IIssue } from "types";
export interface SubIssueListProps {
issues: IIssue[];
projectId: string;
workspaceSlug: string;
parentIssue: IIssue;
handleSubIssueRemove: (subIssueId: string) => void;
}
export const SubIssueList: FC<SubIssueListProps> = ({
issues = [],
handleSubIssueRemove,
parentIssue,
workspaceSlug,
projectId,
}) => {
// states
const [isIssueModalActive, setIssueModalActive] = useState(false);
const [isSubIssueModalActive, setSubIssueModalActive] = useState(false);
const [preloadedData, setPreloadedData] = useState<Partial<IIssue> | null>(null);
const openIssueModal = () => {
setIssueModalActive(true);
};
const closeIssueModal = () => {
setIssueModalActive(false);
};
const openSubIssueModal = () => {
setSubIssueModalActive(true);
};
const closeSubIssueModal = () => {
setSubIssueModalActive(false);
};
return (
<>
<CreateUpdateIssueModal
isOpen={isIssueModalActive}
prePopulateData={{ ...preloadedData }}
handleClose={closeIssueModal}
/>
<AddAsSubIssue
isOpen={isSubIssueModalActive}
setIsOpen={setSubIssueModalActive}
parent={parentIssue}
/>
{parentIssue?.id && workspaceSlug && projectId && issues?.length > 0 ? (
<Disclosure defaultOpen={true}>
{({ open }) => (
<>
<div className="flex items-center justify-between">
<Disclosure.Button className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-gray-100">
<ChevronRightIcon className={`h-3 w-3 ${open ? "rotate-90" : ""}`} />
Sub-issues <span className="ml-1 text-gray-600">{issues.length}</span>
</Disclosure.Button>
{open ? (
<div className="flex items-center">
<button
type="button"
className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-gray-100"
onClick={() => {
openIssueModal();
setPreloadedData({
parent: parentIssue.id,
});
}}
>
<PlusIcon className="h-3 w-3" />
Create new
</button>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => {
setSubIssueModalActive(true);
}}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
) : null}
</div>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className="mt-3 flex flex-col gap-y-1">
{issues.map((issue) => (
<div
key={issue.id}
className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-gray-100"
>
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}>
<a className="flex items-center gap-2 rounded text-xs">
<span
className={`block h-1.5 w-1.5 rounded-full`}
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-gray-600">
{issue.project_detail.identifier}-{issue.sequence_id}
</span>
<span className="max-w-sm break-all font-medium">{issue.name}</span>
</a>
</Link>
<div className="opacity-0 group-hover:opacity-100">
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => handleSubIssueRemove(issue.id)}>
Remove as sub-issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
))}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
) : (
<CustomMenu
label={
<>
<PlusIcon className="h-3 w-3" />
Add sub-issue
</>
}
optionsPosition="left"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
openIssueModal();
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
openSubIssueModal();
}}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
)}
</>
);
};

View File

@ -3,8 +3,7 @@ import Image from "next/image";
// images
import Module from "public/onboarding/module.png";
const BreakIntoModules: React.FC = () => {
return (
const BreakIntoModules: React.FC = () => (
<div className="h-full space-y-4">
<div className="relative h-1/2">
<div
@ -12,7 +11,7 @@ const BreakIntoModules: React.FC = () => {
style={{
background: "linear-gradient(0deg, #fff 84.2%, rgba(255, 255, 255, 0) 34.35%)",
}}
></div>
/>
<Image
src={Module}
className="h-full"
@ -30,6 +29,5 @@ const BreakIntoModules: React.FC = () => {
</div>
</div>
);
};
export default BreakIntoModules;

View File

@ -3,8 +3,7 @@ import Image from "next/image";
// images
import Commands from "public/onboarding/command-menu.png";
const CommandMenu: React.FC = () => {
return (
const CommandMenu: React.FC = () => (
<div className="h-full space-y-4">
<div className="h-1/2 space-y-4">
<h5 className="text-sm text-gray-500">Open the contextual menu with:</h5>
@ -21,6 +20,5 @@ const CommandMenu: React.FC = () => {
</div>
</div>
);
};
export default CommandMenu;

View File

@ -1,10 +1,11 @@
// types
import useToast from "lib/hooks/useToast";
import workspaceService from "lib/services/workspace.service";
import { useForm } from "react-hook-form";
import useToast from "hooks/use-toast";
import workspaceService from "services/workspace.service";
import { IUser } from "types";
import MultiInput from "ui/multi-input";
import OutlineButton from "ui/outline-button";
// ui components
import MultiInput from "components/ui/multi-input";
import OutlineButton from "components/ui/outline-button";
type Props = {
setStep: React.Dispatch<React.SetStateAction<number>>;

View File

@ -3,8 +3,7 @@ import Image from "next/image";
// images
import Cycle from "public/onboarding/cycle.png";
const MoveWithCycles: React.FC = () => {
return (
const MoveWithCycles: React.FC = () => (
<div className="h-full space-y-4">
<div className="relative h-1/2">
<div
@ -12,7 +11,7 @@ const MoveWithCycles: React.FC = () => {
style={{
background: "linear-gradient(0deg, #fff 84.2%, rgba(255, 255, 255, 0) 34.35%)",
}}
></div>
/>
<Image
src={Cycle}
className="h-full"
@ -31,6 +30,5 @@ const MoveWithCycles: React.FC = () => {
</div>
</div>
);
};
export default MoveWithCycles;

View File

@ -3,8 +3,7 @@ import Image from "next/image";
// images
import Issue from "public/onboarding/issue.png";
const PlanWithIssues: React.FC = () => {
return (
const PlanWithIssues: React.FC = () => (
<div className="h-full space-y-4">
<div className="relative h-1/2">
<div
@ -12,7 +11,7 @@ const PlanWithIssues: React.FC = () => {
style={{
background: "linear-gradient(0deg, #fff 84.2%, rgba(255, 255, 255, 0) 34.35%)",
}}
></div>
/>
<Image
src={Issue}
className="h-full"
@ -31,6 +30,5 @@ const PlanWithIssues: React.FC = () => {
</div>
</div>
);
};
export default PlanWithIssues;

View File

@ -3,11 +3,11 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
// hooks
import useToast from "lib/hooks/useToast";
import useToast from "hooks/use-toast";
// services
import userService from "lib/services/user.service";
import userService from "services/user.service";
// ui
import { Input } from "ui";
import { Input } from "components/ui";
// types
import { IUser } from "types";

View File

@ -3,8 +3,7 @@ import Image from "next/image";
// icons
import Logo from "public/logo.png";
const Welcome: React.FC = () => {
return (
const Welcome: React.FC = () => (
<div className="h-full space-y-4">
<div className="h-1/2">
<Image src={Logo} height={100} width={100} alt="Plane Logo" />
@ -18,6 +17,5 @@ const Welcome: React.FC = () => {
</div>
</div>
);
};
export default Welcome;

View File

@ -9,17 +9,17 @@ import { Controller, useForm } from "react-hook-form";
import { Tab } from "@headlessui/react";
// hooks
import useToast from "lib/hooks/useToast";
import useToast from "hooks/use-toast";
// services
import workspaceService from "lib/services/workspace.service";
import workspaceService from "services/workspace.service";
// ui
import { CustomSelect, Input } from "ui";
import { CustomSelect, Input } from "components/ui";
// types
import { IWorkspace, IWorkspaceMemberInvitation } from "types";
// constants
import { companySize } from "constants/";
// fetch-keys
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// types
import { IWorkspace, IWorkspaceMemberInvitation } from "types";
type Props = {
setStep: React.Dispatch<React.SetStateAction<number>>;
@ -83,13 +83,11 @@ const Workspace: React.FC<Props> = ({ setStep, setWorkspace }) => {
action: "accepted" | "withdraw"
) => {
if (action === "accepted") {
setInvitationsRespond((prevData) => {
return [...prevData, workspace_invitation.id];
});
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
} else if (action === "withdraw") {
setInvitationsRespond((prevData) => {
return prevData.filter((item: string) => item !== workspace_invitation.id);
});
setInvitationsRespond((prevData) =>
prevData.filter((item: string) => item !== workspace_invitation.id)
);
}
};

View File

@ -1,20 +1,11 @@
// React
import React, { useState } from "react";
// next
import React from "react";
import Link from "next/link";
import useSWR from "swr";
import { useRouter } from "next/router";
// services
import projectService from "lib/services/project.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Button } from "ui";
// icons
import {
CalendarDaysIcon,
CheckIcon,
MinusIcon,
PencilIcon,
PlusIcon,
TrashIcon,
@ -22,36 +13,26 @@ import {
} from "@heroicons/react/24/outline";
// types
import type { IProject } from "types";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
// common
import { renderShortNumericDateFormat } from "constants/common";
// ui
import { Button } from "components/ui";
// hooks
import useProjectMembers from "hooks/use-project-members";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
type TProjectCardProps = {
export type ProjectCardProps = {
workspaceSlug: string;
project: IProject;
setToJoinProject: (id: string | null) => void;
setDeleteProject: (id: string | null) => void;
};
const ProjectMemberInvitations: React.FC<TProjectCardProps> = (props) => {
export const ProjectCard: React.FC<ProjectCardProps> = (props) => {
const { workspaceSlug, project, setToJoinProject, setDeleteProject } = props;
const { user } = useUser();
// router
const router = useRouter();
const { data: members } = useSWR(PROJECT_MEMBERS(project.id), () =>
projectService.projectMembers(workspaceSlug, project.id)
);
const isMember = members?.some((item: any) => item.member.id === (user as any)?.id);
const canEdit = members?.some(
(item) => (item.member.id === (user as any)?.id && item.role === 20) || item.role === 15
);
const canDelete = members?.some(
(item) => item.member.id === (user as any)?.id && item.role === 20
);
// fetching project members information
const { members, isMember, canDelete, canEdit } = useProjectMembers(workspaceSlug, project.id);
if (!members) {
return (
@ -137,5 +118,3 @@ const ProjectMemberInvitations: React.FC<TProjectCardProps> = (props) => {
</>
);
};
export default ProjectMemberInvitations;

View File

@ -5,17 +5,17 @@ import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
// services
import projectService from "lib/services/project.service";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import type { IProject, IWorkspace } from "types";
import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
// icons
// ui
import { Button, Input } from "components/ui";
// types
// constants
import { PROJECTS_LIST } from "constants/fetch-keys";
// hooks
import useToast from "lib/hooks/useToast";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { Button, Input } from "ui";
// types
import type { IProject, IWorkspace } from "types";
type TConfirmProjectDeletionProps = {
isOpen: boolean;

View File

@ -4,7 +4,7 @@ import { Dialog, Transition } from "@headlessui/react";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { Button } from "ui";
import { Button } from "components/ui";
type Props = {
isOpen: boolean;

View File

@ -8,20 +8,22 @@ import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
// services
import projectServices from "lib/services/project.service";
import workspaceService from "lib/services/workspace.service";
// common
import { getRandomEmoji } from "constants/common";
// constants
import { NETWORK_CHOICES } from "constants/";
// fetch keys
import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
import projectServices from "services/project.service";
import workspaceService from "services/workspace.service";
// hooks
import useToast from "lib/hooks/useToast";
import useToast from "hooks/use-toast";
// ui
import { Button, Input, TextArea, EmojiIconPicker, CustomSelect } from "ui";
import { Button, Input, TextArea, CustomSelect } from "components/ui";
// components
import EmojiIconPicker from "components/emoji-icon-picker";
// helpers
import { getRandomEmoji } from "helpers/functions.helper";
// types
import { IProject } from "types";
// fetch-keys
import { PROJECTS_LIST, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
// constants
import { NETWORK_CHOICES } from "constants/";
type Props = {
isOpen: boolean;
@ -96,9 +98,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
}
}, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]);
useEffect(() => {
return () => setIsChangeIdentifierRequired(true);
}, [isOpen]);
useEffect(() => () => setIsChangeIdentifierRequired(true), [isOpen]);
const handleClose = () => {
setIsOpen(false);

View File

@ -2,25 +2,24 @@ import React, { useCallback } from "react";
// swr
import useSWR, { mutate } from "swr";
// services
import stateService from "lib/services/state.service";
// constants
import { STATE_LIST, CYCLE_ISSUES } from "constants/fetch-keys";
import { useRouter } from "next/router";
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import stateService from "services/state.service";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// components
import SingleBoard from "components/project/cycles/board-view/single-board";
// ui
import { Spinner } from "ui";
import { Spinner } from "components/ui";
// types
import { CycleIssueResponse, IIssue, IProjectMember, NestedKeyOf, Properties } from "types";
import { useRouter } from "next/router";
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import issuesService from "lib/services/issues.service";
import { CycleIssueResponse, IIssue, IProjectMember } from "types";
import issuesService from "services/issues.service";
// constants
import { STATE_LIST, CYCLE_ISSUES } from "constants/fetch-keys";
type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
properties: Properties;
selectedGroup: NestedKeyOf<IIssue> | null;
issues: IIssue[];
members: IProjectMember[] | undefined;
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: () => void;
@ -32,28 +31,27 @@ type Props = {
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| undefined
| null
>
>;
};
const CyclesBoardView: React.FC<Props> = (props) => {
const {
groupedByIssues,
properties,
selectedGroup,
members,
openCreateIssueModal,
openIssuesListModal,
removeIssueFromCycle,
partialUpdateIssue,
handleDeleteIssue,
setPreloadedData,
} = props;
const CyclesBoardView: React.FC<Props> = ({
issues,
members,
openCreateIssueModal,
openIssuesListModal,
removeIssueFromCycle,
partialUpdateIssue,
handleDeleteIssue,
setPreloadedData,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId
@ -125,14 +123,16 @@ const CyclesBoardView: React.FC<Props> = (props) => {
[workspaceSlug, groupedByIssues, projectId, selectedGroup, states, cycleId]
);
if (issueView !== "kanban") return <></>;
return (
<>
{groupedByIssues ? (
<div className="h-full w-full">
<div className="h-screen w-full">
<DragDropContext onDragEnd={handleOnDragEnd}>
<div className="h-full w-full overflow-hidden">
<div className="h-full w-full">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden pb-3">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup) => (
<SingleBoard
key={singleGroup}

View File

@ -1,24 +1,25 @@
// 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";
// services
import workspaceService from "lib/services/workspace.service";
import workspaceService from "services/workspace.service";
// components
import SingleIssue from "components/common/board-view/single-issue";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import BoardHeader from "components/common/board-view/board-header";
// ui
import { CustomMenu } from "ui";
import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
// common
import { addSpaceIfCamelCase } from "constants/common";
import { useRouter } from "next/router";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd";
type Props = {
properties: Properties;
@ -39,7 +40,7 @@ type Props = {
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| undefined
| null
>
>;
stateId: string | null;
@ -60,7 +61,7 @@ const SingleModuleBoard: React.FC<Props> = ({
setPreloadedData,
stateId,
}) => {
// TODO: will use this to collapse/expand the board
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
const router = useRouter();
@ -84,64 +85,25 @@ const SingleModuleBoard: React.FC<Props> = ({
return (
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
<div
className={`flex justify-between p-3 pb-0 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`}
>
<div
className={`flex w-full items-center justify-between ${
!isCollapsed ? "flex-col gap-2" : "gap-1"
}`}
>
<div
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}`,
backgroundColor: `${bgColor}20`,
}}
>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 text-sm text-gray-500">
{groupedByIssues[groupTitle].length}
</span>
</div>
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem
onClick={() => {
openCreateIssueModal();
if (selectedGroup !== null) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => openIssuesListModal()}>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
<BoardHeader
addIssueToState={() => {
openCreateIssueModal();
if (selectedGroup !== null) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}
}}
bgColor={bgColor ?? ""}
createdBy={createdBy}
groupTitle={groupTitle}
groupedByIssues={groupedByIssues}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
selectedGroup={selectedGroup}
/>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
@ -158,11 +120,7 @@ const SingleModuleBoard: React.FC<Props> = ({
]?.map((assignee) => {
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
return tempPerson;
});
return (

View File

@ -5,15 +5,12 @@ import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import cycleService from "lib/services/cycles.service";
// fetch api
import { CYCLE_LIST } from "constants/fetch-keys";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// services
import cycleService from "services/cycles.service";
// ui
import { Button } from "ui";
import { Button } from "components/ui";
// icons
// types
import type { ICycle } from "types";
type TConfirmCycleDeletionProps = {
@ -21,6 +18,8 @@ type TConfirmCycleDeletionProps = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data?: ICycle;
};
// fetch-keys
import { CYCLE_LIST } from "constants/fetch-keys";
const ConfirmCycleDeletion: React.FC<TConfirmCycleDeletionProps> = (props) => {
const { isOpen, setIsOpen, data } = props;
@ -63,7 +62,7 @@ const ConfirmCycleDeletion: React.FC<TConfirmCycleDeletionProps> = (props) => {
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog
as="div"
className="relative z-10"
className="relative z-20"
initialFocus={cancelButtonRef}
onClose={handleClose}
>
@ -79,7 +78,7 @@ const ConfirmCycleDeletion: React.FC<TConfirmCycleDeletionProps> = (props) => {
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}

View File

@ -7,17 +7,17 @@ import { mutate } from "swr";
import { Controller, useForm } from "react-hook-form";
// headless
import { Dialog, Transition } from "@headlessui/react";
// services
import cycleService from "lib/services/cycles.service";
// fetch keys
import { CYCLE_LIST } from "constants/fetch-keys";
// common
import { renderDateFormat } from "constants/common";
// ui
import { Button, Input, TextArea, Select, CustomSelect } from "ui";
// types
import type { ICycle } from "types";
// services
import cycleService from "services/cycles.service";
import { Button, Input, TextArea, CustomSelect } from "components/ui";
// ui
// common
import { renderDateFormat } from "helpers/date-time.helper";
// fetch keys
import { CYCLE_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;

View File

@ -1,22 +1,26 @@
// react
import { useEffect } from "react";
// next
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// hooks
import useToast from "lib/hooks/useToast";
// ui
import { Loader } from "ui";
// icons
import { CalendarDaysIcon, ChartPieIcon, LinkIcon, UserIcon } from "@heroicons/react/24/outline";
// services
import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Loader } from "components/ui";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { groupBy } from "helpers/array.helper";
// types
import { CycleIssueResponse, ICycle } from "types";
// common
import { copyTextToClipboard, groupBy } from "constants/common";
import { mutate } from "swr";
import cyclesService from "lib/services/cycles.service";
import { CYCLE_DETAIL } from "constants/api-routes";
// fetch-keys
import { CYCLE_DETAIL } from "constants/fetch-keys";
type Props = {
cycle: ICycle | undefined;
@ -131,7 +135,7 @@ const CycleDetailSidebar: React.FC<Props> = ({ cycle, isOpen, cycleIssues }) =>
</div>
<div className="flex items-center gap-2 sm:basis-1/2">
<div className="grid flex-shrink-0 place-items-center">
<span className="h-4 w-4 rounded-full border-2 border-gray-300 border-r-blue-500"></span>
<span className="h-4 w-4 rounded-full border-2 border-gray-300 border-r-blue-500" />
</div>
{groupedIssues.completed.length}/{cycleIssues?.length}
</div>
@ -187,19 +191,19 @@ const CycleDetailSidebar: React.FC<Props> = ({ cycle, isOpen, cycleIssues }) =>
</div>
</div>
</div>
<div className="py-1"></div>
<div className="py-1" />
</div>
</>
) : (
<Loader>
<div className="space-y-2">
<Loader.Item height="15px" width="50%"></Loader.Item>
<Loader.Item height="15px" width="30%"></Loader.Item>
<Loader.Item height="15px" width="50%" />
<Loader.Item height="15px" width="30%" />
</div>
<div className="mt-8 space-y-3">
<Loader.Item height="30px"></Loader.Item>
<Loader.Item height="30px"></Loader.Item>
<Loader.Item height="30px"></Loader.Item>
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
)}

View File

@ -4,57 +4,54 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// icons
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
// services
import workspaceService from "lib/services/workspace.service";
// constants
import { addSpaceIfCamelCase } from "constants/common";
import { WORKSPACE_MEMBERS, STATE_LIST } from "constants/fetch-keys";
// services
import stateService from "lib/services/state.service";
import workspaceService from "services/workspace.service";
import stateService from "services/state.service";
// hooks
import useUser from "lib/hooks/useUser";
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// components
import SingleListIssue from "components/common/list-view/single-issue";
// ui
import { CustomMenu, Spinner } from "ui";
// icons
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
import { CustomMenu, Spinner } from "components/ui";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
import { IIssue, IWorkspaceMember } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS, STATE_LIST } from "constants/fetch-keys";
type Props = {
groupedByIssues: {
[key: string]: (IIssue & { bridge?: string })[];
};
properties: Properties;
selectedGroup: NestedKeyOf<IIssue> | null;
issues: IIssue[];
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: () => void;
removeIssueFromCycle: (bridgeId: string) => void;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
setPreloadedData: React.Dispatch<
React.SetStateAction<
| (Partial<IIssue> & {
actionType: "createIssue" | "edit" | "delete";
})
| undefined
| null
>
>;
};
const CyclesListView: React.FC<Props> = ({
groupedByIssues,
selectedGroup,
issues,
openCreateIssueModal,
openIssuesListModal,
properties,
removeIssueFromCycle,
handleDeleteIssue,
setPreloadedData,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId, cycleId } = router.query;
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
const { data: people } = useSWR<IWorkspaceMember[]>(
workspaceSlug ? WORKSPACE_MEMBERS : null,
@ -68,6 +65,8 @@ const CyclesListView: React.FC<Props> = ({
: null
);
if (issueView !== "list") return <></>;
return (
<div className="flex flex-col space-y-5">
{Object.keys(groupedByIssues).map((singleGroup) => {
@ -136,10 +135,10 @@ const CyclesListView: React.FC<Props> = ({
<SingleListIssue
key={issue.id}
type="cycle"
typeId={cycleId as string}
issue={issue}
properties={properties}
editIssue={() => openCreateIssueModal(issue, "edit")}
handleDeleteIssue={() => handleDeleteIssue(issue.id)}
removeIssue={() => removeIssueFromCycle(issue.bridge ?? "")}
/>
);

View File

@ -5,7 +5,7 @@ import SingleStat from "components/project/cycles/stats-view/single-stat";
import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion";
// types
import { ICycle, SelectCycleType } from "types";
import { CompletedCycleIcon, CurrentCycleIcon, UpcomingCycleIcon } from "ui/icons";
import { CompletedCycleIcon, CurrentCycleIcon, UpcomingCycleIcon } from "components/icons";
type TCycleStatsViewProps = {
cycles: ICycle[];

View File

@ -7,20 +7,20 @@ import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// services
import cyclesService from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Button, CustomMenu } from "ui";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { ArrowPathIcon, CheckIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import { ArrowPathIcon, UserIcon } from "@heroicons/react/24/outline";
import cyclesService from "services/cycles.service";
// hooks
// ui
import { Button, CustomMenu } from "components/ui";
// icons
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
import { groupBy } from "helpers/array.helper";
// types
import { CycleIssueResponse, ICycle } from "types";
// fetch-keys
import { CYCLE_ISSUES } from "constants/fetch-keys";
// common
import { groupBy, renderShortNumericDateFormat } from "constants/common";
type TSingleStatProps = {
cycle: ICycle;
@ -126,34 +126,32 @@ const SingleStat: React.FC<TSingleStatProps> = (props) => {
<div className="col-span-2 space-y-3 px-5">
<h4 className="text-sm tracking-widest">PROGRESS</h4>
<div className="space-y-3 text-xs">
{Object.keys(groupedIssues).map((group) => {
return (
<div key={group} className="flex items-center gap-2">
<div className="flex basis-2/3 items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
></span>
<h6 className="text-xs capitalize">{group}</h6>
</div>
<div>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${Math.round(
(groupedIssues[group].length / cycleIssues.length) * 100
)}%`
: "0%"}
</span>
</span>
</div>
{Object.keys(groupedIssues).map((group) => (
<div key={group} className="flex items-center gap-2">
<div className="flex basis-2/3 items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
/>
<h6 className="text-xs capitalize">{group}</h6>
</div>
);
})}
<div>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${Math.round(
(groupedIssues[group].length / cycleIssues.length) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</div>

View File

@ -1 +1,4 @@
export * from "./create-project-modal";
export * from "./sidebar-list";
export * from "./join-project";
export * from "./card";

View File

@ -4,42 +4,35 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext } from "react-beautiful-dnd";
// react-beautiful-dnd
import { DragDropContext, DropResult } from "react-beautiful-dnd";
// hook
import useIssuesProperties from "lib/hooks/useIssuesProperties";
import useIssuesProperties from "hooks/use-issue-properties";
import useIssueView from "hooks/use-issue-view";
// services
import stateServices from "lib/services/state.service";
import issuesServices from "lib/services/issues.service";
import projectService from "lib/services/project.service";
// fetching keys
import { STATE_LIST, PROJECT_ISSUES_LIST, PROJECT_MEMBERS } from "constants/fetch-keys";
import stateServices from "services/state.service";
import issuesServices from "services/issues.service";
import projectService from "services/project.service";
// 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 { CreateUpdateIssueModal } from "components/issues/modal";
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
// ui
import { Spinner } from "ui";
import { Spinner } from "components/ui";
// types
import type { IState, IIssue, NestedKeyOf, IssueResponse } from "types";
import type { IState, IIssue, IssueResponse } from "types";
// fetch-keys
import { STATE_LIST, PROJECT_ISSUES_LIST, PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
groupedByIssues: {
[key: string]: IIssue[];
};
issues: IIssue[];
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
};
const BoardView: React.FC<Props> = ({
selectedGroup,
groupedByIssues,
handleDeleteIssue,
partialUpdateIssue,
}) => {
const [isIssueOpen, setIsIssueOpen] = useState(false);
const BoardView: React.FC<Props> = ({ issues, handleDeleteIssue, partialUpdateIssue }) => {
const [createIssueModal, setCreateIssueModal] = useState(false);
const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false);
const [issueDeletionData, setIssueDeletionData] = useState<IIssue | undefined>();
const [preloadedData, setPreloadedData] = useState<
@ -49,6 +42,8 @@ const BoardView: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
const { data: states, mutate: mutateState } = useSWR<IState[]>(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug
@ -75,10 +70,9 @@ const BoardView: React.FC<Props> = ({
(result: DropResult) => {
if (!result.destination) return;
const { source, destination, type } = result;
const draggedItem = groupedByIssues[source.droppableId][source.index];
if (destination.droppableId === "trashBox") {
setIssueDeletionData(draggedItem);
// setIssueDeletionData(draggedItem);
setIsIssueDeletionOpen(true);
} else {
if (type === "state") {
@ -117,6 +111,7 @@ const BoardView: React.FC<Props> = ({
console.error(err);
});
} else {
const draggedItem = groupedByIssues[source.droppableId][source.index];
if (source.droppableId !== destination.droppableId) {
const sourceGroup = source.droppableId; // source group id
const destinationGroup = destination.droppableId; // destination group id
@ -188,6 +183,8 @@ const BoardView: React.FC<Props> = ({
[workspaceSlug, mutateState, groupedByIssues, projectId, selectedGroup, states]
);
if (issueView !== "kanban") return <></>;
return (
<>
<ConfirmIssueDeletion
@ -195,16 +192,15 @@ const BoardView: React.FC<Props> = ({
handleClose={() => setIsIssueDeletionOpen(false)}
data={issueDeletionData}
/>
<CreateUpdateIssuesModal
isOpen={isIssueOpen && preloadedData?.actionType === "createIssue"}
setIsOpen={setIsIssueOpen}
<CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
handleClose={() => setCreateIssueModal(false)}
prePopulateData={{
...preloadedData,
}}
projectId={projectId as string}
/>
{groupedByIssues ? (
<div className="h-full w-full">
<div className="h-screen w-full">
<DragDropContext onDragEnd={handleOnDragEnd}>
<div className="h-full w-full overflow-hidden">
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
@ -214,7 +210,7 @@ const BoardView: React.FC<Props> = ({
{...provided.droppableProps}
ref={provided.innerRef}
>
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden pb-3">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => (
<SingleBoard
key={singleGroup}
@ -228,7 +224,7 @@ const BoardView: React.FC<Props> = ({
}
groupedByIssues={groupedByIssues}
index={index}
setIsIssueOpen={setIsIssueOpen}
setIsIssueOpen={setCreateIssueModal}
properties={properties}
setPreloadedData={setPreloadedData}
stateId={

View File

@ -4,22 +4,20 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd";
// components
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import workspaceService from "lib/services/workspace.service";
import SingleIssue from "components/common/board-view/single-issue";
import BoardHeader from "components/common/board-view/board-header";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
import { addSpaceIfCamelCase } from "constants/common";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { PlusIcon } from "@heroicons/react/24/outline";
// services
import workspaceService from "services/workspace.service";
// types
import { IIssue, Properties, NestedKeyOf, IWorkspaceMember } from "types";
import SingleIssue from "components/common/board-view/single-issue";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
@ -79,6 +77,16 @@ const SingleBoard: React.FC<Props> = ({
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const addIssueToState = () => {
setIsIssueOpen(true);
if (selectedGroup)
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
};
return (
<Draggable draggableId={groupTitle} index={index}>
{(provided, snapshot) => (
@ -92,80 +100,17 @@ const SingleBoard: React.FC<Props> = ({
<div
className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}
>
<div
className={`flex justify-between p-3 pb-0 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<button
type="button"
{...provided.dragHandleProps}
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="mt-[-0.7rem] h-4 w-4 text-gray-600" />
</button>
<div
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}`,
backgroundColor: `${bgColor}20`,
}}
>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
>
{groupTitle === null || groupTitle === "null"
? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 text-sm text-gray-500">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
>
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</button>
<button
type="button"
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)
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}}
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
</div>
<BoardHeader
addIssueToState={addIssueToState}
bgColor={bgColor}
createdBy={createdBy}
groupTitle={groupTitle}
groupedByIssues={groupedByIssues}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
selectedGroup={selectedGroup}
provided={provided}
/>
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
@ -182,11 +127,7 @@ const SingleBoard: React.FC<Props> = ({
]?.map((assignee) => {
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
return {
avatar: tempPerson?.avatar,
first_name: tempPerson?.first_name,
email: tempPerson?.email,
};
return tempPerson;
});
return (
@ -215,16 +156,7 @@ const SingleBoard: React.FC<Props> = ({
<button
type="button"
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) {
setPreloadedData({
state: stateId !== null ? stateId : undefined,
[selectedGroup]: groupTitle,
actionType: "createIssue",
});
}
}}
onClick={addIssueToState}
>
<PlusIcon className="mr-1 h-3 w-3" />
Create

View File

@ -4,21 +4,22 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// headless ui
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, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// common
import { groupBy } from "constants/common";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { Button } from "ui";
// types
import type { IState } from "types";
// services
import stateServices from "services/state.service";
import issuesServices from "services/issues.service";
// ui
import { Button } from "components/ui";
// helpers
import { groupBy } from "helpers/array.helper";
// fetch api
import { STATE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
onClose: () => void;

Some files were not shown because too many files have changed in this diff Show More