forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into chore/api_endpoints
This commit is contained in:
commit
2e513af4f3
@ -155,6 +155,16 @@ class ChangePasswordSerializer(serializers.Serializer):
|
||||
"""
|
||||
old_password = serializers.CharField(required=True)
|
||||
new_password = serializers.CharField(required=True)
|
||||
confirm_password = serializers.CharField(required=True)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get("old_password") == data.get("new_password"):
|
||||
raise serializers.ValidationError("New password cannot be same as old password.")
|
||||
|
||||
if data.get("new_password") != data.get("confirm_password"):
|
||||
raise serializers.ValidationError("confirm password should be same as the new password.")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ResetPasswordSerializer(serializers.Serializer):
|
||||
|
@ -131,21 +131,13 @@ class ChangePasswordEndpoint(BaseAPIView):
|
||||
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
if serializer.is_valid():
|
||||
# Check old password
|
||||
if not user.object.check_password(serializer.data.get("old_password")):
|
||||
if not user.check_password(serializer.data.get("old_password")):
|
||||
return Response(
|
||||
{"old_password": ["Wrong password."]},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# set_password also hashes the password that the user will get
|
||||
self.object.set_password(serializer.data.get("new_password"))
|
||||
self.object.save()
|
||||
response = {
|
||||
"status": "success",
|
||||
"code": status.HTTP_200_OK,
|
||||
"message": "Password updated successfully",
|
||||
}
|
||||
|
||||
return Response(response)
|
||||
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.save()
|
||||
return Response({"message": "Password updated successfully"}, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
@ -36,6 +36,8 @@ module.exports = {
|
||||
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||
"onbording-shadow-sm": "var(--color-onboarding-shadow-sm)",
|
||||
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
@ -192,6 +194,7 @@ module.exports = {
|
||||
border: {
|
||||
100: convertToRGB("--color-onboarding-border-100"),
|
||||
200: convertToRGB("--color-onboarding-border-200"),
|
||||
300: convertToRGB("--color-onboarding-border-300"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -372,8 +375,9 @@ module.exports = {
|
||||
96: "21.6rem",
|
||||
},
|
||||
backgroundImage: {
|
||||
"onboarding-gradient-primary": "var( --gradient-onboarding-primary)",
|
||||
"onboarding-gradient-secondary": "var( --gradient-onboarding-secondary)",
|
||||
"onboarding-gradient-100": "var( --gradient-onboarding-100)",
|
||||
"onboarding-gradient-200": "var( --gradient-onboarding-200)",
|
||||
"onboarding-gradient-300": "var( --gradient-onboarding-300)",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
|
@ -17,7 +17,7 @@ export const Spinner: React.FC<ISpinner> = ({
|
||||
aria-hidden="true"
|
||||
height={height}
|
||||
width={width}
|
||||
className={`mr-2 animate-spin fill-blue-600 text-custom-text-200 ${className}`}
|
||||
className={`animate-spin fill-blue-600 text-custom-text-200 ${className}`}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -1,18 +1,17 @@
|
||||
// react
|
||||
import React, { useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
import { mutate } from "swr";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { UserService } from "services/user.service";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -20,19 +19,40 @@ type Props = {
|
||||
};
|
||||
|
||||
const authService = new AuthService();
|
||||
const userService = new UserService();
|
||||
|
||||
const DeleteAccountModal: React.FC<Props> = (props) => {
|
||||
export const DeactivateAccountModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
// states
|
||||
const [switchingAccount, setSwitchingAccount] = useState(false);
|
||||
const [isDeactivating, setIsDeactivating] = useState(false);
|
||||
|
||||
const {
|
||||
user: { deactivateAccount },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
const handleClose = () => {
|
||||
setSwitchingAccount(false);
|
||||
setIsDeactivating(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSwitchAccount = async () => {
|
||||
setSwitchingAccount(true);
|
||||
|
||||
await authService
|
||||
.signOut()
|
||||
.then(() => {
|
||||
mutate("CURRENT_USER_DETAILS", null);
|
||||
setTheme("system");
|
||||
router.push("/");
|
||||
handleClose();
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
@ -40,33 +60,31 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
})
|
||||
);
|
||||
)
|
||||
.finally(() => setSwitchingAccount(false));
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
await userService
|
||||
.deleteAccount()
|
||||
setIsDeactivating(true);
|
||||
|
||||
await deactivateAccount()
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Account deleted successfully.",
|
||||
});
|
||||
handleClose();
|
||||
router.push("/");
|
||||
})
|
||||
.catch((err) =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.data?.error,
|
||||
message: err?.error,
|
||||
})
|
||||
);
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
)
|
||||
.finally(() => setIsDeactivating(false));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -99,32 +117,29 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
|
||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
|
||||
Not the right workspace?
|
||||
Deactivate account?
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 px-4">
|
||||
<ul className="text-onboarding-text-300 list-disc font-normal text-base">
|
||||
<li>Delete this account if you have another and won’t use this account.</li>
|
||||
<li>Switch to another account if you’d like to come back to this account another time.</li>
|
||||
<li>Deactivate this account if you have another and won{"'"}t use this account.</li>
|
||||
<li>Switch to another account if you{"'"}d like to come back to this account another time.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4 p-4 mb-2 sm:px-6">
|
||||
<span className="text-sm font-medium hover:cursor-pointer" onClick={handleSignOut}>
|
||||
Switch account
|
||||
</span>
|
||||
<button
|
||||
className="py-1.5 px-3 font-medium rounded-sm text-red-500 border border-red-500 text-sm "
|
||||
onClick={handleDeleteAccount}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete account"}
|
||||
</button>
|
||||
<div className="flex items-center justify-end gap-2 p-4 mb-2 sm:px-6">
|
||||
<Button variant="link-primary" onClick={handleSwitchAccount} loading={switchingAccount}>
|
||||
{switchingAccount ? "Switching..." : "Switch account"}
|
||||
</Button>
|
||||
<Button variant="outline-danger" onClick={handleDeleteAccount}>
|
||||
{isDeactivating ? "Deactivating..." : "Deactivate account"}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
@ -134,5 +149,3 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAccountModal;
|
@ -1,17 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { XCircle } from "lucide-react";
|
||||
// ui
|
||||
import { Button, Input } from "@plane/ui";
|
||||
// components
|
||||
import { AuthType } from "components/page-views";
|
||||
// services
|
||||
import { AuthService } from "services/auth.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useTimer from "hooks/use-timer";
|
||||
// icons
|
||||
import { XCircle } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// types
|
||||
type EmailCodeFormValues = {
|
||||
email: string;
|
||||
key?: string;
|
||||
@ -20,7 +19,14 @@ type EmailCodeFormValues = {
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
type Props = {
|
||||
handleSignIn: any;
|
||||
authType: AuthType;
|
||||
};
|
||||
|
||||
export const EmailCodeForm: React.FC<Props> = (Props) => {
|
||||
const { handleSignIn, authType } = Props;
|
||||
// states
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [codeResent, setCodeResent] = useState(false);
|
||||
const [isCodeResending, setIsCodeResending] = useState(false);
|
||||
@ -37,7 +43,6 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
setError,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailCodeFormValues>({
|
||||
defaultValues: {
|
||||
@ -49,14 +54,13 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode;
|
||||
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting;
|
||||
|
||||
const onSubmit = async ({ email }: EmailCodeFormValues) => {
|
||||
setErrorResendingCode(false);
|
||||
await authService
|
||||
.emailCode({ email })
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
setSentEmail(email);
|
||||
setValue("key", res.key);
|
||||
setCodeSent(true);
|
||||
@ -139,12 +143,20 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-onboarding-text-100">
|
||||
Let’s get you prepped!
|
||||
{authType === "sign-in" ? "Get on your flight deck!" : "Let’s get you prepped!"}
|
||||
</h1>
|
||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
||||
This whole thing will take less than two minutes.
|
||||
</p>
|
||||
<p className="text-center text-sm text-onboarding-text-200 mt-1">Promise!</p>
|
||||
{authType == "sign-up" ? (
|
||||
<div>
|
||||
<p className="text-center text-sm text-onboarding-text-200 mt-3">
|
||||
This whole thing will take less than two minutes.
|
||||
</p>
|
||||
<p className="text-center text-sm text-onboarding-text-200 mt-1">Promise!</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-sm text-onboarding-text-200 px-20 mt-3">
|
||||
Sign in with the email you used to sign up for Plane
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -216,11 +228,39 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.token)}
|
||||
placeholder="get-set-fly"
|
||||
placeholder="gets-sets-flys"
|
||||
className="border-onboarding-border-100 h-[46px] w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{resendCodeTimer <= 0 && !isResendDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex absolute w-fit right-3.5 justify-end text-xs outline-none cursor-pointer text-custom-primary-100`}
|
||||
onClick={() => {
|
||||
setIsCodeResending(true);
|
||||
onSubmit({ email: getValues("email") }).then(() => {
|
||||
setCodeResent(true);
|
||||
setIsCodeResending(false);
|
||||
setResendCodeTimer(30);
|
||||
});
|
||||
}}
|
||||
disabled={isResendDisabled}
|
||||
>
|
||||
<span className="font-medium">Resend</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`flex w-full justify-end text-xs outline-none ${
|
||||
isResendDisabled ? "cursor-default text-custom-text-200" : "cursor-pointer text-custom-primary-100"
|
||||
} `}
|
||||
>
|
||||
{resendCodeTimer > 0 ? (
|
||||
<span className="text-right">Request new code in {resendCodeTimer}s</span>
|
||||
) : isCodeResending ? (
|
||||
"Sending new code..."
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -238,8 +278,8 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
>
|
||||
{isLoading ? "Signing in..." : "Next step"}
|
||||
</Button>
|
||||
<div className="w-[70%] my-4 mx-auto">
|
||||
<p className="text-xs text-onboarding-text-300">
|
||||
<div className="w-3/4 my-4 mx-auto">
|
||||
<p className="text-xs text-center text-onboarding-text-300">
|
||||
When you click the button above, you agree with our{" "}
|
||||
<a
|
||||
href="https://plane.so/terms-and-conditions"
|
||||
|
@ -6,16 +6,18 @@ import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// images
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
import githubLightModeImage from "/public/logos/github-black.png";
|
||||
import githubDarkModeImage from "/public/logos/github-dark.svg";
|
||||
import { AuthType } from "components/page-views";
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
clientId: string;
|
||||
authType: AuthType;
|
||||
}
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
const { handleSignIn, clientId } = props;
|
||||
const { handleSignIn, clientId, authType } = props;
|
||||
// states
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||
@ -24,7 +26,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
query: { code },
|
||||
} = useRouter();
|
||||
// theme
|
||||
const { theme } = useTheme();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (code && !gitCode) {
|
||||
@ -37,22 +39,23 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
||||
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
setLoginCallBackURL(`${origin}/` as any);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Link
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
>
|
||||
<button
|
||||
className={`flex w-full items-center justify-center gap-2 hover:bg-onboarding-background-300 rounded border border-onboarding-border-200 p-2 text-sm font-medium text-custom-text-100 duration-300 h-[46px]`}
|
||||
className={`flex w-full items-center justify-center gap-2 hover:bg-onboarding-background-300 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 h-[42px] ${
|
||||
resolvedTheme === "dark" ? "bg-[#2F3135] border-[#43484F]" : "border-[#D9E4FF]"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
|
||||
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
<span className="text-onboarding-text-200">Sign in with GitHub</span>
|
||||
<span className="text-onboarding-text-200">{authType == "sign-in" ? "Sign-in" : "Sign-up"} with GitHub</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./deactivate-account-modal";
|
||||
export * from "./email-code-form";
|
||||
export * from "./email-password-form";
|
||||
export * from "./email-forgot-password-form";
|
||||
|
@ -1,12 +1,7 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// react-hook-form
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
// types
|
||||
import { IWorkspace } from "types";
|
||||
// icons
|
||||
import {
|
||||
BarChart2,
|
||||
Briefcase,
|
||||
@ -19,7 +14,16 @@ import {
|
||||
PenSquare,
|
||||
Search,
|
||||
Settings,
|
||||
Bell,
|
||||
} from "lucide-react";
|
||||
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// types
|
||||
import { IWorkspace } from "types";
|
||||
// assets
|
||||
import projectEmoji from "public/emoji/project-emoji.svg";
|
||||
|
||||
const workspaceLinks = [
|
||||
{
|
||||
@ -39,7 +43,7 @@ const workspaceLinks = [
|
||||
name: "All Issues",
|
||||
},
|
||||
{
|
||||
Icon: CheckCircle,
|
||||
Icon: Bell,
|
||||
name: "Notifications",
|
||||
},
|
||||
];
|
||||
@ -89,22 +93,23 @@ const DummySidebar: React.FC<Props> = (props) => {
|
||||
const { workspace: workspaceStore, user: userStore } = useMobxStore();
|
||||
const workspace = workspaceStore.workspaces ? workspaceStore.workspaces[0] : null;
|
||||
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const handleZoomWorkspace = (value: string) => {
|
||||
// console.log(lastWorkspaceName,value);
|
||||
if (lastWorkspaceName === value) return;
|
||||
lastWorkspaceName = value;
|
||||
if (timer > 0) {
|
||||
timer += 2;
|
||||
timer = Math.min(timer, 4);
|
||||
timer = Math.min(timer, 2);
|
||||
} else {
|
||||
timer = 2;
|
||||
timer = Math.min(timer, 4);
|
||||
timer = Math.min(timer, 2);
|
||||
const interval = setInterval(() => {
|
||||
if (timer < 0) {
|
||||
setValue!("name", lastWorkspaceName);
|
||||
clearInterval(interval);
|
||||
}
|
||||
console.log("timer", timer);
|
||||
timer--;
|
||||
}, 1000);
|
||||
}
|
||||
@ -112,7 +117,7 @@ const DummySidebar: React.FC<Props> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (watch) {
|
||||
watch();
|
||||
watch("name");
|
||||
}
|
||||
});
|
||||
|
||||
@ -126,22 +131,34 @@ const DummySidebar: React.FC<Props> = (props) => {
|
||||
render={({ field: { value } }) => {
|
||||
if (value.length > 0) {
|
||||
handleZoomWorkspace(value);
|
||||
} else {
|
||||
lastWorkspaceName = "";
|
||||
}
|
||||
return timer > 0 ? (
|
||||
<div className="py-6 pl-4 top-3 mt-4 transition-all bg-onboarding-background-200 w-full max-w-screen-sm flex items-center ml-6 border-8 border-onboarding-background-100 rounded-md">
|
||||
<div className="bg-onboarding-background-100 w-full p-1 flex items-center">
|
||||
<div className="flex flex-shrink-0">
|
||||
<Avatar
|
||||
name={value.length > 0 ? value[0].toLocaleUpperCase() : "N"}
|
||||
src={""}
|
||||
size={30}
|
||||
shape="square"
|
||||
fallbackBackgroundColor="black"
|
||||
className="!text-base"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`top-3 mt-4 transition-all bg-onboarding-background-200 w-full max-w-screen-sm flex items-center ml-6 border-[6px] ${
|
||||
resolvedTheme == "dark" ? "border-onboarding-background-100" : "border-custom-primary-20"
|
||||
} rounded-xl`}
|
||||
>
|
||||
<div className="border rounded-lg py-6 pl-4 w-full border-onboarding-background-400">
|
||||
<div
|
||||
className={`${
|
||||
resolvedTheme == "light" ? "bg-[#F5F5F5]" : "bg-[#363A40]"
|
||||
} w-full p-1 flex items-center`}
|
||||
>
|
||||
<div className="flex flex-shrink-0">
|
||||
<Avatar
|
||||
name={value.length > 0 ? value[0].toLocaleUpperCase() : "N"}
|
||||
src={""}
|
||||
size={30}
|
||||
shape="square"
|
||||
fallbackBackgroundColor="black"
|
||||
className="!text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-xl font-medium text-onboarding-text-100 ml-2 truncate">{value}</span>
|
||||
<span className="text-xl font-medium text-onboarding-text-100 ml-2 truncate">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -206,7 +223,7 @@ const DummySidebar: React.FC<Props> = (props) => {
|
||||
<div className={`flex items-center justify-between w-full px-1 mb-3 gap-2 mt-4 `}>
|
||||
<div
|
||||
className={`relative flex items-center justify-between w-full rounded gap-1 group
|
||||
px-3 shadow-custom-sidebar-shadow-2xs border-[0.5px] border-custom-border-200
|
||||
px-3 shadow-custom-shadow-2xs border-onboarding-border-100 border
|
||||
`}
|
||||
>
|
||||
<div className={`relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 outline-none`}>
|
||||
@ -217,7 +234,7 @@ const DummySidebar: React.FC<Props> = (props) => {
|
||||
|
||||
<div
|
||||
className={`flex items-center justify-center rounded flex-shrink-0 p-2 outline-none
|
||||
shadow-custom-sidebar-shadow-2xs border-[0.5px] border-onboarding-border-200
|
||||
shadow-custom-shadow-2xs border border-onboarding-border-100
|
||||
`}
|
||||
>
|
||||
<Search className="h-4 w-4 text-onboarding-text-200" />
|
||||
@ -244,11 +261,15 @@ const DummySidebar: React.FC<Props> = (props) => {
|
||||
<div className="px-3">
|
||||
{" "}
|
||||
<div className="w-4/5 flex items-center text-base font-medium text-custom-text-200 mb-3 justify-between">
|
||||
<span> Plane web</span>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Image src={projectEmoji} alt="Plane Logo" className="h-4 w-4" />
|
||||
<span> Plane</span>
|
||||
</div>
|
||||
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
{projectLinks.map((link) => (
|
||||
<a className="block w-full">
|
||||
<a className="block ml-6 w-full">
|
||||
<div
|
||||
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-base font-medium outline-none
|
||||
text-custom-sidebar-text-200 focus:bg-custom-sidebar-background-80
|
||||
|
@ -2,17 +2,17 @@ import React from "react";
|
||||
|
||||
const OnboardingStepIndicator = ({ step }: { step: number }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-4 w-4 rounded-full bg-custom-primary-100 z-10" />
|
||||
<div className="h-3 w-3 rounded-full bg-custom-primary-100 z-10" />
|
||||
<div className={`h-1 w-14 -ml-1 ${step >= 2 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
|
||||
<div
|
||||
className={` z-10 -ml-1 rounded-full ${
|
||||
step >= 2 ? "bg-custom-primary-100 h-4 w-4" : " h-3 w-3 bg-onboarding-background-100"
|
||||
step >= 2 ? "bg-custom-primary-100 h-3 w-3" : " h-2 w-2 bg-onboarding-background-100"
|
||||
}`}
|
||||
/>
|
||||
<div className={`h-1 w-14 -ml-1 ${step >= 3 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
|
||||
<div
|
||||
className={`rounded-full -ml-1 z-10 ${
|
||||
step >= 3 ? "bg-custom-primary-100 h-4 w-4" : "h-3 w-3 bg-onboarding-background-100"
|
||||
step >= 3 ? "bg-custom-primary-100 h-3 w-3" : "h-2 w-2 bg-onboarding-background-100"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
@ -13,7 +13,9 @@ import JoinProjectImg from "public/auth/project-not-authorized.svg";
|
||||
export const JoinProject: React.FC = () => {
|
||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const {
|
||||
user: { joinProject },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -23,7 +25,7 @@ export const JoinProject: React.FC = () => {
|
||||
|
||||
setIsJoiningProject(true);
|
||||
|
||||
projectStore.joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => {
|
||||
joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => {
|
||||
setIsJoiningProject(false);
|
||||
});
|
||||
};
|
||||
|
@ -4,7 +4,6 @@ import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
// components
|
||||
import { CommandModal, ShortcutsModal } from "components/command-palette";
|
||||
import { BulkDeleteIssuesModal } from "components/core";
|
||||
@ -30,7 +29,11 @@ export const CommandPalette: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
|
||||
// store
|
||||
const { commandPalette, theme: themeStore } = useMobxStore();
|
||||
const {
|
||||
commandPalette,
|
||||
theme: { toggleSidebar },
|
||||
user: { currentUser },
|
||||
} = useMobxStore();
|
||||
const {
|
||||
toggleCommandPaletteModal,
|
||||
isCreateIssueModalOpen,
|
||||
@ -52,9 +55,6 @@ export const CommandPalette: FC = observer(() => {
|
||||
isDeleteIssueModalOpen,
|
||||
toggleDeleteIssueModal,
|
||||
} = commandPalette;
|
||||
const { toggleSidebar } = themeStore;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -153,7 +153,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
if (!user) return null;
|
||||
if (!currentUser) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -223,7 +223,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
onClose={() => {
|
||||
toggleBulkDeleteIssueModal(false);
|
||||
}}
|
||||
user={user}
|
||||
user={currentUser}
|
||||
/>
|
||||
<CommandModal />
|
||||
</>
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hook
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// services
|
||||
import { IssueLabelService } from "services/issue";
|
||||
// icons
|
||||
import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui";
|
||||
import {
|
||||
@ -29,11 +27,7 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
// services
|
||||
const issueLabelService = new IssueLabelService();
|
||||
import { useEffect } from "react";
|
||||
|
||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
const router = useRouter();
|
||||
@ -44,7 +38,11 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
<a
|
||||
aria-disabled={activity.issue === null}
|
||||
href={`${
|
||||
activity.issue_detail ? `/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}` : "#"
|
||||
activity.issue_detail
|
||||
? `/${workspaceSlug ?? activity.workspace_detail?.slug}/projects/${activity.project}/issues/${
|
||||
activity.issue
|
||||
}`
|
||||
: "#"
|
||||
}`}
|
||||
target={activity.issue === null ? "_self" : "_blank"}
|
||||
rel={activity.issue === null ? "" : "noopener noreferrer"}
|
||||
@ -63,7 +61,9 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/${workspaceSlug}/profile/${activity.new_identifier ?? activity.old_identifier}`}
|
||||
href={`/${workspaceSlug ?? activity.workspace_detail?.slug}/profile/${
|
||||
activity.new_identifier ?? activity.old_identifier
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
|
||||
@ -73,25 +73,27 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LabelPill = ({ labelId }: { labelId: string }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
|
||||
const {
|
||||
workspace: { labels, fetchWorkspaceLabels },
|
||||
} = useMobxStore();
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null
|
||||
);
|
||||
const workspaceLabels = labels[workspaceSlug];
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);
|
||||
}, [fetchWorkspaceLabels, workspaceLabels, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
||||
backgroundColor: workspaceLabels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const EstimatePoint = ({ point }: { point: string }) => {
|
||||
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
||||
@ -243,24 +245,6 @@ const activityDetails: {
|
||||
},
|
||||
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||
},
|
||||
relates_to: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked that this issue relates to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
if (activity.verb === "created")
|
||||
@ -365,13 +349,13 @@ const activityDetails: {
|
||||
icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity, showIssue) => {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
added a new label{" "}
|
||||
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<LabelPill labelId={activity.new_identifier ?? ""} />
|
||||
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs w-min whitespace-nowrap">
|
||||
<LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
@ -387,7 +371,7 @@ const activityDetails: {
|
||||
<>
|
||||
removed the label{" "}
|
||||
<span className="flex truncate items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<LabelPill labelId={activity.old_identifier ?? ""} />
|
||||
<LabelPill labelId={activity.old_identifier ?? ""} workspaceSlug={workspaceSlug} />
|
||||
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
@ -586,6 +570,24 @@ const activityDetails: {
|
||||
),
|
||||
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
relates_to: {
|
||||
message: (activity) => {
|
||||
if (activity.old_value === "")
|
||||
return (
|
||||
<>
|
||||
marked that this issue relates to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
start_date: {
|
||||
message: (activity, showIssue) => {
|
||||
if (!activity.new_value)
|
||||
@ -675,7 +677,12 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
|
||||
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
||||
);
|
||||
|
||||
export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIssueActivity; showIssue?: boolean }) => {
|
||||
type ActivityMessageProps = {
|
||||
activity: IIssueActivity;
|
||||
showIssue?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityMessage = ({ activity, showIssue = false }: ActivityMessageProps) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
@ -684,7 +691,7 @@ export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIs
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
||||
activity,
|
||||
showIssue,
|
||||
workspaceSlug?.toString() ?? ""
|
||||
workspaceSlug ? workspaceSlug.toString() : activity.workspace_detail?.slug ?? ""
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -30,9 +30,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const createCycle = async (payload: Partial<ICycle>) =>
|
||||
cycleStore
|
||||
.createCycle(workspaceSlug, projectId, payload)
|
||||
const createCycle = async (payload: Partial<ICycle>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const selectedProjectId = payload.project ?? projectId.toString();
|
||||
await cycleStore
|
||||
.createCycle(workspaceSlug, selectedProjectId, payload)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@ -47,10 +49,13 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Error in creating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) =>
|
||||
cycleStore
|
||||
.patchCycle(workspaceSlug, projectId, cycleId, payload)
|
||||
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const selectedProjectId = payload.project ?? projectId.toString();
|
||||
await cycleStore
|
||||
.patchCycle(workspaceSlug, selectedProjectId, cycleId, payload)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@ -65,6 +70,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
||||
message: "Error in updating cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const dateChecker = async (payload: CycleDateCheckData) => {
|
||||
let status = false;
|
||||
|
@ -270,7 +270,20 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
|
||||
: null;
|
||||
|
||||
if (!cycleDetails) return null;
|
||||
if (!cycleDetails)
|
||||
return (
|
||||
<Loader className="px-5">
|
||||
<div className="space-y-2">
|
||||
<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 height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
|
||||
const endDate = new Date(cycleDetails.end_date ?? "");
|
||||
const startDate = new Date(cycleDetails.start_date ?? "");
|
||||
@ -300,57 +313,91 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{cycleDetails ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<button
|
||||
className="flex items-center justify-center h-5 w-5 rounded-full bg-custom-border-300"
|
||||
onClick={() => handleClose()}
|
||||
>
|
||||
<ChevronRight className="h-3 w-3 text-white stroke-2" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<button onClick={handleCopyText}>
|
||||
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||
</button>
|
||||
{!isCompleted && (
|
||||
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<button
|
||||
className="flex items-center justify-center h-5 w-5 rounded-full bg-custom-border-300"
|
||||
onClick={() => handleClose()}
|
||||
>
|
||||
<ChevronRight className="h-3 w-3 text-white stroke-2" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<button onClick={handleCopyText}>
|
||||
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||
</button>
|
||||
{!isCompleted && (
|
||||
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>Delete cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{cycleDetails.name}</h4>
|
||||
<div className="flex items-center gap-5">
|
||||
{currentCycle && (
|
||||
<span
|
||||
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
|
||||
style={{
|
||||
color: currentCycle.color,
|
||||
backgroundColor: `${currentCycle.color}20`,
|
||||
}}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{cycleDetails.name}</h4>
|
||||
<div className="flex items-center gap-5">
|
||||
{currentCycle && (
|
||||
<span
|
||||
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
|
||||
style={{
|
||||
color: currentCycle.color,
|
||||
backgroundColor: `${currentCycle.color}20`,
|
||||
}}
|
||||
>
|
||||
{currentCycle.value === "current"
|
||||
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
|
||||
: `${currentCycle.label}`}
|
||||
</span>
|
||||
)}
|
||||
<div className="relative flex h-full w-52 items-center gap-2.5">
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="text-sm text-custom-text-300 font-medium cursor-default"
|
||||
>
|
||||
{currentCycle.value === "current"
|
||||
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
|
||||
: `${currentCycle.label}`}
|
||||
</span>
|
||||
)}
|
||||
<div className="relative flex h-full w-52 items-center gap-2.5">
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||
<CustomRangeDatePicker
|
||||
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
selectsStart
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-300" />
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<>
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="text-sm text-custom-text-300 font-medium cursor-default"
|
||||
>
|
||||
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
|
||||
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
@ -362,193 +409,145 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||
<CustomRangeDatePicker
|
||||
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
|
||||
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleStartDateChange(val);
|
||||
handleEndDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
selectsStart
|
||||
minDate={new Date(`${watch("start_date")}`)}
|
||||
selectsEnd
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-300" />
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<>
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="text-sm text-custom-text-300 font-medium cursor-default"
|
||||
>
|
||||
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||
</Popover.Button>
|
||||
</>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
|
||||
<CustomRangeDatePicker
|
||||
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
handleEndDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
minDate={new Date(`${watch("start_date")}`)}
|
||||
selectsEnd
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
</Popover>
|
||||
{cycleDetails.description && (
|
||||
<span className="whitespace-normal text-sm leading-5 py-2.5 text-custom-text-200 break-words w-full">
|
||||
{cycleDetails.description}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-5 pt-2.5 pb-6">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
</div>
|
||||
<div className="flex items-center w-1/2 rounded-sm">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar name={cycleDetails.owned_by.display_name} src={cycleDetails.owned_by.avatar} />
|
||||
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cycleDetails.description && (
|
||||
<span className="whitespace-normal text-sm leading-5 py-2.5 text-custom-text-200 break-words w-full">
|
||||
{cycleDetails.description}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-5 pt-2.5 pb-6">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
</div>
|
||||
<div className="flex items-center w-1/2 rounded-sm">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar name={cycleDetails.owned_by.display_name} src={cycleDetails.owned_by.avatar} />
|
||||
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
|
||||
<LayersIcon className="h-4 w-4" />
|
||||
<span className="text-base">Issues</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
|
||||
<LayersIcon className="h-4 w-4" />
|
||||
<span className="text-base">Issues</span>
|
||||
</div>
|
||||
<div className="flex items-center w-1/2">
|
||||
<span className="text-sm text-custom-text-300 px-1.5">{issueCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center w-1/2">
|
||||
<span className="text-sm text-custom-text-300 px-1.5">{issueCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 py-5 px-1.5">
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
|
||||
<Disclosure.Button
|
||||
className="flex w-full items-center justify-between gap-2 p-1.5"
|
||||
disabled={!isStartValid || !isEndValid}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 text-sm">
|
||||
<span className="font-medium text-custom-text-200">Progress</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 py-5 px-1.5">
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
|
||||
<Disclosure.Button
|
||||
className="flex w-full items-center justify-between gap-2 p-1.5"
|
||||
disabled={!isStartValid || !isEndValid}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 text-sm">
|
||||
<span className="font-medium text-custom-text-200">Progress</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
{progressPercentage ? (
|
||||
<span className="flex items-center justify-center h-5 w-9 rounded text-xs font-medium text-amber-500 bg-amber-50">
|
||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||
<div className="flex items-center gap-2.5">
|
||||
{progressPercentage ? (
|
||||
<span className="flex items-center justify-center h-5 w-9 rounded text-xs font-medium text-amber-500 bg-amber-50">
|
||||
{progressPercentage ? `${progressPercentage}%` : ""}
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{isStartValid && isEndValid ? (
|
||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<AlertCircle height={14} width={14} className="text-custom-text-200" />
|
||||
<span className="text-xs italic text-custom-text-200">
|
||||
Invalid date. Please enter valid date.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Transition show={open}>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isStartValid && isEndValid ? (
|
||||
<div className="h-full w-full pt-4">
|
||||
<div className="flex items-start gap-4 py-2 text-xs">
|
||||
<div className="flex items-center gap-3 text-custom-text-100">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||
<span>Ideal</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-40 w-80">
|
||||
<ProgressChart
|
||||
distribution={cycleDetails.distribution?.completion_chart ?? {}}
|
||||
startDate={cycleDetails.start_date ?? ""}
|
||||
endDate={cycleDetails.end_date ?? ""}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{isStartValid && isEndValid ? (
|
||||
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<AlertCircle height={14} width={14} className="text-custom-text-200" />
|
||||
<span className="text-xs italic text-custom-text-200">
|
||||
Invalid date. Please enter valid date.
|
||||
</span>
|
||||
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
|
||||
<div className="h-full w-full pt-5 border-t border-custom-border-200">
|
||||
<SidebarProgressStats
|
||||
distribution={cycleDetails.distribution}
|
||||
groupedIssues={{
|
||||
backlog: cycleDetails.backlog_issues,
|
||||
unstarted: cycleDetails.unstarted_issues,
|
||||
started: cycleDetails.started_issues,
|
||||
completed: cycleDetails.completed_issues,
|
||||
cancelled: cycleDetails.cancelled_issues,
|
||||
}}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
isPeekView={Boolean(peekCycle)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Transition show={open}>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-3">
|
||||
{isStartValid && isEndValid ? (
|
||||
<div className="h-full w-full pt-4">
|
||||
<div className="flex items-start gap-4 py-2 text-xs">
|
||||
<div className="flex items-center gap-3 text-custom-text-100">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
||||
<span>Ideal</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-40 w-80">
|
||||
<ProgressChart
|
||||
distribution={cycleDetails.distribution?.completion_chart ?? {}}
|
||||
startDate={cycleDetails.start_date ?? ""}
|
||||
endDate={cycleDetails.end_date ?? ""}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
|
||||
<div className="h-full w-full pt-5 border-t border-custom-border-200">
|
||||
<SidebarProgressStats
|
||||
distribution={cycleDetails.distribution}
|
||||
groupedIssues={{
|
||||
backlog: cycleDetails.backlog_issues,
|
||||
unstarted: cycleDetails.unstarted_issues,
|
||||
started: cycleDetails.started_issues,
|
||||
completed: cycleDetails.completed_issues,
|
||||
cancelled: cycleDetails.cancelled_issues,
|
||||
}}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
isPeekView={Boolean(peekCycle)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader className="px-5">
|
||||
<div className="space-y-2">
|
||||
<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 height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/i
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@ -19,11 +20,18 @@ type Props = {
|
||||
blocks: IGanttBlock[] | null;
|
||||
enableReorder: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate } = props;
|
||||
const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate, quickAddCallback, viewId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
@ -152,7 +160,9 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
{enableQuickIssueCreate && <GanttInlineCreateIssueForm />}
|
||||
{enableQuickIssueCreate && (
|
||||
<GanttInlineCreateIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
|
@ -19,25 +19,32 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycleId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
cycle: cycleStore,
|
||||
cycleIssueFilter: cycleIssueFilterStore,
|
||||
cycleIssueFilters: cycleIssueFiltersStore,
|
||||
projectIssuesFilter: projectIssueFiltersStore,
|
||||
project: { currentProjectDetails },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectLabel: { projectLabels },
|
||||
projectState: projectStateStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
|
||||
|
||||
@ -49,58 +56,44 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
layout,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
const newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? [];
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (cycleIssueFilterStore.cycleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, cycleId);
|
||||
},
|
||||
[cycleId, cycleIssueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayPropertiesUpdate = useCallback(
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, cycleId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const cyclesList = cycleStore.projectCycles;
|
||||
@ -173,25 +166,25 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FilterSelection
|
||||
filters={cycleIssueFilterStore.cycleFilters}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels ?? undefined}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
|
@ -9,6 +9,7 @@ export * from "./workspace-analytics";
|
||||
export * from "./workspace-dashboard";
|
||||
export * from "./projects";
|
||||
export * from "./profile-preferences";
|
||||
export * from "./profile-settings";
|
||||
export * from "./cycles";
|
||||
export * from "./modules-list";
|
||||
export * from "./project-settings";
|
||||
|
@ -19,24 +19,30 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
module: moduleStore,
|
||||
moduleFilter: moduleFilterStore,
|
||||
project: { currentProjectDetails },
|
||||
projectLabel: { projectLabels },
|
||||
projectIssuesFilter: projectIssueFiltersStore,
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
projectLabel: { projectLabels },
|
||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
||||
|
||||
@ -45,61 +51,49 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
setValue(`${!isSidebarCollapsed}`);
|
||||
};
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
layout,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
const newValues = moduleFilterStore.moduleFilters?.[key] ?? [];
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (moduleFilterStore.moduleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, moduleId);
|
||||
},
|
||||
[moduleId, moduleFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayPropertiesUpdate = useCallback(
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, moduleId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
||||
@ -172,25 +166,25 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FilterSelection
|
||||
filters={moduleFilterStore.moduleFilters}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels ?? undefined}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
|
30
web/components/headers/profile-settings.tsx
Normal file
30
web/components/headers/profile-settings.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { FC } from "react";
|
||||
// ui
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { Settings } from "lucide-react";
|
||||
|
||||
interface IProfileSettingHeader {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const ProfileSettingsHeader: FC<IProfileSettingHeader> = (props) => {
|
||||
const { title } = props;
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
label="My Profile"
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
link={`/me/profile`}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -16,85 +16,72 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
project: { currentProjectDetails },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
inbox: inboxStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
// issue filters
|
||||
projectIssuesFilter: { issueFilters, updateFilters },
|
||||
projectIssues: {},
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
layout,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newValues = issueFilterStore.userFilters?.[key] ?? [];
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilterStore.userFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: newValues,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout });
|
||||
},
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayPropertiesUpdate = useCallback(
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId.toString()]?.[0] : undefined;
|
||||
const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId]?.[0] : undefined;
|
||||
|
||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||
|
||||
@ -173,29 +160,29 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FilterSelection
|
||||
filters={issueFilterStore.userFilters}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels ?? undefined}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{projectId && inboxStore.isInboxEnabled && inboxDetails && (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId.toString())}`}>
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId)}`}>
|
||||
<a>
|
||||
<Button variant="neutral-primary" size="sm" className="relative">
|
||||
Inbox
|
||||
|
@ -14,10 +14,15 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const { workspaceSlug, projectId, viewId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
@ -27,67 +32,54 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectViews: projectViewsStore,
|
||||
viewIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
layout,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, viewId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, viewId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !viewId) return;
|
||||
|
||||
const newValues = storedFilters?.[key] ?? [];
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (storedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, viewId);
|
||||
},
|
||||
[projectViewFiltersStore, storedFilters, viewId, workspaceSlug]
|
||||
[workspaceSlug, projectId, viewId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, viewId, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayPropertiesUpdate = useCallback(
|
||||
const handleDisplayProperties = useCallback(
|
||||
(property: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, viewId);
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
[workspaceSlug, projectId, viewId, updateFilters]
|
||||
);
|
||||
|
||||
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||
@ -157,25 +149,25 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
<FilterSelection
|
||||
filters={storedFilters ?? {}}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels ?? undefined}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
@ -1,36 +1,14 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// ui
|
||||
import { Breadcrumbs } from "@plane/ui";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
// hooks
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export interface IUserProfileHeader {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const UserProfileHeader: FC<IUserProfileHeader> = observer((props) => {
|
||||
const { title } = props;
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
label="Profile"
|
||||
icon={<UserCircle2 className="h-4 w-4 text-custom-text-300" />}
|
||||
link={`/${workspaceSlug}/me/profile`}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label={title} />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
export const UserProfileHeader = () => (
|
||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||
<div>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/me/profile" />
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
</div>
|
||||
);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Cog, LogIn, LogOut, Settings, UserCircle2 } from "lucide-react";
|
||||
import { mutate } from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
@ -23,7 +25,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
|
||||
{
|
||||
name: "Settings",
|
||||
icon: Settings,
|
||||
link: `/${workspaceSlug}/me/profile`,
|
||||
link: `/me/profile`,
|
||||
},
|
||||
];
|
||||
|
||||
@ -39,6 +41,7 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
} = useMobxStore();
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
// redirect url for normal mode
|
||||
const redirectWorkspaceSlug =
|
||||
@ -51,6 +54,8 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
await authService
|
||||
.signOut()
|
||||
.then(() => {
|
||||
mutate("CURRENT_USER_DETAILS", null);
|
||||
setTheme("system");
|
||||
router.push("/");
|
||||
})
|
||||
.catch(() =>
|
||||
@ -70,13 +75,13 @@ export const InstanceSidebarDropdown = observer(() => {
|
||||
sidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}>
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center justify-center h-6 w-6 bg-custom-sidebar-background-80 rounded`}
|
||||
>
|
||||
<Cog className="h-5 w-5 text-custom-text-200" />
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>
|
||||
)}
|
||||
{!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { useEffect, useState, Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
@ -17,14 +13,9 @@ type Props = {
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeleteIssueModal: React.FC<Props> = observer((props) => {
|
||||
export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
const { data, isOpen, handleClose, onSubmit } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { issueDetail: issueDetailStore } = useMobxStore();
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -37,12 +28,7 @@ export const DeleteIssueModal: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
const handleIssueDelete = async () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await issueDetailStore.deleteIssue(workspaceSlug.toString(), data.project, data.id);
|
||||
|
||||
if (onSubmit) await onSubmit().finally(() => setIsDeleteLoading(false));
|
||||
};
|
||||
|
||||
@ -75,9 +61,9 @@ export const DeleteIssueModal: React.FC<Props> = observer((props) => {
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
|
||||
</span>
|
||||
@ -114,4 +100,4 @@ export const DeleteIssueModal: React.FC<Props> = observer((props) => {
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -0,0 +1,89 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { ICycleIssuesStore, IModuleIssuesStore, IProjectIssuesStore, IViewIssuesStore } from "store/issues";
|
||||
import { IIssueCalendarViewStore, IssueStore } from "store/issue";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues } from "store/issues/types";
|
||||
|
||||
interface IBaseCalendarRoot {
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
calendarViewStore: IIssueCalendarViewStore;
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
issueActions: {
|
||||
[EIssueActions.DELETE]: (issue: IIssue) => void;
|
||||
[EIssueActions.UPDATE]?: (issue: IIssue) => void;
|
||||
[EIssueActions.REMOVE]?: (issue: IIssue) => void;
|
||||
};
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
const { issueStore, calendarViewStore, QuickActions, issueActions, viewId } = props;
|
||||
const { projectIssuesFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const displayFilters = issueFilterStore.issueFilters?.displayFilters;
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues;
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
calendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(date: string, issue: IIssue, action: EIssueActions) => {
|
||||
if (issueActions[action]) {
|
||||
issueActions[action]!(issue);
|
||||
}
|
||||
},
|
||||
[issueStore]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
layout={displayFilters?.calendar?.layout}
|
||||
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(issue) => (
|
||||
<QuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE]
|
||||
? async (data) => handleIssues(issue.target_date ?? "", data, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE]
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -8,19 +8,28 @@ import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { ICalendarWeek } from "./types";
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueGroupedStructure | null;
|
||||
issues: IIssueResponse | undefined;
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
layout: "month" | "week" | undefined;
|
||||
showWeekends: boolean;
|
||||
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
const { issues, layout, showWeekends, handleIssues, quickActions } = props;
|
||||
const { issues, groupedIssueIds, layout, showWeekends, handleIssues, quickActions, quickAddCallback, viewId } = props;
|
||||
|
||||
const { calendar: calendarStore } = useMobxStore();
|
||||
|
||||
@ -49,9 +58,12 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
key={weekIndex}
|
||||
week={week}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -60,9 +72,12 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
<CalendarWeekDays
|
||||
week={calendarStore.allDaysOfActiveWeek}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
enableQuickIssueCreate
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -4,31 +4,48 @@ import { Droppable } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarIssueBlocks, ICalendarDate, CalendarInlineCreateIssueForm } from "components/issues";
|
||||
import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
date: ICalendarDate;
|
||||
issues: IIssueGroupedStructure | null;
|
||||
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void;
|
||||
issues: IIssueResponse | undefined;
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
const { date, issues, handleIssues, quickActions, enableQuickIssueCreate } = props;
|
||||
const {
|
||||
date,
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
|
||||
const issuesList = issues ? (issues as IIssueGroupedStructure)[renderDateFormat(date.date)] : null;
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[renderDateFormat(date.date)] : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -64,14 +81,22 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlocks issues={issuesList} handleIssues={handleIssues} quickActions={quickActions} />
|
||||
<CalendarIssueBlocks
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
/>
|
||||
{enableQuickIssueCreate && (
|
||||
<div className="py-1 px-2">
|
||||
<CalendarInlineCreateIssueForm
|
||||
<CalendarQuickAddIssueForm
|
||||
formKey="target_date"
|
||||
groupId={renderDateFormat(date.date)}
|
||||
prePopulatedData={{
|
||||
target_date: renderDateFormat(date.date),
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -7,4 +7,4 @@ export * from "./header";
|
||||
export * from "./issue-blocks";
|
||||
export * from "./week-days";
|
||||
export * from "./week-header";
|
||||
export * from "./inline-create-issue-form";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
@ -5,66 +5,74 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
issues: IIssue[] | null;
|
||||
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void;
|
||||
issues: IIssueResponse | undefined;
|
||||
issueIdList: string[] | null;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
const { issues, handleIssues, quickActions } = props;
|
||||
const { issues, issueIdList, handleIssues, quickActions } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issues?.map((issue, index) => (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="p-1 px-2 relative"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||
)}
|
||||
{issueIdList?.map((issueId, index) => {
|
||||
if (!issues?.[issueId]) return null;
|
||||
|
||||
const issue = issues?.[issueId];
|
||||
return (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
|
||||
snapshot.isDragging
|
||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||
: "bg-custom-background-100 hover:bg-custom-background-90"
|
||||
}`}
|
||||
className="p-1 px-2 relative"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={issue?.workspace_detail?.slug}
|
||||
projectId={issue?.project_detail?.id}
|
||||
issueId={issue?.id}
|
||||
// TODO: add the logic here
|
||||
handleIssue={(issueToUpdate) => {
|
||||
handleIssues(issue.target_date ?? "", { ...issue, ...issueToUpdate }, "update");
|
||||
}}
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
|
||||
snapshot.isDragging
|
||||
? "shadow-custom-shadow-rg bg-custom-background-90"
|
||||
: "bg-custom-background-100 hover:bg-custom-background-90"
|
||||
}`}
|
||||
>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div className="text-xs truncate">{issue.name}</div>
|
||||
</Tooltip>
|
||||
</IssuePeekOverview>
|
||||
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={issue?.workspace_detail?.slug}
|
||||
projectId={issue?.project_detail?.id}
|
||||
issueId={issue?.id}
|
||||
// TODO: add the logic here
|
||||
handleIssue={(issueToUpdate) => {
|
||||
handleIssues(issue.target_date ?? "", { ...issue, ...issueToUpdate }, EIssueActions.UPDATE);
|
||||
}}
|
||||
>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div className="text-xs truncate">{issue.name}</div>
|
||||
</Tooltip>
|
||||
</IssuePeekOverview>
|
||||
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -7,19 +7,26 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
// icons
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
formKey: keyof IIssue;
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
@ -49,15 +56,14 @@ const Inputs = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData, groupId } = props;
|
||||
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
const { workspace: workspaceStore, project: projectStore } = useMobxStore();
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -67,7 +73,10 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
// derived values
|
||||
const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null;
|
||||
const projectDetail: IProject | null =
|
||||
(workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null;
|
||||
|
||||
const {
|
||||
reset,
|
||||
@ -84,9 +93,6 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
@ -106,42 +112,36 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
if (isSubmitting || !groupId || !workspaceDetail || !projectDetail) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
const payload = createIssuePayload(workspaceDetail, projectDetail, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: groupId ?? null,
|
||||
sub_group_id: null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
quickAddCallback &&
|
||||
(await quickAddCallback(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
...payload,
|
||||
},
|
||||
viewId
|
||||
));
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
console.error(err);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.message || "Some error occurred. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -159,7 +159,7 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex w-full px-2 border-[0.5px] border-custom-border-200 rounded z-50 items-center gap-x-2 bg-custom-background-100 shadow-custom-shadow-2xs transition-opacity"
|
||||
>
|
||||
<Inputs register={register} setFocus={setFocus} projectDetails={projectDetails} />
|
||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetails={projectDetail} />
|
||||
</form>
|
||||
</div>
|
||||
)}
|
@ -1,80 +1,43 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart, CycleIssueQuickActions } from "components/issues";
|
||||
import { CycleIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
|
||||
export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
const {
|
||||
cycleIssue: cycleIssueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
cycleIssueCalendarView: cycleIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
const { cycleIssues: cycleIssueStore, cycleIssueCalendarView: cycleIssueCalendarViewStore } = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
cycleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = cycleIssueStore.getIssues;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(date: string, issue: IIssue, action: "update" | "delete" | "remove") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
cycleIssueStore.updateIssueStructure(date, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") cycleIssueStore.deleteIssue(date, null, issue);
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
cycleIssueStore.deleteIssue(date, null, issue);
|
||||
cycleIssueStore.removeIssueFromCycle(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
cycleId.toString(),
|
||||
issue.bridge_id
|
||||
);
|
||||
}
|
||||
cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
|
||||
},
|
||||
[cycleIssueStore, issueDetailStore, cycleId, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
|
||||
cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues as IIssueGroupedStructure | null}
|
||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
||||
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(issue) => (
|
||||
<CycleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
|
||||
handleRemoveFromCycle={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
<BaseCalendarRoot
|
||||
issueStore={cycleIssueStore}
|
||||
calendarViewStore={cycleIssueCalendarViewStore}
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
viewId={cycleId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,82 +1,42 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart, ModuleIssueQuickActions } from "components/issues";
|
||||
import { ModuleIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
|
||||
export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
const {
|
||||
moduleIssue: moduleIssueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
moduleIssueCalendarView: moduleIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
const { moduleIssues: moduleIssueStore, moduleIssueCalendarView: moduleIssueCalendarViewStore } = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
moduleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
|
||||
},
|
||||
[EIssueActions.DELETE]: (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
|
||||
},
|
||||
};
|
||||
|
||||
const issues = moduleIssueStore.getIssues;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(date: string, issue: IIssue, action: "update" | "delete" | "remove") => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
moduleIssueStore.updateIssueStructure(date, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
} else {
|
||||
moduleIssueStore.deleteIssue(date, null, issue);
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
}
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
moduleIssueStore.deleteIssue(date, null, issue);
|
||||
moduleIssueStore.removeIssueFromModule(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
moduleId.toString(),
|
||||
issue.bridge_id
|
||||
);
|
||||
}
|
||||
},
|
||||
[moduleIssueStore, issueDetailStore, moduleId, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues as IIssueGroupedStructure | null}
|
||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
||||
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(issue) => (
|
||||
<ModuleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
|
||||
handleRemoveFromModule={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
<BaseCalendarRoot
|
||||
issueStore={moduleIssueStore}
|
||||
calendarViewStore={moduleIssueCalendarViewStore}
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
viewId={moduleId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,72 +1,42 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { IIssue } from "types";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const CalendarLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
||||
|
||||
const {
|
||||
issue: issueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectIssues: issueStore,
|
||||
issueCalendarView: issueCalendarViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
issueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(date: string, issue: IIssue, action: "update" | "delete") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === "update") {
|
||||
issueStore.updateIssueStructure(date, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
} else {
|
||||
issueStore.deleteIssue(date, null, issue);
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
}
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[issueStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues as IIssueGroupedStructure | null}
|
||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
||||
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
<BaseCalendarRoot
|
||||
issueStore={issueStore}
|
||||
calendarViewStore={issueCalendarViewStore}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -7,66 +7,39 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
|
||||
export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||
const {
|
||||
projectViewIssues: projectViewIssuesStore,
|
||||
issueFilter: issueFilterStore,
|
||||
viewIssues: projectViewIssuesStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectViewIssueCalendarView: projectViewIssueCalendarViewStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
projectViewIssueCalendarViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = projectViewIssuesStore.getIssues;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(date: string, issue: IIssue, action: "update" | "delete") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === "update") {
|
||||
projectViewIssuesStore.updateIssueStructure(date, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
} else {
|
||||
projectViewIssuesStore.deleteIssue(date, null, issue);
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
}
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[projectViewIssuesStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart
|
||||
issues={issues as IIssueGroupedStructure | null}
|
||||
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
|
||||
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
<BaseCalendarRoot
|
||||
issueStore={projectViewIssuesStore}
|
||||
calendarViewStore={projectViewIssueCalendarViewStore}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -8,19 +8,37 @@ import { CalendarDayTile } from "components/issues";
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICalendarDate, ICalendarWeek } from "./types";
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueGroupedStructure | null;
|
||||
issues: IIssueResponse | undefined;
|
||||
groupedIssueIds: IGroupedIssues;
|
||||
week: ICalendarWeek | undefined;
|
||||
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void;
|
||||
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (issue: IIssue) => React.ReactNode;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
const { issues, week, handleIssues, quickActions, enableQuickIssueCreate } = props;
|
||||
const {
|
||||
issues,
|
||||
groupedIssueIds,
|
||||
week,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
enableQuickIssueCreate,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
@ -43,9 +61,12 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
key={renderDateFormat(date.date)}
|
||||
date={date}
|
||||
issues={issues}
|
||||
groupedIssueIds={groupedIssueIds}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -6,61 +6,69 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycleId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
cycleIssueFilter: cycleIssueFilterStore,
|
||||
projectState: projectStateStore,
|
||||
projectMember: { projectMembers },
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const userFilters = cycleIssueFilterStore.cycleFilters;
|
||||
const userFilters = issueFilters?.filters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters).forEach(([key, value]) => {
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!value) {
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), {
|
||||
[key]: null,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
cycleId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? [];
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
cycleId
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId?.toString(), {
|
||||
...newFilters,
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, cycleId);
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
@ -74,7 +82,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||
states={projectStateStore.states?.[cycleId ?? ""]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,61 +7,69 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
projectLabel: { projectLabels },
|
||||
moduleFilter: moduleFilterStore,
|
||||
projectState: projectStateStore,
|
||||
projectMember: { projectMembers },
|
||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const userFilters = moduleFilterStore.moduleFilters;
|
||||
const userFilters = issueFilters?.filters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters).forEach(([key, value]) => {
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!value) {
|
||||
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
|
||||
[key]: null,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
moduleId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = moduleFilterStore.moduleFilters?.[key] ?? [];
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
moduleId
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId?.toString(), {
|
||||
...newFilters,
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, moduleId);
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
@ -75,7 +83,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||
states={projectStateStore.states?.[moduleId ?? ""]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,65 +7,53 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectState: projectStateStore,
|
||||
projectMember: { projectMembers },
|
||||
projectIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const userFilters = issueFilterStore.userFilters;
|
||||
const userFilters = issueFilters?.filters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters).forEach(([key, value]) => {
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!value) {
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: null,
|
||||
},
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = issueFilterStore.userFilters?.[key] ?? [];
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: newValues,
|
||||
},
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: { ...newFilters },
|
||||
});
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters });
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
|
@ -12,65 +12,78 @@ import { Button } from "@plane/ui";
|
||||
import { areFiltersDifferent } from "helpers/filter.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
|
||||
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const { workspaceSlug, projectId, viewId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectMember: { projectMembers },
|
||||
projectViews: projectViewsStore,
|
||||
projectViewFilters: projectViewFiltersStore,
|
||||
viewIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
||||
|
||||
const userFilters = issueFilters?.filters;
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(storedFilters ?? {}).forEach(([key, value]) => {
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!viewId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
if (!value) {
|
||||
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||
[key]: null,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = storedFilters?.[key] ?? [];
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(storedFilters ?? {}).forEach((key) => {
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||
...newFilters,
|
||||
});
|
||||
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, viewId);
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
const handleUpdateView = () => {
|
||||
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
|
||||
|
||||
@ -82,17 +95,6 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
// update stored filters when view details are fetched
|
||||
useEffect(() => {
|
||||
if (!viewId || !viewDetails) return;
|
||||
|
||||
if (!projectViewFiltersStore.storedFilters[viewId.toString()])
|
||||
projectViewFiltersStore.updateStoredFilters(viewId.toString(), viewDetails?.query_data ?? {});
|
||||
}, [projectViewFiltersStore, viewDetails, viewId]);
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 p-4">
|
||||
<AppliedFiltersList
|
||||
|
@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { IssueGanttBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
renderIssueBlocksStructure,
|
||||
IssueGanttSidebar,
|
||||
} from "components/gantt-chart";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import {
|
||||
ICycleIssuesFilterStore,
|
||||
ICycleIssuesStore,
|
||||
IModuleIssuesFilterStore,
|
||||
IModuleIssuesStore,
|
||||
IProjectIssuesFilterStore,
|
||||
IProjectIssuesStore,
|
||||
IViewIssuesFilterStore,
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { EUserWorkspaceRoles } from "layouts/settings-layout/workspace/sidebar";
|
||||
import { TUnGroupedIssues } from "store/issues/types";
|
||||
|
||||
interface IBaseGanttRoot {
|
||||
issueFiltersStore:
|
||||
| IProjectIssuesFilterStore
|
||||
| IModuleIssuesFilterStore
|
||||
| ICycleIssuesFilterStore
|
||||
| IViewIssuesFilterStore;
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
|
||||
const { issueFiltersStore, issueStore, viewId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters;
|
||||
|
||||
const issuesResponse = issueStore.getIssues;
|
||||
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
|
||||
|
||||
const issues = issueIds.map((id) => issuesResponse?.[id]);
|
||||
|
||||
const updateIssue = (issue: IIssue, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
//Todo fix sort order in the structure
|
||||
issueStore.updateIssue(workspaceSlug, issue.project, issue.id, {
|
||||
start_date: payload.start_date,
|
||||
target_date: payload.target_date,
|
||||
});
|
||||
};
|
||||
|
||||
const isAllowed = (projectDetails?.member_role || 0) >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
||||
sidebarToRender={(props) => (
|
||||
<IssueGanttSidebar
|
||||
{...props}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
enableQuickIssueCreate
|
||||
/>
|
||||
)}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,57 +1,15 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { IssueGanttBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
renderIssueBlocksStructure,
|
||||
IssueGanttSidebar,
|
||||
} from "components/gantt-chart";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const CycleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
const { cycleId } = router.query as { cycleId: string };
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
|
||||
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
||||
|
||||
const issues = cycleIssueStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
cycleIssueStore.updateGanttIssueStructure(workspaceSlug.toString(), cycleId.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
||||
sidebarToRender={(props) => <IssueGanttSidebar {...props} />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters.order_by === "sort_order" && isAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <BaseGanttRoot issueFiltersStore={cycleIssueFilterStore} issueStore={cycleIssueStore} viewId={cycleId} />;
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
export * from "./blocks";
|
||||
export * from "./cycle-root";
|
||||
export * from "./quick-add-issue-form";
|
||||
export * from "./module-root";
|
||||
export * from "./project-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./root";
|
||||
export * from "./inline-create-issue-form";
|
||||
|
@ -1,57 +1,15 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
renderIssueBlocksStructure,
|
||||
IssueGanttSidebar,
|
||||
} from "components/gantt-chart";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const ModuleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
const { moduleId } = router.query as { moduleId: string };
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
|
||||
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
||||
|
||||
const issues = moduleIssueStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
moduleIssueStore.updateGanttIssueStructure(workspaceSlug.toString(), moduleId.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
sidebarToRender={(data) => <IssueGanttSidebar {...data} />}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters.order_by === "sort_order" && isAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <BaseGanttRoot issueFiltersStore={moduleIssueFilterStore} issueStore={moduleIssueStore} viewId={moduleId} />;
|
||||
});
|
||||
|
12
web/components/issues/issue-layouts/gantt/project-root.tsx
Normal file
12
web/components/issues/issue-layouts/gantt/project-root.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
|
||||
export const GanttLayout: React.FC = observer(() => {
|
||||
const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={projectIssueFiltersStore} issueStore={projectIssuesStore} />;
|
||||
});
|
@ -1,59 +1,11 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { IssueGanttBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
renderIssueBlocksStructure,
|
||||
ProjectViewGanttSidebar,
|
||||
} from "components/gantt-chart";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
|
||||
export const ProjectViewGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, viewId } = router.query;
|
||||
const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { projectViewIssues: projectViewIssuesStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
||||
|
||||
const issues = projectViewIssuesStore.getIssues;
|
||||
|
||||
const updateIssue = (block: any, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !viewId) return;
|
||||
|
||||
projectViewIssuesStore.updateGanttIssueStructure(workspaceSlug.toString(), viewId.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
||||
sidebarToRender={(props) => <ProjectViewGanttSidebar {...props} />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters.order_by === "sort_order" && isAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <BaseGanttRoot issueFiltersStore={projectIssueViewFiltersStore} issueStore={projectIssueViewStore} />;
|
||||
});
|
||||
|
@ -20,6 +20,13 @@ import { createIssuePayload } from "helpers/issue.helper";
|
||||
type Props = {
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
@ -47,14 +54,14 @@ const Inputs = (props: any) => {
|
||||
};
|
||||
|
||||
export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData } = props;
|
||||
const { prePopulatedData, quickAddCallback, viewId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
const { workspace: workspaceStore } = useMobxStore();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
@ -114,15 +121,7 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: null,
|
||||
sub_group_id: null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
quickAddCallback && quickAddCallback(workspaceSlug, projectId, payload, viewId);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { IssueGanttBlock } from "components/issues";
|
||||
import {
|
||||
GanttChartRoot,
|
||||
IBlockUpdateData,
|
||||
renderIssueBlocksStructure,
|
||||
IssueGanttSidebar,
|
||||
} from "components/gantt-chart";
|
||||
// types
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const GanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
const updateIssue = (block: IIssue, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
issueStore.updateGanttIssueStructure(workspaceSlug.toString(), block, payload);
|
||||
};
|
||||
|
||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full">
|
||||
<GanttChartRoot
|
||||
border={false}
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
|
||||
blockUpdateHandler={updateIssue}
|
||||
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} handleIssue={updateIssue} />}
|
||||
sidebarToRender={(props) => <IssueGanttSidebar {...props} enableQuickIssueCreate />}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
enableBlockRightResize={isAllowed}
|
||||
enableBlockMove={isAllowed}
|
||||
enableReorder={appliedDisplayFilters.order_by === "sort_order" && isAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
194
web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
Normal file
194
web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
import { FC, useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { ICycleIssuesStore, IModuleIssuesStore, IProjectIssuesStore, IViewIssuesStore } from "store/issues";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
import { IIssueKanBanViewStore } from "store/issue";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
//components
|
||||
import { KanBan } from "./default";
|
||||
import { KanBanSwimLanes } from "./swimlanes";
|
||||
|
||||
export interface IBaseKanBanLayout {
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
kanbanViewStore: IIssueKanBanViewStore;
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
issueActions: {
|
||||
[EIssueActions.DELETE]: (issue: IIssue) => void;
|
||||
[EIssueActions.UPDATE]?: (issue: IIssue) => void;
|
||||
[EIssueActions.REMOVE]?: (issue: IIssue) => void;
|
||||
};
|
||||
showLoader?: boolean;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
|
||||
const { issueStore, kanbanViewStore, QuickActions, issueActions, showLoader, viewId } = props;
|
||||
|
||||
const {
|
||||
project: { workspaceProjects },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectIssuesFilter: issueFilterStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = issueStore?.getIssues || {};
|
||||
const issueIds = issueStore?.getIssuesIds || [];
|
||||
|
||||
const displayFilters = issueFilterStore?.issueFilters?.displayFilters;
|
||||
const displayProperties = issueFilterStore?.issueFilters?.displayProperties || null;
|
||||
|
||||
const sub_group_by: string | null = displayFilters?.sub_group_by || null;
|
||||
|
||||
const group_by: string | null = displayFilters?.group_by || null;
|
||||
|
||||
const order_by: string | null = displayFilters?.order_by || null;
|
||||
|
||||
const userDisplayFilters = displayFilters || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = sub_group_by ? "swimlanes" : "default";
|
||||
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
|
||||
const onDragStart = () => {
|
||||
setIsDragStarted(true);
|
||||
};
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
setIsDragStarted(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (
|
||||
result.destination &&
|
||||
result.source &&
|
||||
result.source.droppableId &&
|
||||
result.destination.droppableId &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index
|
||||
)
|
||||
return;
|
||||
|
||||
currentKanBanView === "default"
|
||||
? kanbanViewStore?.handleDragDrop(result.source, result.destination)
|
||||
: kanbanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
async (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => {
|
||||
if (issueActions[action]) {
|
||||
issueActions[action]!(issue);
|
||||
}
|
||||
},
|
||||
[issueStore]
|
||||
);
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
kanbanViewStore.handleKanBanToggle(toggle, value);
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLoader && issueStore?.loader === "mutation" && (
|
||||
<div className="fixed top-16 right-2 z-30 bg-custom-background-80 shadow-custom-shadow-sm w-10 h-10 rounded flex justify-center items-center">
|
||||
<Spinner className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||
{currentKanBanView === "default" ? (
|
||||
<KanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<QuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE]
|
||||
? async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE]
|
||||
? async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.REMOVE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={kanbanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
enableQuickIssueCreate
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<QuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE]
|
||||
? async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE]
|
||||
? async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.REMOVE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={kanbanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
@ -5,6 +5,7 @@ import { Tooltip } from "@plane/ui";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
// types
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
sub_group_id: string;
|
||||
@ -13,14 +14,9 @@ interface IssueBlockProps {
|
||||
issue: IIssue;
|
||||
isDragDisabled: boolean;
|
||||
showEmptyGroup: boolean;
|
||||
handleIssues: (
|
||||
sub_group_by: string | null,
|
||||
group_by: string | null,
|
||||
issue: IIssue,
|
||||
action: "update" | "delete"
|
||||
) => void;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
@ -37,7 +33,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
} = props;
|
||||
|
||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update");
|
||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -79,7 +75,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!columnId && columnId === "null" ? null : columnId,
|
||||
{ ...issue, ...issueToUpdate },
|
||||
"update"
|
||||
EIssueActions.UPDATE
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
@ -1,21 +1,19 @@
|
||||
// components
|
||||
import { KanbanIssueBlock } from "components/issues";
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssueResponse } from "store/issues/types";
|
||||
|
||||
interface IssueBlocksListProps {
|
||||
sub_group_id: string;
|
||||
columnId: string;
|
||||
issues: IIssue[];
|
||||
issues: IIssueResponse;
|
||||
issueIds: string[];
|
||||
isDragDisabled: boolean;
|
||||
showEmptyGroup: boolean;
|
||||
handleIssues: (
|
||||
sub_group_by: string | null,
|
||||
group_by: string | null,
|
||||
issue: IIssue,
|
||||
action: "update" | "delete"
|
||||
) => void;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
|
||||
@ -23,6 +21,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
|
||||
sub_group_id,
|
||||
columnId,
|
||||
issues,
|
||||
issueIds,
|
||||
showEmptyGroup,
|
||||
isDragDisabled,
|
||||
handleIssues,
|
||||
@ -32,22 +31,28 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
|
||||
|
||||
return (
|
||||
<>
|
||||
{issues && issues.length > 0 ? (
|
||||
{issueIds && issueIds.length > 0 ? (
|
||||
<>
|
||||
{issues.map((issue, index) => (
|
||||
<KanbanIssueBlock
|
||||
key={`kanban-issue-block-${issue.id}`}
|
||||
index={index}
|
||||
issue={issue}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
columnId={columnId}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={isDragDisabled}
|
||||
/>
|
||||
))}
|
||||
{issueIds.map((issueId, index) => {
|
||||
if (!issues[issueId]) return null;
|
||||
|
||||
const issue = issues[issueId];
|
||||
|
||||
return (
|
||||
<KanbanIssueBlock
|
||||
key={`kanban-issue-block-${issue.id}`}
|
||||
index={index}
|
||||
issue={issue}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
columnId={columnId}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={isDragDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
!isDragDisabled && (
|
||||
|
@ -5,40 +5,47 @@ import { Droppable } from "@hello-pangea/dnd";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "components/issues";
|
||||
// types
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
import { IIssueDisplayProperties, IIssue, IState } from "types";
|
||||
// constants
|
||||
import { getValueFromObject } from "constants/issue";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
|
||||
|
||||
export interface IGroupByKanBan {
|
||||
issues: any;
|
||||
issues: IIssueResponse;
|
||||
issueIds: any;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
order_by: string | null;
|
||||
sub_group_id: string;
|
||||
list: any;
|
||||
listKey: string;
|
||||
states: IState[] | null;
|
||||
isDragDisabled: boolean;
|
||||
handleIssues: (
|
||||
sub_group_by: string | null,
|
||||
group_by: string | null,
|
||||
issue: IIssue,
|
||||
action: "update" | "delete"
|
||||
) => void;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
showEmptyGroup: boolean;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
isDragStarted?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
const {
|
||||
issues,
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
order_by,
|
||||
@ -54,6 +61,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleKanBanToggle,
|
||||
enableQuickIssueCreate,
|
||||
isDragStarted,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const verticalAlignPosition = (_list: any) =>
|
||||
@ -74,7 +83,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
column_value={_list}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
issues_count={issues?.[getValueFromObject(_list, listKey) as string]?.length || 0}
|
||||
issues_count={issueIds?.[getValueFromObject(_list, listKey) as string]?.length || 0}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
/>
|
||||
@ -102,7 +111,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
<KanbanIssueBlocksList
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={getValueFromObject(_list, listKey) as string}
|
||||
issues={issues[getValueFromObject(_list, listKey) as string]}
|
||||
issues={issues}
|
||||
issueIds={issueIds?.[getValueFromObject(_list, listKey) as string] || []}
|
||||
isDragDisabled={isDragDisabled}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -125,13 +135,16 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
|
||||
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky bottom-0 z-[0]">
|
||||
{enableQuickIssueCreate && (
|
||||
<BoardInlineCreateIssueForm
|
||||
<KanBanQuickAddIssueForm
|
||||
formKey="name"
|
||||
groupId={getValueFromObject(_list, listKey) as string}
|
||||
subGroupId={sub_group_id}
|
||||
prePopulatedData={{
|
||||
...(group_by && { [group_by]: getValueFromObject(_list, listKey) }),
|
||||
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -152,19 +165,15 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
});
|
||||
|
||||
export interface IKanBan {
|
||||
issues: any;
|
||||
issues: IIssueResponse;
|
||||
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
order_by: string | null;
|
||||
sub_group_id?: string;
|
||||
handleIssues: (
|
||||
sub_group_by: string | null,
|
||||
group_by: string | null,
|
||||
issue: IIssue,
|
||||
action: "update" | "delete"
|
||||
) => void;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
showEmptyGroup: boolean;
|
||||
@ -176,11 +185,19 @@ export interface IKanBan {
|
||||
projects: any;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
isDragStarted?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
const {
|
||||
issues,
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
order_by,
|
||||
@ -199,6 +216,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
projects,
|
||||
enableQuickIssueCreate,
|
||||
isDragStarted,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const { issueKanBanView: issueKanBanViewStore } = useMobxStore();
|
||||
@ -208,12 +227,14 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
{group_by && group_by === "project" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projects}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -223,18 +244,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "state" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={states}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -244,18 +269,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "state_detail.group" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={stateGroups}
|
||||
listKey={`key`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -265,18 +294,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "priority" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={priorities}
|
||||
listKey={`key`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -286,18 +319,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "labels" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -307,18 +344,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "assignees" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={members ? [...members, { id: "None", display_name: "None" }] : members}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -328,18 +369,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "created_by" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={members}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
@ -349,6 +394,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isDragStarted={isDragStarted}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from "./block";
|
||||
export * from "./roots";
|
||||
export * from "./blocks-list";
|
||||
export * from "./inline-create-issue-form";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
@ -17,7 +17,7 @@ export interface IKanBanProperties {
|
||||
columnId: string;
|
||||
issue: IIssue;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
showEmptyGroup: boolean;
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{displayProperties && displayProperties?.state && (
|
||||
<IssuePropertyState
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.state_detail || null}
|
||||
value={issue?.state || null}
|
||||
onChange={handleState}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
@ -105,7 +105,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
)}
|
||||
|
||||
{/* label */}
|
||||
{displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && (
|
||||
{displayProperties && displayProperties?.labels && (
|
||||
<IssuePropertyLabels
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.labels || null}
|
||||
@ -116,7 +116,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
)}
|
||||
|
||||
{/* start date */}
|
||||
{displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && (
|
||||
{displayProperties && displayProperties?.start_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
@ -126,7 +126,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
)}
|
||||
|
||||
{/* target/due date */}
|
||||
{displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && (
|
||||
{displayProperties && displayProperties?.due_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
@ -135,6 +135,18 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* assignee */}
|
||||
{displayProperties && displayProperties?.assignee && (
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleAssignee}
|
||||
disabled={false}
|
||||
multiple
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{displayProperties && displayProperties?.estimate && (
|
||||
<IssuePropertyEstimates
|
||||
@ -176,18 +188,6 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* assignee */}
|
||||
{displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees.length > 0) && (
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleAssignee}
|
||||
disabled={false}
|
||||
multiple
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -8,23 +8,11 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
groupId?: string;
|
||||
subGroupId?: string;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
import { IIssue, IProject } from "types";
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetails } = props;
|
||||
@ -48,106 +36,117 @@ const Inputs = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const BoardInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData, groupId, subGroupId } = props;
|
||||
interface IKanBanQuickAddIssueForm {
|
||||
formKey: keyof IIssue;
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = observer((props) => {
|
||||
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
const { workspace: workspaceStore, project: projectStore } = useMobxStore();
|
||||
|
||||
const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null;
|
||||
const projectDetail: IProject | null =
|
||||
(workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null;
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
register,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IIssue>({ defaultValues });
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
if (isSubmitting || !groupId || !workspaceDetail || !projectDetail) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
const payload = createIssuePayload(workspaceDetail, projectDetail, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: groupId ?? null,
|
||||
sub_group_id: subGroupId ?? null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
quickAddCallback &&
|
||||
(await quickAddCallback(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
...payload,
|
||||
},
|
||||
viewId
|
||||
));
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
console.error(err);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.message || "Some error occurred. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOpen && (
|
||||
{isOpen ? (
|
||||
<div className="shadow-custom-shadow-sm">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex items-center gap-x-3 border-[0.5px] w-full border-t-0 border-custom-border-100 px-3 bg-custom-background-100"
|
||||
>
|
||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetail={projectDetail} />
|
||||
</form>
|
||||
<div className="text-xs italic text-custom-text-200 px-3 py-2">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full flex items-center text-custom-primary-100 p-3 py-3 cursor-pointer gap-2"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* {isOpen && (
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
@ -172,7 +171,7 @@ export const BoardInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,179 +1,48 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
// ui
|
||||
import { CycleIssueQuickActions } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { EIssueActions } from "../../types";
|
||||
// components
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
|
||||
export interface ICycleKanBanLayout {}
|
||||
|
||||
export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
|
||||
|
||||
// store
|
||||
const {
|
||||
project: projectStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
cycleIssue: cycleIssueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
cycleIssueKanBanView: cycleIssueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
const { cycleIssues: cycleIssueStore, cycleIssueKanBanView: cycleIssueKanBanViewStore } = useMobxStore();
|
||||
|
||||
const issues = cycleIssueStore?.getIssues;
|
||||
|
||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null;
|
||||
|
||||
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
|
||||
|
||||
const displayProperties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
||||
? "swimlanes"
|
||||
: "default";
|
||||
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
|
||||
// const onDragStart = () => {
|
||||
// setIsDragStarted(true);
|
||||
// };
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
setIsDragStarted(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (
|
||||
result.destination &&
|
||||
result.source &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index
|
||||
)
|
||||
return;
|
||||
|
||||
currentKanBanView === "default"
|
||||
? cycleIssueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||
: cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") cycleIssueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
cycleIssueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
cycleIssueStore.removeIssueFromCycle(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
cycleId.toString(),
|
||||
issue.bridge_id
|
||||
);
|
||||
}
|
||||
cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
|
||||
cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
|
||||
},
|
||||
[cycleIssueStore, issueDetailStore, cycleId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
// const estimates =
|
||||
// currentProjectDetails?.estimate !== null
|
||||
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||
// : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{cycleIssueStore.loader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`relative min-w-full min-h-full h-max bg-custom-background-90 px-3 horizontal-scroll-enable`}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{currentKanBanView === "default" ? (
|
||||
<KanBan
|
||||
issues={issues}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<CycleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
issues={issues}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<CycleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issueStore={cycleIssueStore}
|
||||
kanbanViewStore={cycleIssueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
viewId={cycleId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,176 +1,74 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { ModuleIssueQuickActions } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
|
||||
export interface IModuleKanBanLayout {}
|
||||
|
||||
export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
|
||||
|
||||
// store
|
||||
const {
|
||||
project: { workspaceProjects },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
moduleIssue: moduleIssueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
moduleIssues: moduleIssueStore,
|
||||
moduleIssueKanBanView: moduleIssueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = moduleIssueStore?.getIssues;
|
||||
// const handleIssues = useCallback(
|
||||
// (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => {
|
||||
// if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
||||
// if (action === "update") {
|
||||
// moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
// issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
// }
|
||||
// if (action === "delete") moduleIssueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
// if (action === "remove" && issue.bridge_id) {
|
||||
// moduleIssueStore.deleteIssue(group_by, null, issue);
|
||||
// moduleIssueStore.removeIssueFromModule(
|
||||
// workspaceSlug.toString(),
|
||||
// issue.project,
|
||||
// moduleId.toString(),
|
||||
// issue.bridge_id
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug]
|
||||
// );
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null;
|
||||
|
||||
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
|
||||
|
||||
const displayProperties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
||||
? "swimlanes"
|
||||
: "default";
|
||||
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
|
||||
// const onDragStart = () => {
|
||||
// setIsDragStarted(true);
|
||||
// };
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
setIsDragStarted(false);
|
||||
if (!result) return;
|
||||
|
||||
if (
|
||||
result.destination &&
|
||||
result.source &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index
|
||||
)
|
||||
return;
|
||||
|
||||
currentKanBanView === "default"
|
||||
? moduleIssueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||
: moduleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") moduleIssueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
moduleIssueStore.deleteIssue(group_by, null, issue);
|
||||
moduleIssueStore.removeIssueFromModule(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
moduleId.toString(),
|
||||
issue.bridge_id
|
||||
);
|
||||
}
|
||||
moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, issue.id, moduleId, issue.bridge_id);
|
||||
},
|
||||
[moduleIssueStore, issueDetailStore, moduleId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
// const estimates =
|
||||
// currentProjectDetails?.estimate !== null
|
||||
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||
// : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{moduleIssueStore.loader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`relative min-w-full min-h-full h-max bg-custom-background-90 px-3 horizontal-scroll-enable`}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{currentKanBanView === "default" ? (
|
||||
<KanBan
|
||||
issues={issues}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ModuleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
issues={issues}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ModuleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issueStore={moduleIssueStore}
|
||||
kanbanViewStore={moduleIssueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
viewId={moduleId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ import { Spinner } from "@plane/ui";
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
|
||||
export interface IProfileIssuesKanBanLayout {}
|
||||
|
||||
@ -71,14 +72,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete") => {
|
||||
(sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === "update") {
|
||||
if (action === EIssueActions.UPDATE) {
|
||||
profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") profileIssuesStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
if (action === EIssueActions.DELETE) profileIssuesStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
},
|
||||
[profileIssuesStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
@ -104,7 +105,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{currentKanBanView === "default" ? (
|
||||
<KanBan
|
||||
issues={issues}
|
||||
issues={{}}
|
||||
issueIds={[]}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -112,8 +114,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
@ -130,7 +132,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
issues={issues}
|
||||
issues={{}}
|
||||
issueIds={[]}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -138,8 +141,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
|
@ -1,18 +1,14 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
|
||||
export interface IKanBanLayout {}
|
||||
|
||||
@ -21,149 +17,31 @@ export const KanBanLayout: React.FC = observer(() => {
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
||||
|
||||
const {
|
||||
project: { workspaceProjects },
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
issue: issueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
projectIssues: issueStore,
|
||||
issueKanBanView: issueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = issueStore?.getIssues;
|
||||
|
||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null;
|
||||
|
||||
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
|
||||
|
||||
const displayProperties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
||||
? "swimlanes"
|
||||
: "default";
|
||||
|
||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||
|
||||
const onDragStart = () => {
|
||||
setIsDragStarted(true);
|
||||
};
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
setIsDragStarted(false);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (
|
||||
result.destination &&
|
||||
result.source &&
|
||||
result.source.droppableId &&
|
||||
result.destination.droppableId &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index
|
||||
)
|
||||
return;
|
||||
|
||||
currentKanBanView === "default"
|
||||
? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === "update") {
|
||||
issueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") issueStore.deleteIssue(group_by, sub_group_by, issue);
|
||||
await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
},
|
||||
[issueStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||
await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
// const estimates =
|
||||
// currentProjectDetails?.estimate !== null
|
||||
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||
// : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueStore.loader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative min-w-full min-h-full h-max bg-custom-background-90 px-3 horizontal-scroll-enable">
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||
{currentKanBanView === "default" ? (
|
||||
<KanBan
|
||||
issues={issues}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
enableQuickIssueCreate
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
issues={issues}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(sub_group_by, group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={workspaceProjects}
|
||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||
isDragStarted={isDragStarted}
|
||||
/>
|
||||
)}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issueStore={issueStore}
|
||||
kanbanViewStore={issueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,107 +1,47 @@
|
||||
import React from "react";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { KanBanSwimLanes } from "../swimlanes";
|
||||
import { KanBan } from "../default";
|
||||
import { useRouter } from "next/router";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
// constant
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
|
||||
// components
|
||||
import { BaseKanBanRoot } from "../base-kanban-root";
|
||||
|
||||
export interface IViewKanBanLayout {}
|
||||
|
||||
export const ProjectViewKanBanLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
||||
|
||||
const {
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
issue: issueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueKanBanView: issueKanBanViewStore,
|
||||
}: RootStore = useMobxStore();
|
||||
viewIssues: projectViewIssuesStore,
|
||||
issueKanBanView: projectViewIssueKanBanViewStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = issueStore?.getIssues;
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
|
||||
await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const display_properties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
||||
? "swimlanes"
|
||||
: "default";
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
if (!result) return;
|
||||
|
||||
if (
|
||||
result.destination &&
|
||||
result.source &&
|
||||
result.destination.droppableId === result.source.droppableId &&
|
||||
result.destination.index === result.source.index
|
||||
)
|
||||
return;
|
||||
|
||||
currentKanBanView === "default"
|
||||
? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||
await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => {
|
||||
issueStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
// const labels = projectStore?.projectLabels || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStateStore?.projectStates || null;
|
||||
const estimates = null;
|
||||
|
||||
return null;
|
||||
|
||||
// return (
|
||||
// <div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||
// <DragDropContext onDragEnd={onDragEnd}>
|
||||
// {currentKanBanView === "default" ? (
|
||||
// <KanBan
|
||||
// issues={issues}
|
||||
// sub_group_by={sub_group_by}
|
||||
// group_by={group_by}
|
||||
// handleIssues={updateIssue}
|
||||
// display_properties={display_properties}
|
||||
// kanBanToggle={() => {}}
|
||||
// handleKanBanToggle={() => {}}
|
||||
// states={states}
|
||||
// stateGroups={stateGroups}
|
||||
// priorities={priorities}
|
||||
// labels={labels}
|
||||
// members={members}
|
||||
// projects={projects}
|
||||
// estimates={estimates}
|
||||
// />
|
||||
// ) : (
|
||||
// <KanBanSwimLanes
|
||||
// issues={issues}
|
||||
// sub_group_by={sub_group_by}
|
||||
// group_by={group_by}
|
||||
// handleIssues={updateIssue}
|
||||
// display_properties={display_properties}
|
||||
// kanBanToggle={() => {}}
|
||||
// handleKanBanToggle={() => {}}
|
||||
// states={states}
|
||||
// stateGroups={stateGroups}
|
||||
// priorities={priorities}
|
||||
// labels={labels}
|
||||
// members={members}
|
||||
// projects={projects}
|
||||
// estimates={estimates}
|
||||
// />
|
||||
// )}
|
||||
// </DragDropContext>
|
||||
// </div>
|
||||
// );
|
||||
return (
|
||||
<BaseKanBanRoot
|
||||
issueActions={issueActions}
|
||||
issueStore={projectViewIssuesStore}
|
||||
kanbanViewStore={projectViewIssueKanBanViewStore}
|
||||
showLoader={true}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -6,11 +6,14 @@ import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
||||
import { KanBan } from "./default";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
|
||||
// constants
|
||||
import { getValueFromObject } from "constants/issue";
|
||||
import { EIssueActions } from "../types";
|
||||
|
||||
interface ISubGroupSwimlaneHeader {
|
||||
issues: any;
|
||||
issues: IIssueResponse;
|
||||
issueIds: any;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
list: any;
|
||||
@ -20,6 +23,7 @@ interface ISubGroupSwimlaneHeader {
|
||||
}
|
||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
issues,
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
list,
|
||||
@ -29,9 +33,9 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
}) => {
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
let issueCount = 0;
|
||||
issues &&
|
||||
Object.keys(issues)?.forEach((_issueKey: any) => {
|
||||
issueCount += issues?.[_issueKey]?.[column_id]?.length || 0;
|
||||
issueIds &&
|
||||
Object.keys(issueIds)?.forEach((_issueKey: any) => {
|
||||
issueCount += issueIds?.[_issueKey]?.[column_id]?.length || 0;
|
||||
});
|
||||
return issueCount;
|
||||
};
|
||||
@ -58,6 +62,8 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
};
|
||||
|
||||
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
issues: IIssueResponse;
|
||||
issueIds: any;
|
||||
order_by: string | null;
|
||||
showEmptyGroup: boolean;
|
||||
states: IState[] | null;
|
||||
@ -66,15 +72,9 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
labels: IIssueLabel[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
issues: any;
|
||||
handleIssues: (
|
||||
sub_group_by: string | null,
|
||||
group_by: string | null,
|
||||
issue: IIssue,
|
||||
action: "update" | "delete"
|
||||
) => void;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
isDragStarted?: boolean;
|
||||
@ -82,6 +82,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
const {
|
||||
issues,
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
order_by,
|
||||
@ -104,9 +105,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
let issueCount = 0;
|
||||
issues?.[column_id] &&
|
||||
Object.keys(issues?.[column_id])?.forEach((_list: any) => {
|
||||
issueCount += issues?.[column_id]?.[_list]?.length || 0;
|
||||
issueIds?.[column_id] &&
|
||||
Object.keys(issueIds?.[column_id])?.forEach((_list: any) => {
|
||||
issueCount += issueIds?.[column_id]?.[_list]?.length || 0;
|
||||
});
|
||||
return issueCount;
|
||||
};
|
||||
@ -134,7 +135,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
{!kanBanToggle?.subgroupByIssuesVisibility.includes(getValueFromObject(_list, listKey) as string) && (
|
||||
<div className="relative">
|
||||
<KanBan
|
||||
issues={issues?.[getValueFromObject(_list, listKey) as string]}
|
||||
issues={issues}
|
||||
issueIds={issueIds?.[getValueFromObject(_list, listKey) as string]}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -163,18 +165,14 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
});
|
||||
|
||||
export interface IKanBanSwimLanes {
|
||||
issues: any;
|
||||
issues: IIssueResponse;
|
||||
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
order_by: string | null;
|
||||
handleIssues: (
|
||||
sub_group_by: string | null,
|
||||
group_by: string | null,
|
||||
issue: IIssue,
|
||||
action: "update" | "delete"
|
||||
) => void;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
showEmptyGroup: boolean;
|
||||
@ -190,6 +188,7 @@ export interface IKanBanSwimLanes {
|
||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
const {
|
||||
issues,
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
order_by,
|
||||
@ -214,6 +213,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "project" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={projects}
|
||||
@ -226,6 +226,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "state" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={states}
|
||||
@ -238,6 +239,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "state_detail.group" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={stateGroups}
|
||||
@ -250,6 +252,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "priority" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={priorities}
|
||||
@ -262,6 +265,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "labels" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
|
||||
@ -274,6 +278,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "assignees" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={members ? [...members, { id: "None", display_name: "None" }] : members}
|
||||
@ -286,6 +291,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{group_by && group_by === "created_by" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={members}
|
||||
@ -299,6 +305,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "project" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -323,6 +330,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "state" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -347,6 +355,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "state" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -371,6 +380,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "state_detail.group" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -395,6 +405,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "priority" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -419,6 +430,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "labels" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -443,6 +455,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "assignees" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
@ -467,6 +480,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
{sub_group_by && sub_group_by === "created_by" && (
|
||||
<SubGroupSwimlane
|
||||
issues={issues}
|
||||
issueIds={issueIds}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
order_by={order_by}
|
||||
|
120
web/components/issues/issue-layouts/list/base-list-root.tsx
Normal file
120
web/components/issues/issue-layouts/list/base-list-root.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { List } from "./default";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue";
|
||||
import { FC } from "react";
|
||||
import { IIssue, IProject } from "types";
|
||||
import { IProjectStore } from "store/project";
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { IQuickActionProps } from "./list-view-types";
|
||||
import {
|
||||
ICycleIssuesFilterStore,
|
||||
ICycleIssuesStore,
|
||||
IModuleIssuesFilterStore,
|
||||
IModuleIssuesStore,
|
||||
IProjectIssuesFilterStore,
|
||||
IProjectIssuesStore,
|
||||
IViewIssuesFilterStore,
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { IIssueResponse } from "store/issues/types";
|
||||
|
||||
enum EIssueActions {
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
REMOVE = "remove",
|
||||
}
|
||||
|
||||
interface IBaseListRoot {
|
||||
issueFilterStore:
|
||||
| IProjectIssuesFilterStore
|
||||
| IModuleIssuesFilterStore
|
||||
| ICycleIssuesFilterStore
|
||||
| IViewIssuesFilterStore;
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
issueActions: {
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => void;
|
||||
[EIssueActions.UPDATE]?: (group_by: string | null, issue: IIssue) => void;
|
||||
[EIssueActions.REMOVE]?: (group_by: string | null, issue: IIssue) => void;
|
||||
};
|
||||
getProjects: (projectStore: IProjectStore) => IProject[] | null;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
const { issueFilterStore, issueStore, QuickActions, issueActions, getProjects, viewId } = props;
|
||||
|
||||
const {
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectLabel: { projectLabels },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueIds = issueStore.getIssuesIds || [];
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
const displayFilters = issueFilterStore?.issueFilters?.displayFilters;
|
||||
const group_by = displayFilters?.group_by || null;
|
||||
const showEmptyGroup = displayFilters?.show_empty_groups ?? false;
|
||||
|
||||
const displayProperties = issueFilterStore?.issueFilters?.displayProperties;
|
||||
|
||||
const states = projectStateStore?.projectStates;
|
||||
const priorities = ISSUE_PRIORITIES;
|
||||
const labels = projectLabels;
|
||||
const stateGroups = ISSUE_STATE_GROUPS;
|
||||
const projects = getProjects(projectStore);
|
||||
const members = projectMembers?.map((m) => m.member) ?? null;
|
||||
const handleIssues = async (issue: IIssue, action: EIssueActions) => {
|
||||
if (issueActions[action]) {
|
||||
issueActions[action]!(group_by, issue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueStore.loader === "mutation" ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`relative w-full h-full bg-custom-background-90`}>
|
||||
<List
|
||||
issues={issues as unknown as IIssueResponse}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(group_by, issue) => (
|
||||
<QuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||
handleUpdate={
|
||||
issueActions[EIssueActions.UPDATE]
|
||||
? async (data) => handleIssues(data, EIssueActions.UPDATE)
|
||||
: undefined
|
||||
}
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
issueIds={issueIds}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={true}
|
||||
isReadonly={false}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,26 +1,27 @@
|
||||
// components
|
||||
import { KanBanProperties } from "./properties";
|
||||
import { ListProperties } from "./properties";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { Spinner, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayProperties } from "types";
|
||||
import { EIssueActions } from "../types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
columnId: string;
|
||||
|
||||
issue: IIssue;
|
||||
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
|
||||
handleIssues: (issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
isReadonly?: boolean;
|
||||
showEmptyGroup?: boolean;
|
||||
}
|
||||
|
||||
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
const { columnId, issue, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props;
|
||||
const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props;
|
||||
|
||||
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
|
||||
handleIssues(group_by, issueToUpdate, "update");
|
||||
handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -31,16 +32,18 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
{issue?.project_detail?.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||
)}
|
||||
|
||||
<IssuePeekOverview
|
||||
workspaceSlug={issue?.workspace_detail?.slug}
|
||||
projectId={issue?.project_detail?.id}
|
||||
issueId={issue?.id}
|
||||
isArchived={issue?.archived_at !== null}
|
||||
handleIssue={(issueToUpdate) => {
|
||||
handleIssues(!columnId && columnId === "null" ? null : columnId, issueToUpdate as IIssue, "update");
|
||||
handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE);
|
||||
}}
|
||||
>
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
@ -49,15 +52,22 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
</IssuePeekOverview>
|
||||
|
||||
<div className="ml-auto flex-shrink-0 flex items-center gap-2">
|
||||
<KanBanProperties
|
||||
columnId={columnId}
|
||||
issue={issue}
|
||||
isReadonly={isReadonly}
|
||||
handleIssues={updateIssue}
|
||||
displayProperties={displayProperties}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
/>
|
||||
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
|
||||
{!issue?.tempId ? (
|
||||
<>
|
||||
<ListProperties
|
||||
columnId={columnId}
|
||||
issue={issue}
|
||||
isReadonly={isReadonly}
|
||||
handleIssues={updateIssue}
|
||||
displayProperties={displayProperties}
|
||||
/>
|
||||
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-4 h-4">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -3,33 +3,34 @@ import { FC } from "react";
|
||||
import { IssueBlock } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayProperties } from "types";
|
||||
import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types";
|
||||
import { EIssueActions } from "../types";
|
||||
|
||||
interface Props {
|
||||
columnId: string;
|
||||
issues: IIssue[];
|
||||
issueIds: IGroupedIssues | TUnGroupedIssues | any;
|
||||
issues: IIssueResponse;
|
||||
isReadonly?: boolean;
|
||||
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
|
||||
handleIssues: (issue: IIssue, action: EIssueActions) => void;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
showEmptyGroup?: boolean;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
}
|
||||
|
||||
export const IssueBlocksList: FC<Props> = (props) => {
|
||||
const { columnId, issues, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props;
|
||||
const { columnId, issueIds, issues, handleIssues, quickActions, displayProperties, isReadonly } = props;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
|
||||
{issues && issues.length > 0 ? (
|
||||
issues.map((issue) => (
|
||||
{issueIds && issueIds.length > 0 ? (
|
||||
issueIds.map((issueId: string) => (
|
||||
<IssueBlock
|
||||
key={issue.id}
|
||||
key={issues[issueId].id}
|
||||
columnId={columnId}
|
||||
issue={issue}
|
||||
issue={issues[issueId]}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
isReadonly={isReadonly}
|
||||
displayProperties={displayProperties}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
@ -1,243 +1,321 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { ListGroupByHeaderRoot } from "./headers/group-by-root";
|
||||
import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues";
|
||||
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
|
||||
// types
|
||||
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||
import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||
import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types";
|
||||
import { EIssueActions } from "../types";
|
||||
// constants
|
||||
import { getValueFromObject } from "constants/issue";
|
||||
|
||||
export interface IGroupByList {
|
||||
issueIds: IGroupedIssues | TUnGroupedIssues | any;
|
||||
issues: any;
|
||||
group_by: string | null;
|
||||
list: any;
|
||||
isReadonly?: boolean;
|
||||
listKey: string;
|
||||
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
states: IState[] | null;
|
||||
is_list?: boolean;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
handleIssues: (issue: IIssue, action: EIssueActions) => Promise<void>;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
enableIssueQuickAdd: boolean;
|
||||
showEmptyGroup?: boolean;
|
||||
isReadonly: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
||||
const GroupByList: React.FC<IGroupByList> = (props) => {
|
||||
const {
|
||||
issueIds,
|
||||
issues,
|
||||
group_by,
|
||||
list,
|
||||
isReadonly,
|
||||
listKey,
|
||||
is_list = false,
|
||||
states,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
is_list = false,
|
||||
enableQuickIssueCreate,
|
||||
enableIssueQuickAdd,
|
||||
showEmptyGroup,
|
||||
isReadonly,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const prePopulateQuickAddData = (groupByKey: string | null, value: any) => {
|
||||
const defaultState = states?.find((state) => state.default);
|
||||
if (groupByKey === null) return { state: defaultState?.id };
|
||||
else {
|
||||
if (groupByKey === "state") return { state: groupByKey === "state" ? value : defaultState?.id };
|
||||
else return { state: defaultState?.id, [groupByKey]: value };
|
||||
}
|
||||
};
|
||||
|
||||
const validateEmptyIssueGroups = (issues: IIssue[]) => {
|
||||
const issuesCount = issues?.length || 0;
|
||||
if (!showEmptyGroup && issuesCount <= 0) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: any) => (
|
||||
<div key={getValueFromObject(_list, listKey) as string} className={`flex-shrink-0 flex flex-col`}>
|
||||
<div className="flex-shrink-0 w-full py-1 sticky top-0 z-[2] px-3 bg-custom-background-90">
|
||||
<ListGroupByHeaderRoot
|
||||
column_id={getValueFromObject(_list, listKey) as string}
|
||||
column_value={_list}
|
||||
group_by={group_by}
|
||||
issues_count={
|
||||
is_list ? issues?.length || 0 : issues?.[getValueFromObject(_list, listKey) as string]?.length || 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{issues && (
|
||||
<IssueBlocksList
|
||||
columnId={getValueFromObject(_list, listKey) as string}
|
||||
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
/>
|
||||
)}
|
||||
{enableQuickIssueCreate && (
|
||||
<ListInlineCreateIssueForm
|
||||
groupId={getValueFromObject(_list, listKey) as string}
|
||||
prePopulatedData={{
|
||||
[group_by!]: getValueFromObject(_list, listKey),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
list.map(
|
||||
(_list: any) =>
|
||||
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[getValueFromObject(_list, listKey) as string]) && (
|
||||
<div key={getValueFromObject(_list, listKey) as string} className={`flex-shrink-0 flex flex-col`}>
|
||||
<div className="flex-shrink-0 w-full py-1 sticky top-0 z-[2] px-3 bg-custom-background-90 border-b border-custom-border-200">
|
||||
<ListGroupByHeaderRoot
|
||||
column_id={getValueFromObject(_list, listKey) as string}
|
||||
column_value={_list}
|
||||
group_by={group_by}
|
||||
issues_count={
|
||||
is_list
|
||||
? issueIds?.length || 0
|
||||
: issueIds?.[getValueFromObject(_list, listKey) as string]?.length || 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{issues && (
|
||||
<IssueBlocksList
|
||||
columnId={getValueFromObject(_list, listKey) as string}
|
||||
issueIds={is_list ? issueIds || 0 : issueIds?.[getValueFromObject(_list, listKey) as string] || 0}
|
||||
issues={issues}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
isReadonly={isReadonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{enableIssueQuickAdd && (
|
||||
<div className="flex-shrink-0 w-full sticky bottom-0 z-[1]">
|
||||
<ListQuickAddIssueForm
|
||||
prePopulatedData={prePopulateQuickAddData(group_by, getValueFromObject(_list, listKey))}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: update all the types
|
||||
export interface IList {
|
||||
issues: any;
|
||||
issueIds: IGroupedIssues | TUnGroupedIssues | any;
|
||||
issues: IIssueResponse | undefined;
|
||||
group_by: string | null;
|
||||
isReadonly?: boolean;
|
||||
handleDragDrop?: (result: any) => void | undefined;
|
||||
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
|
||||
handleIssues: (issue: IIssue, action: EIssueActions) => Promise<void>;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
showEmptyGroup: boolean;
|
||||
enableIssueQuickAdd: boolean;
|
||||
isReadonly: boolean;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabel[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
stateGroups: any;
|
||||
priorities: any;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
showEmptyGroup?: boolean;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const List: React.FC<IList> = observer((props) => {
|
||||
export const List: React.FC<IList> = (props) => {
|
||||
const {
|
||||
issueIds,
|
||||
issues,
|
||||
group_by,
|
||||
isReadonly,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
displayProperties,
|
||||
showEmptyGroup,
|
||||
enableIssueQuickAdd,
|
||||
isReadonly,
|
||||
|
||||
states,
|
||||
stateGroups,
|
||||
priorities,
|
||||
labels,
|
||||
members,
|
||||
projects,
|
||||
stateGroups,
|
||||
priorities,
|
||||
showEmptyGroup,
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{group_by === null && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as TUnGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={[{ id: "null", title: "All Issues" }]}
|
||||
list={[{ id: `null`, title: `All Issues` }]}
|
||||
listKey={`id`}
|
||||
is_list={true}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
is_list
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "project" && projects && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={projects}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "state" && states && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={states}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "state_detail.group" && stateGroups && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={stateGroups}
|
||||
listKey={`key`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "priority" && priorities && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={priorities}
|
||||
listKey={`key`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "labels" && labels && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={[...labels, { id: "None", name: "None" }]}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "assignees" && members && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={[...members, { id: "None", display_name: "None" }]}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "created_by" && members && (
|
||||
<GroupByList
|
||||
issueIds={issueIds as IGroupedIssues}
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
list={members}
|
||||
listKey={`id`}
|
||||
states={states}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
isReadonly={isReadonly}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||
isReadonly={isReadonly}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -1,8 +1,5 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// services
|
||||
import { ModuleService } from "services/module.service";
|
||||
import { IssueService } from "services/issue";
|
||||
// lucide icons
|
||||
import { CircleDashed, Plus } from "lucide-react";
|
||||
// components
|
||||
@ -23,18 +20,15 @@ interface IHeaderGroupByCard {
|
||||
issuePayload: Partial<IIssue>;
|
||||
}
|
||||
|
||||
const moduleService = new ModuleService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }: IHeaderGroupByCard) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const verticalAlignPosition = false;
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
|
||||
|
||||
const renderExistingIssueModal = moduleId || cycleId;
|
||||
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true };
|
||||
@ -46,15 +40,15 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
|
||||
issues: data.map((i) => i.id),
|
||||
};
|
||||
|
||||
await moduleService
|
||||
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload)
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the module. Please try again.",
|
||||
})
|
||||
);
|
||||
// await moduleService
|
||||
// .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user)
|
||||
// .catch(() =>
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: "Selected issues could not be added to the module. Please try again.",
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||
@ -64,46 +58,27 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
|
||||
issues: data.map((i) => i.id),
|
||||
};
|
||||
|
||||
await issueService
|
||||
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
// await issueService
|
||||
// .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
|
||||
// .catch(() => {
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: "Selected issues could not be added to the cycle. Please try again.",
|
||||
// });
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal isOpen={isOpen} handleClose={() => setIsOpen(false)} prePopulateData={issuePayload} />
|
||||
{renderExistingIssueModal && (
|
||||
<ExistingIssuesListModal
|
||||
isOpen={openExistingIssueListModal}
|
||||
handleClose={() => setOpenExistingIssueListModal(false)}
|
||||
searchParams={ExistingIssuesListModalPayload}
|
||||
handleOnSubmit={moduleId ? handleAddIssuesToModule : handleAddIssuesToCycle}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex-shrink-0 relative flex gap-2 py-1.5 ${
|
||||
verticalAlignPosition ? `flex-col items-center w-11` : `flex-row items-center w-full`
|
||||
}`}
|
||||
>
|
||||
<div className="flex-shrink-0 relative flex gap-2 py-1.5 flex-row items-center w-full">
|
||||
<div className="flex-shrink-0 w-5 h-5 rounded-sm overflow-hidden flex justify-center items-center">
|
||||
{icon ? icon : <CircleDashed className="h-3.5 w-3.5" strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-1 ${verticalAlignPosition ? `flex-col` : `flex-row w-full`}`}>
|
||||
<div
|
||||
className={`font-medium line-clamp-1 text-custom-text-100 ${verticalAlignPosition ? `vertical-lr` : ``}`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div className={`text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}>
|
||||
{count || 0}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-row w-full">
|
||||
<div className="font-medium line-clamp-1 text-custom-text-100">{title}</div>
|
||||
<div className="text-sm font-medium text-custom-text-300 pl-2">{count || 0}</div>
|
||||
</div>
|
||||
|
||||
{renderExistingIssueModal ? (
|
||||
@ -130,6 +105,25 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
|
||||
<Plus width={14} strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isOpen}
|
||||
handleClose={() => setIsOpen(false)}
|
||||
handleSubmit={(data: Partial<IIssue>) => {
|
||||
console.log(data);
|
||||
return Promise.resolve();
|
||||
}}
|
||||
prePopulateData={issuePayload}
|
||||
/>
|
||||
|
||||
{renderExistingIssueModal && (
|
||||
<ExistingIssuesListModal
|
||||
isOpen={openExistingIssueListModal}
|
||||
handleClose={() => setOpenExistingIssueListModal(false)}
|
||||
searchParams={ExistingIssuesListModalPayload}
|
||||
handleOnSubmit={moduleId ? handleAddIssuesToModule : handleAddIssuesToCycle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -2,4 +2,4 @@ export * from "./roots";
|
||||
export * from "./block";
|
||||
export * from "./roots";
|
||||
export * from "./blocks-list";
|
||||
export * from "./inline-create-issue-form";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
@ -1,178 +0,0 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// helpers
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
groupId?: string;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetails } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-xs font-medium text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-3 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData, groupId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IIssue>({ defaultValues });
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
// hooks
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: groupId ?? null,
|
||||
sub_group_id: null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-custom-background-100">
|
||||
{isOpen && (
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="absolute flex items-center gap-x-3 border-[0.5px] w-full border-t-0 border-custom-border-100 px-3 bg-custom-background-100 shadow-custom-shadow-sm z-10"
|
||||
>
|
||||
<Inputs register={register} setFocus={setFocus} projectDetails={projectDetails} />
|
||||
</form>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<p className="text-xs ml-3 my-3 mt-14 italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isOpen && (
|
||||
<div className="w-full border-t-[0.5px] border-custom-border-200">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 p-3"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
6
web/components/issues/issue-layouts/list/list-view-types.d.ts
vendored
Normal file
6
web/components/issues/issue-layouts/list/list-view-types.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export interface IQuickActionProps {
|
||||
issue: IIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleUpdate?: (data: IIssue) => Promise<void>;
|
||||
handleRemoveFromView?: () => Promise<void>;
|
||||
}
|
@ -13,17 +13,16 @@ import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
|
||||
|
||||
export interface IKanBanProperties {
|
||||
export interface IListProperties {
|
||||
columnId: string;
|
||||
issue: IIssue;
|
||||
handleIssues: (group_by: string | null, issue: IIssue) => void;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
isReadonly?: boolean;
|
||||
showEmptyGroup?: boolean;
|
||||
}
|
||||
|
||||
export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly, showEmptyGroup } = props;
|
||||
export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly } = props;
|
||||
|
||||
const handleState = (state: IState) => {
|
||||
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id });
|
||||
@ -60,7 +59,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
{displayProperties && displayProperties?.state && (
|
||||
<IssuePropertyState
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.state_detail || null}
|
||||
value={issue?.state || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleState}
|
||||
disabled={isReadonly}
|
||||
@ -78,7 +77,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* label */}
|
||||
{displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && (
|
||||
{displayProperties && displayProperties?.labels && (
|
||||
<IssuePropertyLabels
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.labels || null}
|
||||
@ -89,7 +88,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* assignee */}
|
||||
{displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && (
|
||||
{displayProperties && displayProperties?.assignee && (
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.assignees || null}
|
||||
@ -101,7 +100,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* start date */}
|
||||
{displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && (
|
||||
{displayProperties && displayProperties?.start_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
@ -111,7 +110,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* target/due date */}
|
||||
{displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && (
|
||||
{displayProperties && displayProperties?.due_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
|
@ -0,0 +1,148 @@
|
||||
import { FC, useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// constants
|
||||
import { IIssue, IProject } from "types";
|
||||
// types
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
|
||||
interface IInputProps {
|
||||
formKey: string;
|
||||
register: any;
|
||||
setFocus: any;
|
||||
projectDetail: IProject | null;
|
||||
}
|
||||
const Inputs: FC<IInputProps> = (props) => {
|
||||
const { formKey, register, setFocus, projectDetail } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus(formKey);
|
||||
}, [formKey, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register(formKey, {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-3 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IListQuickAddIssueForm {
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props) => {
|
||||
const { prePopulatedData, quickAddCallback, viewId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const { workspace: workspaceStore, project: projectStore } = useMobxStore();
|
||||
|
||||
const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null;
|
||||
const projectDetail: IProject | null =
|
||||
(workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null;
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<IIssue>({ defaultValues });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceDetail || !projectDetail) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail, projectDetail, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload }, viewId));
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.message || "Some error occurred. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-custom-background-100 border-t border-b border-custom-border-200 ${
|
||||
errors && errors?.name && errors?.name?.message ? `border-red-500 bg-red-500/10` : ``
|
||||
}`}
|
||||
>
|
||||
{isOpen ? (
|
||||
<div className="shadow-custom-shadow-sm">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex items-center gap-x-3 border-[0.5px] w-full border-t-0 border-custom-border-100 px-3 bg-custom-background-100"
|
||||
>
|
||||
<Inputs formKey={"name"} register={register} setFocus={setFocus} projectDetail={projectDetail} />
|
||||
</form>
|
||||
<div className="text-xs italic text-custom-text-200 px-3 py-2">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full flex items-center text-custom-primary-100 p-3 py-3 cursor-pointer gap-2"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -4,73 +4,42 @@ import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { List } from "../default";
|
||||
import { ArchivedIssueQuickActions } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { BaseListRoot } from "../base-list-root";
|
||||
import { IProjectStore } from "store/project";
|
||||
import { EIssueActions } from "../../types";
|
||||
|
||||
export const ArchivedIssueListLayout: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
project: projectStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectEstimates: { projectEstimates },
|
||||
archivedIssues: archivedIssueStore,
|
||||
archivedIssueFilters: archivedIssueFiltersStore,
|
||||
} = useMobxStore();
|
||||
const { archivedIssues: archivedIssueStore, archivedIssueFilters: archivedIssueFiltersStore } = useMobxStore();
|
||||
|
||||
// derived values
|
||||
const issues = archivedIssueStore.getIssues;
|
||||
const displayProperties = archivedIssueFiltersStore?.userDisplayProperties || null;
|
||||
const group_by: string | null = archivedIssueFiltersStore?.userDisplayFilters?.group_by || null;
|
||||
const showEmptyGroup = archivedIssueFiltersStore?.userDisplayFilters?.show_empty_groups || false;
|
||||
const issueActions = {
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const handleIssues = (group_by: string | null, issue: IIssue, action: "delete" | "update") => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
if (action === "delete") {
|
||||
archivedIssueStore.deleteArchivedIssue(group_by === "null" ? null : group_by, null, issue);
|
||||
archivedIssueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
archivedIssueStore.deleteArchivedIssue(group_by, null, issue);
|
||||
},
|
||||
};
|
||||
|
||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
|
||||
const getProjects = (projectStore: IProjectStore) => {
|
||||
if (!workspaceSlug) return null;
|
||||
return projectStore?.projects[workspaceSlug.toString()] || null;
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
projectDetails?.estimate !== null ? projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null : null;
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-custom-background-90">
|
||||
<List
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
isReadonly
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(group_by, issue) => (
|
||||
<ArchivedIssueQuickActions issue={issue} handleDelete={async () => handleIssues(group_by, issue, "delete")} />
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
// return (
|
||||
// <BaseListRoot
|
||||
// issueFilterStore={archivedIssueFiltersStore}
|
||||
// issueStore={archivedIssueStore}
|
||||
// QuickActions={ArchivedIssueQuickActions}
|
||||
// issueActions={issueActions}
|
||||
// getProjects={getProjects}
|
||||
// />
|
||||
// );
|
||||
});
|
||||
|
@ -1,96 +1,52 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { List } from "../default";
|
||||
import { CycleIssueQuickActions } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { BaseListRoot } from "../base-list-root";
|
||||
import { IProjectStore } from "store/project";
|
||||
import { EIssueActions } from "../../types";
|
||||
|
||||
export interface ICycleListLayout {}
|
||||
|
||||
export const CycleListLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
|
||||
// store
|
||||
const {
|
||||
project: projectStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectEstimates: { projectEstimates },
|
||||
issueFilter: issueFilterStore,
|
||||
cycleIssue: cycleIssueStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
|
||||
const issues = cycleIssueStore?.getIssues;
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const displayProperties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
cycleIssueStore.updateIssueStructure(group_by, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue);
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
cycleIssueStore.deleteIssue(group_by, null, issue);
|
||||
cycleIssueStore.removeIssueFromCycle(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
cycleId.toString(),
|
||||
issue.bridge_id
|
||||
);
|
||||
}
|
||||
cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
|
||||
},
|
||||
[cycleIssueStore, issueDetailStore, cycleId, workspaceSlug]
|
||||
);
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
currentProjectDetails?.estimate !== null
|
||||
? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||
: null;
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
|
||||
cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
|
||||
},
|
||||
};
|
||||
const getProjects = (projectStore: IProjectStore) => {
|
||||
if (!workspaceSlug) return null;
|
||||
return projectStore?.projects[workspaceSlug] || null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-full bg-custom-background-90`}>
|
||||
<List
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(group_by, issue) => (
|
||||
<CycleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
|
||||
handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
</div>
|
||||
<BaseListRoot
|
||||
issueFilterStore={cycleIssueFilterStore}
|
||||
issueStore={cycleIssueStore}
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
getProjects={getProjects}
|
||||
viewId={cycleId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,96 +1,53 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { List } from "../default";
|
||||
import { ModuleIssueQuickActions } from "components/issues";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { BaseListRoot } from "../base-list-root";
|
||||
import { IProjectStore } from "store/project";
|
||||
|
||||
export interface IModuleListLayout {}
|
||||
|
||||
export const ModuleListLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
|
||||
|
||||
const {
|
||||
project: projectStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectEstimates: { projectEstimates },
|
||||
issueFilter: issueFilterStore,
|
||||
moduleIssue: moduleIssueStore,
|
||||
issueDetail: issueDetailStore,
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
|
||||
const issues = moduleIssueStore?.getIssues;
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const displayProperties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
if (action === "update") {
|
||||
moduleIssueStore.updateIssueStructure(group_by, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") moduleIssueStore.deleteIssue(group_by, null, issue);
|
||||
if (action === "remove" && issue.bridge_id) {
|
||||
moduleIssueStore.deleteIssue(group_by, null, issue);
|
||||
moduleIssueStore.removeIssueFromModule(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
moduleId.toString(),
|
||||
issue.bridge_id
|
||||
);
|
||||
}
|
||||
moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
|
||||
},
|
||||
[moduleIssueStore, issueDetailStore, moduleId, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
|
||||
},
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
currentProjectDetails?.estimate !== null
|
||||
? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||
: null;
|
||||
const getProjects = (projectStore: IProjectStore) => {
|
||||
if (!workspaceSlug) return null;
|
||||
return projectStore?.projects[workspaceSlug] || null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-custom-background-90">
|
||||
<List
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(group_by, issue) => (
|
||||
<ModuleIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
|
||||
handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
</div>
|
||||
<BaseListRoot
|
||||
issueFilterStore={moduleIssueFilterStore}
|
||||
issueStore={moduleIssueStore}
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
getProjects={getProjects}
|
||||
viewId={moduleId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,24 +1,19 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { List } from "../default";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
|
||||
export interface IProfileIssuesListLayout {}
|
||||
import { EIssueActions } from "../../types";
|
||||
import { IProjectStore } from "store/project";
|
||||
//components
|
||||
import { BaseListRoot } from "../base-list-root";
|
||||
|
||||
export const ProfileIssuesListLayout: FC = observer(() => {
|
||||
const {
|
||||
workspace: workspaceStore,
|
||||
projectState: projectStateStore,
|
||||
project: projectStore,
|
||||
projectMember: { projectMembers },
|
||||
profileIssueFilters: profileIssueFiltersStore,
|
||||
profileIssues: profileIssuesStore,
|
||||
issueDetail: issueDetailStore,
|
||||
@ -27,53 +22,29 @@ export const ProfileIssuesListLayout: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const issues = profileIssuesStore?.getIssues;
|
||||
|
||||
const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
|
||||
|
||||
const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(group_by: string | null, issue: IIssue, action: "update" | "delete") => {
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === "update") {
|
||||
profileIssuesStore.updateIssueStructure(group_by, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") profileIssuesStore.deleteIssue(group_by, null, issue);
|
||||
profileIssuesStore.updateIssueStructure(group_by, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[profileIssuesStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
|
||||
profileIssuesStore.deleteIssue(group_by, null, issue);
|
||||
},
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const labels = workspaceStore.workspaceLabels || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStore?.workspaceProjects || null;
|
||||
const getProjects = (projectStore: IProjectStore) => projectStore?.workspaceProjects || null;
|
||||
|
||||
return (
|
||||
<div className={`relative w-full h-full bg-custom-background-90`}>
|
||||
<List
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
|
||||
// return (
|
||||
// <BaseListRoot
|
||||
// issueFilterStore={profileIssueFiltersStore}
|
||||
// issueStore={profileIssuesStore}
|
||||
// QuickActions={ProjectIssueQuickActions}
|
||||
// issueActions={issueActions}
|
||||
// getProjects={getProjects}
|
||||
// />
|
||||
// );
|
||||
});
|
||||
|
@ -1,95 +1,46 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { List } from "../default";
|
||||
import { ProjectIssueQuickActions } from "components/issues";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { EIssueActions } from "../../types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { BaseListRoot } from "../base-list-root";
|
||||
import { IProjectStore } from "store/project";
|
||||
|
||||
export const ListLayout: FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
||||
// store
|
||||
const {
|
||||
project: projectStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectEstimates: { projectEstimates },
|
||||
issue: issueStore,
|
||||
issueDetail: issueDetailStore,
|
||||
issueFilter: issueFilterStore,
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { projectIssuesFilter: projectIssuesFilterStore, projectIssues: projectIssuesStore } = useMobxStore();
|
||||
|
||||
const issues = issueStore?.getIssues;
|
||||
|
||||
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
|
||||
const group_by: string | null = userDisplayFilters?.group_by || null;
|
||||
const displayProperties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const handleIssues = useCallback(
|
||||
(group_by: string | null, issue: IIssue, action: "update" | "delete") => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (action === "update") {
|
||||
issueStore.updateIssueStructure(group_by, null, issue);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
}
|
||||
if (action === "delete") issueStore.deleteIssue(group_by, null, issue);
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue);
|
||||
},
|
||||
[issueStore, issueDetailStore, workspaceSlug]
|
||||
);
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||
const estimates =
|
||||
currentProjectDetails?.estimate !== null
|
||||
? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||
: null;
|
||||
const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueStore.loader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative w-full h-full bg-custom-background-90">
|
||||
<List
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={(group_by, issue) => (
|
||||
<ProjectIssueQuickActions
|
||||
issue={issue}
|
||||
handleDelete={async () => handleIssues(group_by, issue, "delete")}
|
||||
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
|
||||
/>
|
||||
)}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={projectLabels}
|
||||
members={projectMembers?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
enableQuickIssueCreate
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
showEmptyGroup={userDisplayFilters.show_empty_groups}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<BaseListRoot
|
||||
issueFilterStore={projectIssuesFilterStore}
|
||||
issueStore={projectIssuesStore}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
getProjects={getProjects}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,57 +1,49 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { List } from "../default";
|
||||
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { useRouter } from "next/router";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { IProjectStore } from "store/project";
|
||||
import { IIssue } from "types";
|
||||
// components
|
||||
import { BaseListRoot } from "../base-list-root";
|
||||
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
|
||||
|
||||
export interface IViewListLayout {}
|
||||
|
||||
export const ProjectViewListLayout: React.FC = observer(() => {
|
||||
const {
|
||||
project: projectStore,
|
||||
issue: issueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
projectState: projectStateStore,
|
||||
}: RootStore = useMobxStore();
|
||||
const { viewIssues: projectViewIssueStore, viewIssuesFilter: projectViewIssueFilterStore }: RootStore =
|
||||
useMobxStore();
|
||||
|
||||
const issues = issueStore?.getIssues;
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
||||
const display_properties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const updateIssue = (group_by: string | null, issue: any) => {
|
||||
issueStore.updateIssueStructure(group_by, null, issue);
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
projectViewIssueStore.updateIssue(workspaceSlug, projectId, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
projectViewIssueStore.removeIssue(workspaceSlug, projectId, issue.id);
|
||||
},
|
||||
};
|
||||
|
||||
const states = projectStateStore?.projectStates || null;
|
||||
const priorities = ISSUE_PRIORITIES || null;
|
||||
// const labels = projectStore?.projectLabels || null;
|
||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||
const projects = projectStateStore?.projectStates || null;
|
||||
const estimates = null;
|
||||
const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects;
|
||||
|
||||
return null;
|
||||
|
||||
// return (
|
||||
// <div className={`relative w-full h-full bg-custom-background-90`}>
|
||||
// <List
|
||||
// issues={issues}
|
||||
// group_by={group_by}
|
||||
// handleIssues={updateIssue}
|
||||
// display_properties={display_properties}
|
||||
// states={states}
|
||||
// stateGroups={stateGroups}
|
||||
// priorities={priorities}
|
||||
// labels={labels}
|
||||
// members={members}
|
||||
// projects={projects}
|
||||
// estimates={estimates}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
return (
|
||||
<BaseListRoot
|
||||
issueFilterStore={projectViewIssueFilterStore}
|
||||
issueStore={projectViewIssueStore}
|
||||
QuickActions={ProjectIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
getProjects={getProjects}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ import { RootStore } from "store/root";
|
||||
export interface IIssuePropertyState {
|
||||
view?: "profile" | "workspace" | "project";
|
||||
projectId: string | null;
|
||||
value: IState;
|
||||
value: any | string | null;
|
||||
onChange: (state: IState) => void;
|
||||
disabled?: boolean;
|
||||
hideDropdownArrow?: boolean;
|
||||
@ -62,6 +62,9 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
projectStateStore.fetchProjectStates(workspaceSlug, projectId).then(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const selectedOption: IState | undefined =
|
||||
(projectStates && value && projectStates?.find((state) => state.id === value)) || undefined;
|
||||
|
||||
const dropdownOptions = projectStates?.map((state) => ({
|
||||
value: state.id,
|
||||
query: state.name,
|
||||
@ -91,10 +94,10 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
: dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={value?.name ?? ""} position="top">
|
||||
<Tooltip tooltipHeading="State" tooltipContent={selectedOption?.name ?? ""} position="top">
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
{value && <StateGroupIcon stateGroup={value.group} color={value.color} />}
|
||||
<span className="truncate line-clamp-1 inline-block w-auto max-w-[100px]">{value?.name ?? "State"}</span>
|
||||
{selectedOption && <StateGroupIcon stateGroup={selectedOption?.group as any} color={selectedOption?.color} />}
|
||||
<span className="truncate line-clamp-1 inline-block">{selectedOption?.name ?? "State"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
@ -104,8 +107,8 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
{workspaceSlug && projectId && (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`text-left w-auto max-w-full ${className}`}
|
||||
value={value.id}
|
||||
className={`flex-shrink-0 text-left w-auto max-w-full ${className}`}
|
||||
value={selectedOption?.id}
|
||||
onChange={(data: string) => {
|
||||
const selectedState = projectStates?.find((state) => state.id === data);
|
||||
if (selectedState) onChange(selectedState);
|
||||
|
@ -9,14 +9,9 @@ import { DeleteArchivedIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const ArchivedIssueQuickActions: React.FC<Props> = (props) => {
|
||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete } = props;
|
||||
|
||||
const router = useRouter();
|
||||
|
@ -10,16 +10,10 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleUpdate: (data: IIssue) => Promise<void>;
|
||||
handleRemoveFromCycle: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const CycleIssueQuickActions: React.FC<Props> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromCycle } = props;
|
||||
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -59,7 +53,7 @@ export const CycleIssueQuickActions: React.FC<Props> = (props) => {
|
||||
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
|
||||
data={issueToEdit}
|
||||
onSubmit={async (data) => {
|
||||
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
|
||||
if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" ellipsis>
|
||||
@ -92,7 +86,7 @@ export const CycleIssueQuickActions: React.FC<Props> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFromCycle();
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -10,16 +10,10 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleUpdate: (data: IIssue) => Promise<void>;
|
||||
handleRemoveFromModule: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromModule } = props;
|
||||
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -59,7 +53,7 @@ export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
|
||||
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
|
||||
data={issueToEdit}
|
||||
onSubmit={async (data) => {
|
||||
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
|
||||
if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" ellipsis>
|
||||
@ -92,7 +86,7 @@ export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveFromModule();
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -10,14 +10,9 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleUpdate: (data: IIssue) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate } = props;
|
||||
|
||||
const router = useRouter();
|
||||
@ -58,7 +53,7 @@ export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
|
||||
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
|
||||
data={issueToEdit}
|
||||
onSubmit={async (data) => {
|
||||
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
|
||||
if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data });
|
||||
}}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" ellipsis>
|
||||
|
@ -24,28 +24,29 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycleId: string;
|
||||
};
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
cycle: cycleStore,
|
||||
cycleIssue: cycleIssueStore,
|
||||
cycleIssueFilter: cycleIssueFilterStore,
|
||||
cycleIssues: { loader, getIssues, fetchIssues },
|
||||
cycleIssuesFilter: { issueFilters, fetchFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
useSWR(workspaceSlug && projectId && cycleId ? `CYCLE_FILTERS_AND_ISSUES_${cycleId.toString()}` : null, async () => {
|
||||
if (workspaceSlug && projectId && cycleId) {
|
||||
// fetching the project display filters and display properties
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
|
||||
// fetching the cycle filters
|
||||
await cycleIssueFilterStore.fetchCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||
|
||||
// fetching the cycle issues
|
||||
await cycleIssueStore.fetchIssues(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||
useSWR(
|
||||
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && cycleId) {
|
||||
await fetchFilters(workspaceSlug, projectId, cycleId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", cycleId);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined;
|
||||
const cycleStatus =
|
||||
@ -53,41 +54,35 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||
: "draft";
|
||||
|
||||
const issueCount = cycleIssueStore.getIssuesCount;
|
||||
|
||||
if (!cycleIssueStore.getIssues)
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||
|
||||
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
||||
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||
<CycleAppliedFiltersRoot />
|
||||
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
|
||||
<CycleEmptyState
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
cycleId={cycleId?.toString()}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
|
||||
{loader === "init-loader" ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* <CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} /> */}
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
@ -27,60 +27,47 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
};
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
moduleIssue: moduleIssueStore,
|
||||
moduleFilter: moduleIssueFilterStore,
|
||||
moduleIssues: { loader, getIssues, fetchIssues },
|
||||
moduleIssuesFilter: { issueFilters, fetchFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && moduleId ? `MODULE_FILTERS_AND_ISSUES_${moduleId.toString()}` : null,
|
||||
workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && moduleId) {
|
||||
// fetching the project display filters and display properties
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||
// fetching the module filters
|
||||
await moduleIssueFilterStore.fetchModuleFilters(workspaceSlug, projectId, moduleId);
|
||||
|
||||
// fetching the module issues
|
||||
await moduleIssueStore.fetchIssues(workspaceSlug, projectId, moduleId);
|
||||
await fetchFilters(workspaceSlug, projectId, moduleId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", moduleId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const issueCount = moduleIssueStore.getIssuesCount;
|
||||
|
||||
if (!moduleIssueStore.getIssues)
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
||||
<ModuleAppliedFiltersRoot />
|
||||
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
|
||||
<ModuleEmptyState
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
moduleId={moduleId?.toString()}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
|
||||
{loader === "init-loader" ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* <ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} /> */}
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ModuleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ModuleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ModuleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -17,48 +17,49 @@ import {
|
||||
import { Spinner } from "@plane/ui";
|
||||
|
||||
export const ProjectLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
const {
|
||||
projectIssues: { loader, getIssues, fetchIssues },
|
||||
projectIssuesFilter: { issueFilters, fetchFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
|
||||
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
|
||||
await fetchFilters(workspaceSlug, projectId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
|
||||
}
|
||||
});
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const issueCount = issueStore.getIssuesCount;
|
||||
|
||||
if (!issueStore.getIssues)
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
||||
<ProjectAppliedFiltersRoot />
|
||||
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
|
||||
<ProjectEmptyState />
|
||||
) : (
|
||||
<div className="w-full h-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
|
||||
{loader === "init-loader" ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && <ProjectEmptyState />} */}
|
||||
<div className="w-full h-full relative overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -19,65 +19,51 @@ import { Spinner } from "@plane/ui";
|
||||
|
||||
export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const { workspaceSlug, projectId, viewId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
projectViews: projectViewsStore,
|
||||
projectViewIssues: projectViewIssuesStore,
|
||||
projectViewFilters: projectViewFiltersStore,
|
||||
viewIssues: { loader, getIssues, fetchIssues },
|
||||
viewIssuesFilter: { issueFilters, fetchFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_FILTERS_AND_ISSUES_${viewId.toString()}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && viewId) {
|
||||
// fetching the project display filters and display properties
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
|
||||
|
||||
// fetching the view details
|
||||
await projectViewsStore.fetchViewDetails(workspaceSlug.toString(), projectId.toString(), viewId.toString());
|
||||
// fetching the view issues
|
||||
await projectViewIssuesStore.fetchViewIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
viewId.toString(),
|
||||
projectViewFiltersStore.storedFilters[viewId.toString()] ?? {}
|
||||
);
|
||||
}
|
||||
useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
|
||||
if (workspaceSlug && projectId && viewId) {
|
||||
await fetchFilters(workspaceSlug, projectId, viewId);
|
||||
// await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const issueCount = projectViewIssuesStore.getIssuesCount;
|
||||
|
||||
if (!projectViewIssuesStore.getIssues)
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full flex flex-col overflow-hidden">
|
||||
<ProjectViewAppliedFiltersRoot />
|
||||
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
|
||||
<ProjectViewEmptyState />
|
||||
) : (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout />
|
||||
) : null}
|
||||
|
||||
{loader === "init-loader" ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && <ProjectViewEmptyState />} */}
|
||||
<div className="w-full h-full relative overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ModuleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<ProjectViewCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<ProjectViewGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<ProjectViewSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,113 @@
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { SpreadsheetView } from "./spreadsheet-view";
|
||||
import { useCallback } from "react";
|
||||
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import {
|
||||
ICycleIssuesFilterStore,
|
||||
ICycleIssuesStore,
|
||||
IModuleIssuesFilterStore,
|
||||
IModuleIssuesStore,
|
||||
IProjectIssuesFilterStore,
|
||||
IProjectIssuesStore,
|
||||
IViewIssuesFilterStore,
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { EFilterType, TUnGroupedIssues } from "store/issues/types";
|
||||
|
||||
interface IBaseSpreadsheetRoot {
|
||||
issueFiltersStore:
|
||||
| IViewIssuesFilterStore
|
||||
| ICycleIssuesFilterStore
|
||||
| IModuleIssuesFilterStore
|
||||
| IProjectIssuesFilterStore;
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
viewId?: string;
|
||||
}
|
||||
|
||||
export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
const { issueFiltersStore, issueStore, viewId } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
const {
|
||||
issueDetail: issueDetailStore,
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
projectLabel: { projectLabels },
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const issuesResponse = issueStore.getIssues;
|
||||
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
|
||||
|
||||
const issues = issueIds?.map((id) => issuesResponse?.[id]);
|
||||
|
||||
const handleIssueAction = async (issue: IIssue, action: "copy" | "delete" | "edit") => {
|
||||
if (!workspaceSlug || !projectId || !user) return;
|
||||
|
||||
if (action === "delete") {
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
|
||||
// issueStore.removeIssueFromStructure(null, null, issue);
|
||||
} else if (action === "edit") {
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue);
|
||||
// issueStore.updateIssueStructure(null, null, issue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFiltersStore.updateFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EFilterType.DISPLAY_FILTERS,
|
||||
{
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
viewId
|
||||
);
|
||||
},
|
||||
[issueFiltersStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleUpdateIssue = useCallback(
|
||||
(issue: IIssue, data: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !user) return;
|
||||
|
||||
const payload = {
|
||||
...issue,
|
||||
...data,
|
||||
};
|
||||
|
||||
// TODO: add update logic from the new store
|
||||
// issueStore.updateIssueStructure(null, null, payload);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||
},
|
||||
[issueDetailStore, projectId, user, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issues={issues as IIssueUnGroupedStructure}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
labels={projectLabels || undefined}
|
||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
disableUserActions={false}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
enableQuickCreateIssue
|
||||
/>
|
||||
);
|
||||
});
|
@ -15,7 +15,7 @@ type Props = {
|
||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIssues }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -2,4 +2,4 @@ export * from "./columns";
|
||||
export * from "./roots";
|
||||
export * from "./spreadsheet-column";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./inline-create-issue-form";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
@ -6,19 +6,26 @@ import { PlusIcon } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// helpers
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssue, IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
formKey: keyof IIssue;
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
@ -48,17 +55,15 @@ const Inputs = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { prePopulatedData, groupId } = props;
|
||||
export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, groupId, subGroupId = null, prePopulatedData, quickAddCallback, viewId } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
|
||||
// store
|
||||
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
const { workspace: workspaceStore, project: projectStore } = useMobxStore();
|
||||
|
||||
const {
|
||||
reset,
|
||||
@ -82,7 +87,9 @@ export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// derived values
|
||||
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
|
||||
const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null;
|
||||
const projectDetail: IProject | null =
|
||||
(workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
@ -106,43 +113,70 @@ export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
// const onSubmitHandler = async (formData: IIssue) => {
|
||||
// if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
// // resetting the form so that user can add another issue quickly
|
||||
// reset({ ...defaultValues });
|
||||
|
||||
// const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
// ...(prePopulatedData ?? {}),
|
||||
// ...formData,
|
||||
// });
|
||||
|
||||
// try {
|
||||
// quickAddStore.createIssue(
|
||||
// workspaceSlug.toString(),
|
||||
// projectId.toString(),
|
||||
// {
|
||||
// group_id: groupId ?? null,
|
||||
// sub_group_id: null,
|
||||
// },
|
||||
// payload
|
||||
// );
|
||||
|
||||
// setToastAlert({
|
||||
// type: "success",
|
||||
// title: "Success!",
|
||||
// message: "Issue created successfully.",
|
||||
// });
|
||||
// } catch (err: any) {
|
||||
// Object.keys(err || {}).forEach((key) => {
|
||||
// const error = err?.[key];
|
||||
// const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: errorTitle || "Some error occurred. Please try again.",
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (isSubmitting || !workspaceDetail || !projectDetail) return;
|
||||
|
||||
// resetting the form so that user can add another issue quickly
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
const payload = createIssuePayload(workspaceDetail, projectDetail, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
try {
|
||||
quickAddStore.createIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
{
|
||||
group_id: groupId ?? null,
|
||||
sub_group_id: null,
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload } as IIssue, viewId));
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
} catch (err: any) {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
console.error(err);
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: err?.message || "Some error occurred. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -156,7 +190,7 @@ export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex border-[0.5px] border-t-0 border-custom-border-100 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10"
|
||||
>
|
||||
<Inputs register={register} setFocus={setFocus} projectDetails={projectDetails} />
|
||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetails={projectDetail} />
|
||||
</form>
|
||||
</div>
|
||||
)}
|
@ -1,70 +1,18 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SpreadsheetView } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||
// constants
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { cycleId } = router.query as { cycleId: string };
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
cycleIssue: cycleIssueStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = cycleIssueStore.getIssues;
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleUpdateIssue = useCallback(
|
||||
(issue: IIssue, data: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const payload = {
|
||||
...issue,
|
||||
...data,
|
||||
};
|
||||
|
||||
cycleIssueStore.updateIssueStructure(null, null, payload);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||
},
|
||||
[issueDetailStore, cycleIssueStore, projectId, workspaceSlug]
|
||||
);
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
|
||||
return (
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issues={issues as IIssueUnGroupedStructure}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
labels={projectLabels || undefined}
|
||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||
handleIssueAction={() => {}}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
disableUserActions={false}
|
||||
/>
|
||||
<BaseSpreadsheetRoot issueStore={cycleIssueStore} issueFiltersStore={cycleIssueFilterStore} viewId={cycleId} />
|
||||
);
|
||||
});
|
||||
|
@ -1,71 +1,18 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SpreadsheetView } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||
// constants
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
moduleIssue: moduleIssueStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = moduleIssueStore.getIssues;
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleUpdateIssue = useCallback(
|
||||
(issue: IIssue, data: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const payload = {
|
||||
...issue,
|
||||
...data,
|
||||
};
|
||||
|
||||
moduleIssueStore.updateIssueStructure(null, null, payload);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||
},
|
||||
[issueDetailStore, moduleIssueStore, projectId, workspaceSlug]
|
||||
);
|
||||
const { moduleId } = router.query as { moduleId: string };
|
||||
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
return (
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issues={issues as IIssueUnGroupedStructure}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
labels={projectLabels ?? undefined}
|
||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||
handleIssueAction={() => {}}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
disableUserActions={false}
|
||||
/>
|
||||
<BaseSpreadsheetRoot issueStore={moduleIssueStore} issueFiltersStore={moduleIssueFilterStore} viewId={moduleId} />
|
||||
);
|
||||
});
|
||||
|
@ -1,86 +1,11 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SpreadsheetView } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||
// constants
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
|
||||
|
||||
export const ProjectSpreadsheetLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issue: issueStore,
|
||||
issueFilter: issueFilterStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
user: userStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const user = userStore.currentUser;
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleIssueAction = async (issue: IIssue, action: "copy" | "delete" | "edit") => {
|
||||
if (!workspaceSlug || !projectId || !user) return;
|
||||
|
||||
if (action === "delete") {
|
||||
issueDetailStore.deleteIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
|
||||
issueStore.removeIssueFromStructure(null, null, issue);
|
||||
} else if (action === "edit") {
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue);
|
||||
issueStore.updateIssueStructure(null, null, issue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateIssue = useCallback(
|
||||
(issue: IIssue, data: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !user) return;
|
||||
|
||||
const payload = {
|
||||
...issue,
|
||||
...data,
|
||||
};
|
||||
|
||||
issueStore.updateIssueStructure(null, null, payload);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||
},
|
||||
[issueStore, issueDetailStore, projectId, user, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issues={issues as IIssueUnGroupedStructure}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
labels={projectLabels || undefined}
|
||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
disableUserActions={false}
|
||||
enableQuickCreateIssue
|
||||
/>
|
||||
);
|
||||
const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
|
||||
return <BaseSpreadsheetRoot issueStore={projectIssuesStore} issueFiltersStore={projectIssueFiltersStore} />;
|
||||
});
|
||||
|
@ -1,70 +1,11 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SpreadsheetView } from "components/issues";
|
||||
// types
|
||||
import { IIssue, IIssueDisplayFilterOptions } from "types";
|
||||
// constants
|
||||
import { IIssueUnGroupedStructure } from "store/issue";
|
||||
import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
|
||||
|
||||
export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
projectViewIssues: projectViewIssueStore,
|
||||
issueDetail: issueDetailStore,
|
||||
projectLabel: { projectLabels },
|
||||
projectMember: { projectMembers },
|
||||
projectState: projectStateStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const issues = projectViewIssueStore.getIssues;
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleUpdateIssue = useCallback(
|
||||
(issue: IIssue, data: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const payload = {
|
||||
...issue,
|
||||
...data,
|
||||
};
|
||||
|
||||
projectViewIssueStore.updateIssueStructure(null, null, payload);
|
||||
issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
||||
},
|
||||
[issueDetailStore, projectViewIssueStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issues={issues as IIssueUnGroupedStructure}
|
||||
members={projectMembers?.map((m) => m.member)}
|
||||
labels={projectLabels || undefined}
|
||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||
handleIssueAction={() => {}}
|
||||
handleUpdateIssue={handleUpdateIssue}
|
||||
disableUserActions={false}
|
||||
/>
|
||||
);
|
||||
const { viewIssues: projectViewIssuesStore, viewIssuesFilter: projectViewIssueFiltersStore } = useMobxStore();
|
||||
return <BaseSpreadsheetRoot issueStore={projectViewIssuesStore} issueFiltersStore={projectViewIssueFiltersStore} />;
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetInlineCreateIssueForm } from "components/issues";
|
||||
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
|
||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// types
|
||||
@ -19,6 +19,13 @@ type Props = {
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
quickAddCallback?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IIssue,
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
disableUserActions: boolean;
|
||||
enableQuickCreateIssue?: boolean;
|
||||
};
|
||||
@ -34,7 +41,8 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
states,
|
||||
handleIssueAction,
|
||||
handleUpdateIssue,
|
||||
openIssuesListModal,
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
disableUserActions,
|
||||
enableQuickCreateIssue,
|
||||
} = props;
|
||||
@ -132,7 +140,9 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="border-t border-custom-border-100">
|
||||
<div className="mb-3 z-50 sticky bottom-0 left-0">
|
||||
{enableQuickCreateIssue && <SpreadsheetInlineCreateIssueForm />}
|
||||
{enableQuickCreateIssue && (
|
||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* {!disableUserActions &&
|
||||
|
5
web/components/issues/issue-layouts/types.ts
Normal file
5
web/components/issues/issue-layouts/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum EIssueActions {
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
REMOVE = "remove",
|
||||
}
|
@ -109,7 +109,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
if (isArchived) await archivedIssuesStore.deleteArchivedIssue(workspaceSlug, projectId, issue!);
|
||||
else await issueStore.deleteIssue(workspaceSlug, projectId, issue!);
|
||||
else await issueStore.removeIssueFromStructure(workspaceSlug, projectId, issue!);
|
||||
const { query } = router;
|
||||
if (query.peekIssueId) {
|
||||
issueDetailStore.setPeekId(null);
|
||||
|
@ -39,12 +39,21 @@ export interface IssuesModalProps {
|
||||
| "cycle"
|
||||
)[];
|
||||
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
||||
handleSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
||||
}
|
||||
|
||||
const issueDraftService = new IssueDraftService();
|
||||
|
||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||
const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props;
|
||||
const {
|
||||
data,
|
||||
handleClose,
|
||||
isOpen,
|
||||
prePopulateData: prePopulateDataProps,
|
||||
fieldsToShow = ["all"],
|
||||
onSubmit,
|
||||
handleSubmit,
|
||||
} = props;
|
||||
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState(false);
|
||||
@ -186,18 +195,22 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
||||
await issueDetailStore
|
||||
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
||||
.then(async (res) => {
|
||||
issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||
if (handleSubmit) {
|
||||
await handleSubmit(res);
|
||||
} else {
|
||||
issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||
|
||||
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
|
||||
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
|
@ -173,12 +173,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
key={issue.id}
|
||||
value={issue}
|
||||
className={({ active, selected }) =>
|
||||
`group flex items-center justify-between gap-2 cursor-pointer select-none rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
`group flex items-center justify-between gap-2 cursor-pointer select-none rounded-md px-3 py-2 text-custom-text-200 w-full ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-grow truncate">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
@ -188,12 +188,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
<span className="flex-shrink-0 text-xs">
|
||||
{issue.project__identifier}-{issue.sequence_id}
|
||||
</span>{" "}
|
||||
{issue.name}
|
||||
<span className="truncate">{issue.name}</span>
|
||||
</div>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
className="group-hover:block hidden relative z-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
className="group-hover:block hidden flex-shrink-0 relative z-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
@ -49,9 +49,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
|
||||
const createModule = async (payload: Partial<IModule>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const selectedProjectId = payload.project ?? projectId.toString();
|
||||
await moduleStore
|
||||
.createModule(workspaceSlug.toString(), projectId.toString(), payload)
|
||||
.createModule(workspaceSlug.toString(), selectedProjectId, payload)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
|
||||
@ -72,9 +72,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
|
||||
const updateModule = async (payload: Partial<IModule>) => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
const selectedProjectId = payload.project ?? projectId.toString();
|
||||
await moduleStore
|
||||
.updateModuleDetails(workspaceSlug.toString(), projectId.toString(), data.id, payload)
|
||||
.updateModuleDetails(workspaceSlug.toString(), selectedProjectId, data.id, payload)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
|
||||
@ -99,7 +99,6 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
||||
const payload: Partial<IModule> = {
|
||||
...formData,
|
||||
};
|
||||
|
||||
if (!data) await createModule(payload);
|
||||
else await updateModule(payload);
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user