forked from github/plane
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:
parent
9134b0c543
commit
9075f9441c
10
.eslintrc.js
Normal file
10
.eslintrc.js
Normal 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/*/"],
|
||||
},
|
||||
},
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
1
apps/app/.eslintrc.js
Normal file
1
apps/app/.eslintrc.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require("config/.eslintrc");
|
@ -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
|
||||
|
@ -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;
|
@ -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;
|
46
apps/app/components/account/email-signin-form.tsx
Normal file
46
apps/app/components/account/email-signin-form.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
51
apps/app/components/account/github-login-button.tsx
Normal file
51
apps/app/components/account/github-login-button.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
5
apps/app/components/account/index.ts
Normal file
5
apps/app/components/account/index.ts
Normal 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";
|
@ -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,8 +32,7 @@ type BreadcrumbItemProps = {
|
||||
icon?: any;
|
||||
};
|
||||
|
||||
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => {
|
||||
return (
|
||||
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => (
|
||||
<>
|
||||
{link ? (
|
||||
<Link href={link}>
|
||||
@ -54,7 +53,6 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) =>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
109
apps/app/components/common/board-view/board-header.tsx
Normal file
109
apps/app/components/common/board-view/board-header.tsx
Normal 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;
|
@ -1,5 +1,3 @@
|
||||
const SingleBoard = () => {
|
||||
return <></>;
|
||||
};
|
||||
const SingleBoard = () => <></>;
|
||||
|
||||
export default SingleBoard;
|
||||
|
@ -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"
|
||||
|
@ -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,8 +102,7 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
});
|
||||
mutate<IssueResponse>(
|
||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
||||
(prevData) => {
|
||||
return {
|
||||
(prevData) => ({
|
||||
...(prevData as IssueResponse),
|
||||
count: (prevData?.results ?? []).filter(
|
||||
(p) => !data.issue_ids.some((id) => p.id === id)
|
||||
@ -108,8 +110,7 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
results: (prevData?.results ?? []).filter(
|
||||
(p) => !data.issue_ids.some((id) => p.id === id)
|
||||
),
|
||||
};
|
||||
},
|
||||
}),
|
||||
false
|
||||
);
|
||||
})
|
||||
@ -120,9 +121,8 @@ 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}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -135,7 +135,7 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<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"
|
||||
@ -182,10 +182,9 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
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-900 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -242,8 +241,13 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
||||
Delete selected issues
|
||||
<Button
|
||||
onClick={handleSubmit(handleDelete)}
|
||||
theme="danger"
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Deleting..." : "Delete selected issues"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -253,7 +257,6 @@ const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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,18 +146,16 @@ const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{filteredIssues.map((issue) => {
|
||||
return (
|
||||
{filteredIssues.map((issue) => (
|
||||
<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",
|
||||
`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 }) => (
|
||||
@ -179,8 +174,7 @@ const ExistingIssuesListModal: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
|
@ -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,8 +114,19 @@ 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 ${
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data });
|
||||
}}
|
||||
className="group relative flex-shrink-0"
|
||||
>
|
||||
{({ 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"
|
||||
@ -87,8 +138,37 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
: "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(issue.priority ?? "")} */}
|
||||
{issue.priority ?? "None"}
|
||||
{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
|
||||
@ -107,22 +187,44 @@ const SingleListIssue: React.FC<Props> = ({
|
||||
{issue.priority ?? "None"}
|
||||
</div>
|
||||
</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">
|
||||
<CustomSelect
|
||||
label={
|
||||
<>
|
||||
<span
|
||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue?.state_detail?.color,
|
||||
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>
|
||||
/>
|
||||
{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()}>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>Edit</CustomMenu.MenuItem>
|
||||
{type !== "issue" && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<>Remove from {type}</>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue()}>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteIssue(issue)}>
|
||||
Delete permanently
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
|
@ -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;
|
||||
|
@ -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,6 +95,7 @@ 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">
|
||||
{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>
|
||||
@ -95,7 +120,8 @@ const View: React.FC<Props> = ({
|
||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ?? "Select"
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
@ -147,6 +173,7 @@ const View: React.FC<Props> = ({
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
135
apps/app/components/cycles/form.tsx
Normal file
135
apps/app/components/cycles/form.tsx
Normal 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>
|
||||
);
|
||||
};
|
3
apps/app/components/cycles/index.ts
Normal file
3
apps/app/components/cycles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./modal";
|
||||
export * from "./select";
|
||||
export * from "./form";
|
112
apps/app/components/cycles/modal.tsx
Normal file
112
apps/app/components/cycles/modal.tsx
Normal 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>
|
||||
);
|
||||
};
|
131
apps/app/components/cycles/select.tsx
Normal file
131
apps/app/components/cycles/select.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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);
|
||||
|
@ -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)}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
74
apps/app/components/issues/description-form.tsx
Normal file
74
apps/app/components/issues/description-form.tsx
Normal 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>
|
||||
);
|
||||
};
|
382
apps/app/components/issues/form.tsx
Normal file
382
apps/app/components/issues/form.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
5
apps/app/components/issues/index.ts
Normal file
5
apps/app/components/issues/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./list-item";
|
||||
export * from "./description-form";
|
||||
export * from "./sub-issue-list";
|
||||
export * from "./form";
|
||||
export * from "./modal";
|
144
apps/app/components/issues/list-item.tsx
Normal file
144
apps/app/components/issues/list-item.tsx
Normal 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>
|
||||
);
|
||||
};
|
265
apps/app/components/issues/modal.tsx
Normal file
265
apps/app/components/issues/modal.tsx
Normal 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>
|
||||
);
|
||||
};
|
166
apps/app/components/issues/select/assignee.tsx
Normal file
166
apps/app/components/issues/select/assignee.tsx
Normal 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>
|
||||
);
|
||||
};
|
6
apps/app/components/issues/select/index.ts
Normal file
6
apps/app/components/issues/select/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./assignee";
|
||||
export * from "./label";
|
||||
export * from "./parent-issue";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
export * from "./state";
|
206
apps/app/components/issues/select/label.tsx
Normal file
206
apps/app/components/issues/select/label.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
28
apps/app/components/issues/select/parent-issue.tsx
Normal file
28
apps/app/components/issues/select/parent-issue.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
54
apps/app/components/issues/select/priority.tsx
Normal file
54
apps/app/components/issues/select/priority.tsx
Normal 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>
|
||||
);
|
104
apps/app/components/issues/select/project.tsx
Normal file
104
apps/app/components/issues/select/project.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
138
apps/app/components/issues/select/state.tsx
Normal file
138
apps/app/components/issues/select/state.tsx
Normal 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>
|
||||
);
|
||||
};
|
168
apps/app/components/issues/sub-issue-list.tsx
Normal file
168
apps/app/components/issues/sub-issue-list.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>>;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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,16 +31,13 @@ type Props = {
|
||||
| (Partial<IIssue> & {
|
||||
actionType: "createIssue" | "edit" | "delete";
|
||||
})
|
||||
| undefined
|
||||
| null
|
||||
>
|
||||
>;
|
||||
};
|
||||
|
||||
const CyclesBoardView: React.FC<Props> = (props) => {
|
||||
const {
|
||||
groupedByIssues,
|
||||
properties,
|
||||
selectedGroup,
|
||||
const CyclesBoardView: React.FC<Props> = ({
|
||||
issues,
|
||||
members,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
@ -49,11 +45,13 @@ const CyclesBoardView: React.FC<Props> = (props) => {
|
||||
partialUpdateIssue,
|
||||
handleDeleteIssue,
|
||||
setPreloadedData,
|
||||
} = props;
|
||||
|
||||
}) => {
|
||||
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}
|
||||
|
@ -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,45 +85,8 @@ 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={() => {
|
||||
<BoardHeader
|
||||
addIssueToState={() => {
|
||||
openCreateIssueModal();
|
||||
if (selectedGroup !== null) {
|
||||
setPreloadedData({
|
||||
@ -132,16 +96,14 @@ const SingleModuleBoard: React.FC<Props> = ({
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => openIssuesListModal()}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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 (
|
||||
|
@ -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}
|
||||
|
@ -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>>;
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 ?? "")}
|
||||
/>
|
||||
);
|
||||
|
@ -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[];
|
||||
|
@ -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,8 +126,7 @@ 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 (
|
||||
{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
|
||||
@ -135,7 +134,7 @@ const SingleStat: React.FC<TSingleStatProps> = (props) => {
|
||||
style={{
|
||||
backgroundColor: stateGroupColours[group],
|
||||
}}
|
||||
></span>
|
||||
/>
|
||||
<h6 className="text-xs capitalize">{group}</h6>
|
||||
</div>
|
||||
<div>
|
||||
@ -152,8 +151,7 @@ const SingleStat: React.FC<TSingleStatProps> = (props) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1 +1,4 @@
|
||||
export * from "./create-project-modal";
|
||||
export * from "./sidebar-list";
|
||||
export * from "./join-project";
|
||||
export * from "./card";
|
||||
|
@ -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={
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user