Merge branch 'develop' of github.com:makeplane/plane into chore/api_endpoints

This commit is contained in:
pablohashescobar 2023-11-23 15:09:19 +05:30
commit 2e513af4f3
194 changed files with 8101 additions and 4503 deletions

View File

@ -155,6 +155,16 @@ class ChangePasswordSerializer(serializers.Serializer):
""" """
old_password = serializers.CharField(required=True) old_password = serializers.CharField(required=True)
new_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): class ResetPasswordSerializer(serializers.Serializer):

View File

@ -131,21 +131,13 @@ class ChangePasswordEndpoint(BaseAPIView):
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)
if serializer.is_valid(): if serializer.is_valid():
# Check old password if not user.check_password(serializer.data.get("old_password")):
if not user.object.check_password(serializer.data.get("old_password")):
return Response( return Response(
{"old_password": ["Wrong password."]}, {"old_password": ["Wrong password."]},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# set_password also hashes the password that the user will get # set_password also hashes the password that the user will get
self.object.set_password(serializer.data.get("new_password")) user.set_password(serializer.data.get("new_password"))
self.object.save() user.save()
response = { return Response({"message": "Password updated successfully"}, status=status.HTTP_200_OK)
"status": "success",
"code": status.HTTP_200_OK,
"message": "Password updated successfully",
}
return Response(response)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -36,6 +36,8 @@ module.exports = {
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)", "custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)", "custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)", "custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
"onbording-shadow-sm": "var(--color-onboarding-shadow-sm)",
}, },
colors: { colors: {
custom: { custom: {
@ -192,6 +194,7 @@ module.exports = {
border: { border: {
100: convertToRGB("--color-onboarding-border-100"), 100: convertToRGB("--color-onboarding-border-100"),
200: convertToRGB("--color-onboarding-border-200"), 200: convertToRGB("--color-onboarding-border-200"),
300: convertToRGB("--color-onboarding-border-300"),
}, },
}, },
}, },
@ -372,8 +375,9 @@ module.exports = {
96: "21.6rem", 96: "21.6rem",
}, },
backgroundImage: { backgroundImage: {
"onboarding-gradient-primary": "var( --gradient-onboarding-primary)", "onboarding-gradient-100": "var( --gradient-onboarding-100)",
"onboarding-gradient-secondary": "var( --gradient-onboarding-secondary)", "onboarding-gradient-200": "var( --gradient-onboarding-200)",
"onboarding-gradient-300": "var( --gradient-onboarding-300)",
}, },
}, },
fontFamily: { fontFamily: {

View File

@ -17,7 +17,7 @@ export const Spinner: React.FC<ISpinner> = ({
aria-hidden="true" aria-hidden="true"
height={height} height={height}
width={width} 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" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -1,18 +1,17 @@
// react
import React, { useState } from "react"; import React, { useState } from "react";
// next
import { useRouter } from "next/router"; 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"; import { Button } from "@plane/ui";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// services // services
import { AuthService } from "services/auth.service"; 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 = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -20,19 +19,40 @@ type Props = {
}; };
const authService = new AuthService(); 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 { 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 router = useRouter();
const { setTheme } = useTheme();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleSignOut = async () => { const handleClose = () => {
setSwitchingAccount(false);
setIsDeactivating(false);
onClose();
};
const handleSwitchAccount = async () => {
setSwitchingAccount(true);
await authService await authService
.signOut() .signOut()
.then(() => { .then(() => {
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/"); router.push("/");
handleClose();
}) })
.catch(() => .catch(() =>
setToastAlert({ setToastAlert({
@ -40,33 +60,31 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
title: "Error!", title: "Error!",
message: "Failed to sign out. Please try again.", message: "Failed to sign out. Please try again.",
}) })
); )
.finally(() => setSwitchingAccount(false));
}; };
const handleDeleteAccount = async () => { const handleDeleteAccount = async () => {
setIsDeleteLoading(true); setIsDeactivating(true);
await userService
.deleteAccount() await deactivateAccount()
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Account deleted successfully.", message: "Account deleted successfully.",
}); });
handleClose();
router.push("/"); router.push("/");
}) })
.catch((err) => .catch((err) =>
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: err?.data?.error, message: err?.error,
}) })
); )
setIsDeleteLoading(false); .finally(() => setIsDeactivating(false));
};
const handleClose = () => {
onClose();
}; };
return ( 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="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className=""> <div className="">
<div className="flex items-center gap-x-4"> <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" /> <AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</div> </div>
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100"> <Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Not the right workspace? Deactivate account?
</Dialog.Title> </Dialog.Title>
</div> </div>
<div className="mt-6 px-4"> <div className="mt-6 px-4">
<ul className="text-onboarding-text-300 list-disc font-normal text-base"> <ul className="text-onboarding-text-300 list-disc font-normal text-base">
<li>Delete this account if you have another and wont use this account.</li> <li>Deactivate this account if you have another and won{"'"}t use this account.</li>
<li>Switch to another account if youd like to come back to this account another time.</li> <li>Switch to another account if you{"'"}d like to come back to this account another time.</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-4 p-4 mb-2 sm:px-6"> <div className="flex items-center justify-end gap-2 p-4 mb-2 sm:px-6">
<span className="text-sm font-medium hover:cursor-pointer" onClick={handleSignOut}> <Button variant="link-primary" onClick={handleSwitchAccount} loading={switchingAccount}>
Switch account {switchingAccount ? "Switching..." : "Switch account"}
</span> </Button>
<button <Button variant="outline-danger" onClick={handleDeleteAccount}>
className="py-1.5 px-3 font-medium rounded-sm text-red-500 border border-red-500 text-sm " {isDeactivating ? "Deactivating..." : "Deactivate account"}
onClick={handleDeleteAccount} </Button>
>
{isDeleteLoading ? "Deleting..." : "Delete account"}
</button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
@ -134,5 +149,3 @@ const DeleteAccountModal: React.FC<Props> = (props) => {
</Transition.Root> </Transition.Root>
); );
}; };
export default DeleteAccountModal;

View File

@ -1,17 +1,16 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { XCircle } from "lucide-react";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// components
import { AuthType } from "components/page-views";
// services // services
import { AuthService } from "services/auth.service"; import { AuthService } from "services/auth.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useTimer from "hooks/use-timer"; import useTimer from "hooks/use-timer";
// icons
import { XCircle } from "lucide-react";
import { useTheme } from "next-themes";
// types
type EmailCodeFormValues = { type EmailCodeFormValues = {
email: string; email: string;
key?: string; key?: string;
@ -20,7 +19,14 @@ type EmailCodeFormValues = {
const authService = new AuthService(); 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 [codeSent, setCodeSent] = useState(false);
const [codeResent, setCodeResent] = useState(false); const [codeResent, setCodeResent] = useState(false);
const [isCodeResending, setIsCodeResending] = useState(false); const [isCodeResending, setIsCodeResending] = useState(false);
@ -37,7 +43,6 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
setError, setError,
setValue, setValue,
getValues, getValues,
watch,
formState: { errors, isSubmitting, isValid, isDirty }, formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailCodeFormValues>({ } = useForm<EmailCodeFormValues>({
defaultValues: { defaultValues: {
@ -49,14 +54,13 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
reValidateMode: "onChange", reValidateMode: "onChange",
}); });
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode; const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting;
const onSubmit = async ({ email }: EmailCodeFormValues) => { const onSubmit = async ({ email }: EmailCodeFormValues) => {
setErrorResendingCode(false); setErrorResendingCode(false);
await authService await authService
.emailCode({ email }) .emailCode({ email })
.then((res) => { .then((res) => {
console.log(res);
setSentEmail(email); setSentEmail(email);
setValue("key", res.key); setValue("key", res.key);
setCodeSent(true); 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"> <h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-onboarding-text-100">
Lets get you prepped! {authType === "sign-in" ? "Get on your flight deck!" : "Lets get you prepped!"}
</h1> </h1>
{authType == "sign-up" ? (
<div>
<p className="text-center text-sm text-onboarding-text-200 mt-3"> <p className="text-center text-sm text-onboarding-text-200 mt-3">
This whole thing will take less than two minutes. This whole thing will take less than two minutes.
</p> </p>
<p className="text-center text-sm text-onboarding-text-200 mt-1">Promise!</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} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.token)} hasError={Boolean(errors.token)}
placeholder="get-set-fly" placeholder="gets-sets-flys"
className="border-onboarding-border-100 h-[46px] w-full" 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> </div>
</> </>
)} )}
@ -238,8 +278,8 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
> >
{isLoading ? "Signing in..." : "Next step"} {isLoading ? "Signing in..." : "Next step"}
</Button> </Button>
<div className="w-[70%] my-4 mx-auto"> <div className="w-3/4 my-4 mx-auto">
<p className="text-xs text-onboarding-text-300"> <p className="text-xs text-center text-onboarding-text-300">
When you click the button above, you agree with our{" "} When you click the button above, you agree with our{" "}
<a <a
href="https://plane.so/terms-and-conditions" href="https://plane.so/terms-and-conditions"

View File

@ -6,16 +6,18 @@ import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// images // images
import githubBlackImage from "/public/logos/github-black.png"; import githubLightModeImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png"; import githubDarkModeImage from "/public/logos/github-dark.svg";
import { AuthType } from "components/page-views";
export interface GithubLoginButtonProps { export interface GithubLoginButtonProps {
handleSignIn: React.Dispatch<string>; handleSignIn: React.Dispatch<string>;
clientId: string; clientId: string;
authType: AuthType;
} }
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => { export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const { handleSignIn, clientId } = props; const { handleSignIn, clientId, authType } = props;
// states // states
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined); const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
const [gitCode, setGitCode] = useState<null | string>(null); const [gitCode, setGitCode] = useState<null | string>(null);
@ -24,7 +26,7 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
query: { code }, query: { code },
} = useRouter(); } = useRouter();
// theme // theme
const { theme } = useTheme(); const { resolvedTheme } = useTheme();
useEffect(() => { useEffect(() => {
if (code && !gitCode) { if (code && !gitCode) {
@ -37,22 +39,23 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
setLoginCallBackURL(`${origin}/` as any); setLoginCallBackURL(`${origin}/` as any);
}, []); }, []);
return ( return (
<div className="w-full flex justify-center items-center"> <div className="w-full flex justify-center items-center">
<Link <Link
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`} href={`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
> >
<button <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 <Image
src={theme === "dark" ? githubWhiteImage : githubBlackImage} src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20} height={20}
width={20} width={20}
alt="GitHub Logo" 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> </button>
</Link> </Link>
</div> </div>

View File

@ -1,3 +1,4 @@
export * from "./deactivate-account-modal";
export * from "./email-code-form"; export * from "./email-code-form";
export * from "./email-password-form"; export * from "./email-password-form";
export * from "./email-forgot-password-form"; export * from "./email-forgot-password-form";

View File

@ -1,12 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui"; import { useTheme } from "next-themes";
// mobx store import Image from "next/image";
import { useMobxStore } from "lib/mobx/store-provider";
// react-hook-form
import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { Control, Controller, UseFormSetValue, UseFormWatch } from "react-hook-form";
// types
import { IWorkspace } from "types";
// icons
import { import {
BarChart2, BarChart2,
Briefcase, Briefcase,
@ -19,7 +14,16 @@ import {
PenSquare, PenSquare,
Search, Search,
Settings, Settings,
Bell,
} from "lucide-react"; } 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 = [ const workspaceLinks = [
{ {
@ -39,7 +43,7 @@ const workspaceLinks = [
name: "All Issues", name: "All Issues",
}, },
{ {
Icon: CheckCircle, Icon: Bell,
name: "Notifications", name: "Notifications",
}, },
]; ];
@ -89,22 +93,23 @@ const DummySidebar: React.FC<Props> = (props) => {
const { workspace: workspaceStore, user: userStore } = useMobxStore(); const { workspace: workspaceStore, user: userStore } = useMobxStore();
const workspace = workspaceStore.workspaces ? workspaceStore.workspaces[0] : null; const workspace = workspaceStore.workspaces ? workspaceStore.workspaces[0] : null;
const { resolvedTheme } = useTheme();
const handleZoomWorkspace = (value: string) => { const handleZoomWorkspace = (value: string) => {
// console.log(lastWorkspaceName,value); // console.log(lastWorkspaceName,value);
if (lastWorkspaceName === value) return; if (lastWorkspaceName === value) return;
lastWorkspaceName = value; lastWorkspaceName = value;
if (timer > 0) { if (timer > 0) {
timer += 2; timer += 2;
timer = Math.min(timer, 4); timer = Math.min(timer, 2);
} else { } else {
timer = 2; timer = 2;
timer = Math.min(timer, 4); timer = Math.min(timer, 2);
const interval = setInterval(() => { const interval = setInterval(() => {
if (timer < 0) { if (timer < 0) {
setValue!("name", lastWorkspaceName); setValue!("name", lastWorkspaceName);
clearInterval(interval); clearInterval(interval);
} }
console.log("timer", timer);
timer--; timer--;
}, 1000); }, 1000);
} }
@ -112,7 +117,7 @@ const DummySidebar: React.FC<Props> = (props) => {
useEffect(() => { useEffect(() => {
if (watch) { if (watch) {
watch(); watch("name");
} }
}); });
@ -126,10 +131,21 @@ const DummySidebar: React.FC<Props> = (props) => {
render={({ field: { value } }) => { render={({ field: { value } }) => {
if (value.length > 0) { if (value.length > 0) {
handleZoomWorkspace(value); handleZoomWorkspace(value);
} else {
lastWorkspaceName = "";
} }
return timer > 0 ? ( 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
<div className="bg-onboarding-background-100 w-full p-1 flex items-center"> 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"> <div className="flex flex-shrink-0">
<Avatar <Avatar
name={value.length > 0 ? value[0].toLocaleUpperCase() : "N"} name={value.length > 0 ? value[0].toLocaleUpperCase() : "N"}
@ -144,6 +160,7 @@ const DummySidebar: React.FC<Props> = (props) => {
<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> </div>
</div>
) : ( ) : (
<div className="flex transition-all w-full border border-transparent items-center gap-y-2 px-4 pt-6 truncate"> <div className="flex transition-all w-full border border-transparent items-center gap-y-2 px-4 pt-6 truncate">
<div className="flex flex-shrink-0"> <div className="flex flex-shrink-0">
@ -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={`flex items-center justify-between w-full px-1 mb-3 gap-2 mt-4 `}>
<div <div
className={`relative flex items-center justify-between w-full rounded gap-1 group 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`}> <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 <div
className={`flex items-center justify-center rounded flex-shrink-0 p-2 outline-none 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" /> <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="px-3">
{" "} {" "}
<div className="w-4/5 flex items-center text-base font-medium text-custom-text-200 mb-3 justify-between"> <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" /> <ChevronDown className="h-4 w-4" />
</div> </div>
{projectLinks.map((link) => ( {projectLinks.map((link) => (
<a className="block w-full"> <a className="block ml-6 w-full">
<div <div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-base font-medium outline-none 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 text-custom-sidebar-text-200 focus:bg-custom-sidebar-background-80

View File

@ -2,17 +2,17 @@ import React from "react";
const OnboardingStepIndicator = ({ step }: { step: number }) => ( const OnboardingStepIndicator = ({ step }: { step: number }) => (
<div className="flex items-center justify-center"> <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={`h-1 w-14 -ml-1 ${step >= 2 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
<div <div
className={` z-10 -ml-1 rounded-full ${ 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={`h-1 w-14 -ml-1 ${step >= 3 ? "bg-custom-primary-100" : "bg-onboarding-background-100"}`} />
<div <div
className={`rounded-full -ml-1 z-10 ${ 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> </div>

View File

@ -13,7 +13,9 @@ import JoinProjectImg from "public/auth/project-not-authorized.svg";
export const JoinProject: React.FC = () => { export const JoinProject: React.FC = () => {
const [isJoiningProject, setIsJoiningProject] = useState(false); const [isJoiningProject, setIsJoiningProject] = useState(false);
const { project: projectStore } = useMobxStore(); const {
user: { joinProject },
} = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -23,7 +25,7 @@ export const JoinProject: React.FC = () => {
setIsJoiningProject(true); setIsJoiningProject(true);
projectStore.joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => { joinProject(workspaceSlug.toString(), [projectId.toString()]).finally(() => {
setIsJoiningProject(false); setIsJoiningProject(false);
}); });
}; };

View File

@ -4,7 +4,6 @@ import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// components // components
import { CommandModal, ShortcutsModal } from "components/command-palette"; import { CommandModal, ShortcutsModal } from "components/command-palette";
import { BulkDeleteIssuesModal } from "components/core"; import { BulkDeleteIssuesModal } from "components/core";
@ -30,7 +29,11 @@ export const CommandPalette: FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
// store // store
const { commandPalette, theme: themeStore } = useMobxStore(); const {
commandPalette,
theme: { toggleSidebar },
user: { currentUser },
} = useMobxStore();
const { const {
toggleCommandPaletteModal, toggleCommandPaletteModal,
isCreateIssueModalOpen, isCreateIssueModalOpen,
@ -52,9 +55,6 @@ export const CommandPalette: FC = observer(() => {
isDeleteIssueModalOpen, isDeleteIssueModalOpen,
toggleDeleteIssueModal, toggleDeleteIssueModal,
} = commandPalette; } = commandPalette;
const { toggleSidebar } = themeStore;
const { user } = useUser();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -153,7 +153,7 @@ export const CommandPalette: FC = observer(() => {
return () => document.removeEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]); }, [handleKeyDown]);
if (!user) return null; if (!currentUser) return null;
return ( return (
<> <>
@ -223,7 +223,7 @@ export const CommandPalette: FC = observer(() => {
onClose={() => { onClose={() => {
toggleBulkDeleteIssueModal(false); toggleBulkDeleteIssueModal(false);
}} }}
user={user} user={currentUser}
/> />
<CommandModal /> <CommandModal />
</> </>

View File

@ -1,11 +1,9 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr"; // mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hook // hook
import useEstimateOption from "hooks/use-estimate-option"; import useEstimateOption from "hooks/use-estimate-option";
// services
import { IssueLabelService } from "services/issue";
// icons // icons
import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui";
import { import {
@ -29,11 +27,7 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper"; import { capitalizeFirstLetter } from "helpers/string.helper";
// types // types
import { IIssueActivity } from "types"; import { IIssueActivity } from "types";
// fetch-keys import { useEffect } from "react";
import { WORKSPACE_LABELS } from "constants/fetch-keys";
// services
const issueLabelService = new IssueLabelService();
const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter(); const router = useRouter();
@ -44,7 +38,11 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
<a <a
aria-disabled={activity.issue === null} aria-disabled={activity.issue === null}
href={`${ 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"} target={activity.issue === null ? "_self" : "_blank"}
rel={activity.issue === null ? "" : "noopener noreferrer"} rel={activity.issue === null ? "" : "noopener noreferrer"}
@ -63,7 +61,9 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
return ( return (
<a <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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center hover:underline" 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 LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
const router = useRouter(); const {
const { workspaceSlug } = router.query; workspace: { labels, fetchWorkspaceLabels },
} = useMobxStore();
const { data: labels } = useSWR( const workspaceLabels = labels[workspaceSlug];
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
workspaceSlug ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null useEffect(() => {
); if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);
}, [fetchWorkspaceLabels, workspaceLabels, workspaceSlug]);
return ( return (
<span <span
className="h-1.5 w-1.5 rounded-full flex-shrink-0" className="h-1.5 w-1.5 rounded-full flex-shrink-0"
style={{ style={{
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000", backgroundColor: workspaceLabels?.find((l) => l.id === labelId)?.color ?? "#000000",
}} }}
aria-hidden="true" aria-hidden="true"
/> />
); );
}; });
const EstimatePoint = ({ point }: { point: string }) => { const EstimatePoint = ({ point }: { point: string }) => {
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point)); const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
@ -243,24 +245,6 @@ const activityDetails: {
}, },
icon: <CopyPlus size={12} color="#6b7280" />, 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: { cycles: {
message: (activity, showIssue, workspaceSlug) => { message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created") if (activity.verb === "created")
@ -365,13 +349,13 @@ const activityDetails: {
icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />, icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
}, },
labels: { labels: {
message: (activity, showIssue) => { message: (activity, showIssue, workspaceSlug) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
added a new label{" "} 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"> <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 ?? ""} /> <LabelPill labelId={activity.new_identifier ?? ""} workspaceSlug={workspaceSlug} />
<span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span> <span className="font-medium flex-shrink truncate text-custom-text-100">{activity.new_value}</span>
</span> </span>
{showIssue && ( {showIssue && (
@ -387,7 +371,7 @@ const activityDetails: {
<> <>
removed the label{" "} 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"> <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 className="font-medium flex-shrink truncate text-custom-text-100">{activity.old_value}</span>
</span> </span>
{showIssue && ( {showIssue && (
@ -586,6 +570,24 @@ const activityDetails: {
), ),
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />, 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: { start_date: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
if (!activity.new_value) if (!activity.new_value)
@ -675,7 +677,12 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</> <>{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 router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -684,7 +691,7 @@ export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIs
{activityDetails[activity.field as keyof typeof activityDetails]?.message( {activityDetails[activity.field as keyof typeof activityDetails]?.message(
activity, activity,
showIssue, showIssue,
workspaceSlug?.toString() ?? "" workspaceSlug ? workspaceSlug.toString() : activity.workspace_detail?.slug ?? ""
)} )}
</> </>
); );

View File

@ -30,9 +30,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const createCycle = async (payload: Partial<ICycle>) => const createCycle = async (payload: Partial<ICycle>) => {
cycleStore if (!workspaceSlug || !projectId) return;
.createCycle(workspaceSlug, projectId, payload) const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore
.createCycle(workspaceSlug, selectedProjectId, payload)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -47,10 +49,13 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
message: "Error in creating cycle. Please try again.", message: "Error in creating cycle. Please try again.",
}); });
}); });
};
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
cycleStore if (!workspaceSlug || !projectId) return;
.patchCycle(workspaceSlug, projectId, cycleId, payload) const selectedProjectId = payload.project ?? projectId.toString();
await cycleStore
.patchCycle(workspaceSlug, selectedProjectId, cycleId, payload)
.then(() => { .then(() => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -65,6 +70,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
message: "Error in updating cycle. Please try again.", message: "Error in updating cycle. Please try again.",
}); });
}); });
};
const dateChecker = async (payload: CycleDateCheckData) => { const dateChecker = async (payload: CycleDateCheckData) => {
let status = false; let status = false;

View File

@ -270,7 +270,20 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100) ? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
: null; : 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 endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? ""); const startDate = new Date(cycleDetails.start_date ?? "");
@ -300,7 +313,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
/> />
)} )}
{cycleDetails ? (
<> <>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<div> <div>
@ -536,19 +548,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
</div> </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>
)}
</> </>
); );
}); });

View File

@ -12,6 +12,7 @@ import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/i
import { findTotalDaysInRange } from "helpers/date-time.helper"; import { findTotalDaysInRange } from "helpers/date-time.helper";
// types // types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { IIssue } from "types";
type Props = { type Props = {
title: string; title: string;
@ -19,11 +20,18 @@ type Props = {
blocks: IGanttBlock[] | null; blocks: IGanttBlock[] | null;
enableReorder: boolean; enableReorder: boolean;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
}; };
export const IssueGanttSidebar: React.FC<Props> = (props) => { export const IssueGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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 router = useRouter();
const { cycleId } = router.query; const { cycleId } = router.query;
@ -152,7 +160,9 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
)} )}
{droppableProvided.placeholder} {droppableProvided.placeholder}
</> </>
{enableQuickIssueCreate && <GanttInlineCreateIssueForm />} {enableQuickIssueCreate && (
<GanttInlineCreateIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</div> </div>
)} )}
</StrictModeDroppable> </StrictModeDroppable>

View File

@ -19,25 +19,32 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EFilterType } from "store/issues/types";
export const CycleIssuesHeader: React.FC = observer(() => { export const CycleIssuesHeader: React.FC = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
const { const {
issueFilter: issueFilterStore,
cycle: cycleStore, cycle: cycleStore,
cycleIssueFilter: cycleIssueFilterStore, cycleIssueFilters: cycleIssueFiltersStore,
projectIssuesFilter: projectIssueFiltersStore,
project: { currentProjectDetails }, project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectLabel: { projectLabels },
projectState: projectStateStore, projectState: projectStateStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
cycleIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
@ -49,58 +56,44 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
}, },
}); [workspaceSlug, projectId, cycleId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
const newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? [];
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((val) => { value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val); if (!newValues.includes(val)) newValues.push(val);
}); });
} else { } 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); else newValues.push(value);
} }
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), { updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, cycleId);
[key]: newValues,
});
}, },
[cycleId, cycleIssueFilterStore, projectId, workspaceSlug] [workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
); );
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
}, },
}); [workspaceSlug, projectId, cycleId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleDisplayPropertiesUpdate = useCallback( const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => { (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, cycleId);
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
}, },
[issueFilterStore, projectId, workspaceSlug] [workspaceSlug, projectId, cycleId, updateFilters]
); );
const cyclesList = cycleStore.projectCycles; const cyclesList = cycleStore.projectCycles;
@ -173,25 +166,25 @@ export const CycleIssuesHeader: React.FC = observer(() => {
/> />
<FiltersDropdown title="Filters" placement="bottom-end"> <FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection <FilterSelection
filters={cycleIssueFilterStore.cycleFilters} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">

View File

@ -9,6 +9,7 @@ export * from "./workspace-analytics";
export * from "./workspace-dashboard"; export * from "./workspace-dashboard";
export * from "./projects"; export * from "./projects";
export * from "./profile-preferences"; export * from "./profile-preferences";
export * from "./profile-settings";
export * from "./cycles"; export * from "./cycles";
export * from "./modules-list"; export * from "./modules-list";
export * from "./project-settings"; export * from "./project-settings";

View File

@ -19,24 +19,30 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EFilterType } from "store/issues/types";
export const ModuleIssuesHeader: React.FC = observer(() => { export const ModuleIssuesHeader: React.FC = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const { const {
issueFilter: issueFilterStore,
module: moduleStore, module: moduleStore,
moduleFilter: moduleFilterStore, projectIssuesFilter: projectIssueFiltersStore,
project: { currentProjectDetails }, project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
projectLabel: { projectLabels },
moduleIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const { currentProjectDetails } = projectStore;
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
@ -45,61 +51,49 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
setValue(`${!isSidebarCollapsed}`); setValue(`${!isSidebarCollapsed}`);
}; };
const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
}, },
}); [workspaceSlug, projectId, moduleId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
const newValues = moduleFilterStore.moduleFilters?.[key] ?? [];
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((val) => { value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val); if (!newValues.includes(val)) newValues.push(val);
}); });
} else { } 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); else newValues.push(value);
} }
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, moduleId);
[key]: newValues,
});
}, },
[moduleId, moduleFilterStore, projectId, workspaceSlug] [workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
); );
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
}, },
}); [workspaceSlug, projectId, moduleId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleDisplayPropertiesUpdate = useCallback( const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => { (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, moduleId);
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
}, },
[issueFilterStore, projectId, workspaceSlug] [workspaceSlug, projectId, moduleId, updateFilters]
); );
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined; const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
@ -172,25 +166,25 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
/> />
<FiltersDropdown title="Filters" placement="bottom-end"> <FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection <FilterSelection
filters={moduleFilterStore.moduleFilters} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm"> <Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">

View 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>
);
};

View File

@ -16,85 +16,72 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
// helper // helper
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { EFilterType } from "store/issues/types";
export const ProjectIssuesHeader: React.FC = observer(() => { export const ProjectIssuesHeader: React.FC = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { const {
issueFilter: issueFilterStore,
project: { currentProjectDetails }, project: { currentProjectDetails },
projectLabel: { projectLabels }, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
inbox: inboxStore, inbox: inboxStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
// issue filters
projectIssuesFilter: { issueFilters, updateFilters },
projectIssues: {},
} = useMobxStore(); } = useMobxStore();
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,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
const newValues = issueFilterStore.userFilters?.[key] ?? [];
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((val) => { value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val); if (!newValues.includes(val)) newValues.push(val);
}); });
} else { } 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); else newValues.push(value);
} }
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues });
filters: {
[key]: newValues,
}, },
}); [workspaceSlug, projectId, issueFilters, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
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>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
}, },
}); [workspaceSlug, projectId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleDisplayPropertiesUpdate = useCallback( const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => { (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property);
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), 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; const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
@ -173,29 +160,29 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
/> />
<FiltersDropdown title="Filters" placement="bottom-end"> <FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection <FilterSelection
filters={issueFilterStore.userFilters} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
{projectId && inboxStore.isInboxEnabled && inboxDetails && ( {projectId && inboxStore.isInboxEnabled && inboxDetails && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId.toString())}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId)}`}>
<a> <a>
<Button variant="neutral-primary" size="sm" className="relative"> <Button variant="neutral-primary" size="sm" className="relative">
Inbox Inbox

View File

@ -14,10 +14,15 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EFilterType } from "store/issues/types";
export const ProjectViewIssuesHeader: React.FC = observer(() => { export const ProjectViewIssuesHeader: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query as {
workspaceSlug: string;
projectId: string;
viewId: string;
};
const { const {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
@ -27,67 +32,54 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
projectViews: projectViewsStore, projectViews: projectViewsStore,
viewIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined; const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, viewId);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
}, },
}); [workspaceSlug, projectId, viewId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !viewId) return; if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
const newValues = storedFilters?.[key] ?? [];
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((val) => { value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val); if (!newValues.includes(val)) newValues.push(val);
}); });
} else { } 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); else newValues.push(value);
} }
projectViewFiltersStore.updateStoredFilters(viewId.toString(), { updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, viewId);
[key]: newValues,
});
}, },
[projectViewFiltersStore, storedFilters, viewId, workspaceSlug] [workspaceSlug, projectId, viewId, issueFilters, updateFilters]
); );
const handleDisplayFiltersUpdate = useCallback( const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
}, },
}); [workspaceSlug, projectId, viewId, updateFilters]
},
[issueFilterStore, projectId, workspaceSlug]
); );
const handleDisplayPropertiesUpdate = useCallback( const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => { (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, viewId);
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
}, },
[issueFilterStore, projectId, workspaceSlug] [workspaceSlug, projectId, viewId, updateFilters]
); );
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined; const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
@ -157,25 +149,25 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
/> />
<FiltersDropdown title="Filters" placement="bottom-end"> <FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection <FilterSelection
filters={storedFilters ?? {}} filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectLabels ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId ?? ""] ?? undefined}
/> />
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/> />
</FiltersDropdown> </FiltersDropdown>
</div> </div>

View File

@ -1,36 +1,14 @@
import { FC } from "react";
import { useRouter } from "next/router";
// ui // ui
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
import { UserCircle2 } from "lucide-react";
// hooks
import { observer } from "mobx-react-lite";
export interface IUserProfileHeader { export const UserProfileHeader = () => (
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="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 className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div> <div>
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem type="text" label="Activity Overview" link="/me/profile" />
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> </Breadcrumbs>
</div> </div>
</div> </div>
</div> </div>
); );
});

View File

@ -1,9 +1,11 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { Cog, LogIn, LogOut, Settings, UserCircle2 } from "lucide-react"; import { Cog, LogIn, LogOut, Settings, UserCircle2 } from "lucide-react";
import { mutate } from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
@ -23,7 +25,7 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
{ {
name: "Settings", name: "Settings",
icon: Settings, icon: Settings,
link: `/${workspaceSlug}/me/profile`, link: `/me/profile`,
}, },
]; ];
@ -39,6 +41,7 @@ export const InstanceSidebarDropdown = observer(() => {
} = useMobxStore(); } = useMobxStore();
// hooks // hooks
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { setTheme } = useTheme();
// redirect url for normal mode // redirect url for normal mode
const redirectWorkspaceSlug = const redirectWorkspaceSlug =
@ -51,6 +54,8 @@ export const InstanceSidebarDropdown = observer(() => {
await authService await authService
.signOut() .signOut()
.then(() => { .then(() => {
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/"); router.push("/");
}) })
.catch(() => .catch(() =>
@ -70,13 +75,13 @@ export const InstanceSidebarDropdown = observer(() => {
sidebarCollapsed ? "justify-center" : "" 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" /> <Cog className="h-5 w-5 text-custom-text-200" />
</div> </div>
{!sidebarCollapsed && ( {!sidebarCollapsed && <h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>}
<h4 className="text-custom-text-200 font-medium text-base truncate">Instance Admin</h4>
)}
</div> </div>
</div> </div>

View File

@ -1,10 +1,6 @@
import { useEffect, useState, Fragment } from "react"; import { useEffect, useState, Fragment } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
@ -17,14 +13,9 @@ type Props = {
onSubmit?: () => Promise<void>; 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 { data, isOpen, handleClose, onSubmit } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { issueDetail: issueDetailStore } = useMobxStore();
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
useEffect(() => { useEffect(() => {
@ -37,12 +28,7 @@ export const DeleteIssueModal: React.FC<Props> = observer((props) => {
}; };
const handleIssueDelete = async () => { const handleIssueDelete = async () => {
if (!workspaceSlug) return;
setIsDeleteLoading(true); setIsDeleteLoading(true);
await issueDetailStore.deleteIssue(workspaceSlug.toString(), data.project, data.id);
if (onSubmit) await onSubmit().finally(() => setIsDeleteLoading(false)); 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"> <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 flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-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" /> <AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span> </div>
<span className="flex items-center justify-start"> <span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3> <h3 className="text-xl font-medium 2xl:text-2xl">Delete Issue</h3>
</span> </span>
@ -114,4 +100,4 @@ export const DeleteIssueModal: React.FC<Props> = observer((props) => {
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}); };

View File

@ -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>
);
});

View File

@ -8,19 +8,28 @@ import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// types // types
import { ICalendarWeek } from "./types"; import { ICalendarWeek } from "./types";
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
type Props = { type Props = {
issues: IIssueGroupedStructure | null; issues: IIssueResponse | undefined;
groupedIssueIds: IGroupedIssues;
layout: "month" | "week" | undefined; layout: "month" | "week" | undefined;
showWeekends: boolean; showWeekends: boolean;
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void; handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; 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) => { 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(); const { calendar: calendarStore } = useMobxStore();
@ -49,9 +58,12 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
key={weekIndex} key={weekIndex}
week={week} week={week}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate enableQuickIssueCreate
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
))} ))}
</div> </div>
@ -60,9 +72,12 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
<CalendarWeekDays <CalendarWeekDays
week={calendarStore.allDaysOfActiveWeek} week={calendarStore.allDaysOfActiveWeek}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate enableQuickIssueCreate
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
</div> </div>

View File

@ -4,31 +4,48 @@ import { Droppable } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarIssueBlocks, ICalendarDate, CalendarInlineCreateIssueForm } from "components/issues"; import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues";
// helpers // helpers
import { renderDateFormat } from "helpers/date-time.helper"; import { renderDateFormat } from "helpers/date-time.helper";
// types
import { IIssueGroupedStructure } from "store/issue";
// constants // constants
import { MONTHS_LIST } from "constants/calendar"; import { MONTHS_LIST } from "constants/calendar";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
type Props = { type Props = {
date: ICalendarDate; date: ICalendarDate;
issues: IIssueGroupedStructure | null; issues: IIssueResponse | undefined;
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void; groupedIssueIds: IGroupedIssues;
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
}; };
export const CalendarDayTile: React.FC<Props> = observer((props) => { 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 { issueFilter: issueFilterStore } = useMobxStore();
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month"; 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 ( return (
<> <>
@ -64,14 +81,22 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{...provided.droppableProps} {...provided.droppableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
<CalendarIssueBlocks issues={issuesList} handleIssues={handleIssues} quickActions={quickActions} /> <CalendarIssueBlocks
issues={issues}
issueIdList={issueIdList}
handleIssues={handleIssues}
quickActions={quickActions}
/>
{enableQuickIssueCreate && ( {enableQuickIssueCreate && (
<div className="py-1 px-2"> <div className="py-1 px-2">
<CalendarInlineCreateIssueForm <CalendarQuickAddIssueForm
formKey="target_date"
groupId={renderDateFormat(date.date)} groupId={renderDateFormat(date.date)}
prePopulatedData={{ prePopulatedData={{
target_date: renderDateFormat(date.date), target_date: renderDateFormat(date.date),
}} }}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
</div> </div>
)} )}

View File

@ -7,4 +7,4 @@ export * from "./header";
export * from "./issue-blocks"; export * from "./issue-blocks";
export * from "./week-days"; export * from "./week-days";
export * from "./week-header"; export * from "./week-header";
export * from "./inline-create-issue-form"; export * from "./quick-add-issue-form";

View File

@ -5,19 +5,26 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IIssueResponse } from "store/issues/types";
type Props = { type Props = {
issues: IIssue[] | null; issues: IIssueResponse | undefined;
handleIssues: (date: string, issue: IIssue, action: "update" | "delete") => void; issueIdList: string[] | null;
handleIssues: (date: string, issue: IIssue, action: EIssueActions) => void;
quickActions: (issue: IIssue) => React.ReactNode; quickActions: (issue: IIssue) => React.ReactNode;
}; };
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues, handleIssues, quickActions } = props; const { issues, issueIdList, handleIssues, quickActions } = props;
return ( return (
<> <>
{issues?.map((issue, index) => ( {issueIdList?.map((issueId, index) => {
if (!issues?.[issueId]) return null;
const issue = issues?.[issueId];
return (
<Draggable key={issue.id} draggableId={issue.id} index={index}> <Draggable key={issue.id} draggableId={issue.id} index={index}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
@ -52,7 +59,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
issueId={issue?.id} issueId={issue?.id}
// TODO: add the logic here // TODO: add the logic here
handleIssue={(issueToUpdate) => { handleIssue={(issueToUpdate) => {
handleIssues(issue.target_date ?? "", { ...issue, ...issueToUpdate }, "update"); handleIssues(issue.target_date ?? "", { ...issue, ...issueToUpdate }, EIssueActions.UPDATE);
}} }}
> >
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
@ -64,7 +71,8 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
</div> </div>
)} )}
</Draggable> </Draggable>
))} );
})}
</> </>
); );
}); });

View File

@ -7,19 +7,26 @@ import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useKeypress from "hooks/use-keypress"; import useKeypress from "hooks/use-keypress";
import useProjectDetails from "hooks/use-project-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// helpers // helpers
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// icons // icons
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
// types // types
import { IIssue } from "types"; import { IIssue, IProject } from "types";
type Props = { type Props = {
formKey: keyof IIssue;
groupId?: string; groupId?: string;
subGroupId?: string | null;
prePopulatedData?: Partial<IIssue>; 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> = { const defaultValues: Partial<IIssue> = {
@ -49,15 +56,14 @@ const Inputs = (props: any) => {
); );
}; };
export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) => { export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { prePopulatedData, groupId } = props; const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store const { workspace: workspaceStore, project: projectStore } = useMobxStore();
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore();
// ref // ref
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -67,7 +73,10 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
const { setToastAlert } = useToast(); 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 { const {
reset, reset,
@ -84,9 +93,6 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
useKeypress("Escape", handleClose); useKeypress("Escape", handleClose);
useOutsideClickDetector(ref, handleClose); useOutsideClickDetector(ref, handleClose);
// derived values
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
useEffect(() => { useEffect(() => {
if (!isOpen) reset({ ...defaultValues }); if (!isOpen) reset({ ...defaultValues });
}, [isOpen, reset]); }, [isOpen, reset]);
@ -106,42 +112,36 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
}, [errors, setToastAlert]); }, [errors, setToastAlert]);
const onSubmitHandler = async (formData: IIssue) => { 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 });
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
const payload = createIssuePayload(workspaceDetail!, projectDetails!, { const payload = createIssuePayload(workspaceDetail, projectDetail, {
...(prePopulatedData ?? {}), ...(prePopulatedData ?? {}),
...formData, ...formData,
}); });
try { try {
quickAddStore.createIssue( quickAddCallback &&
workspaceSlug.toString(), (await quickAddCallback(
projectId.toString(), workspaceSlug,
projectId,
{ {
group_id: groupId ?? null, ...payload,
sub_group_id: null,
}, },
payload viewId
); ));
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
} catch (err: any) { } catch (err: any) {
Object.keys(err || {}).forEach((key) => { console.error(err);
const error = err?.[key];
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: errorTitle || "Some error occurred. Please try again.", message: err?.message || "Some error occurred. Please try again.",
});
}); });
} }
}; };
@ -159,7 +159,7 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
onSubmit={handleSubmit(onSubmitHandler)} 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" 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> </form>
</div> </div>
)} )}

View File

@ -1,80 +1,43 @@
import { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart, CycleIssueQuickActions } from "components/issues"; import { CycleIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../../types";
import { BaseCalendarRoot } from "../base-calendar-root";
export const CycleCalendarLayout: React.FC = observer(() => { export const CycleCalendarLayout: React.FC = observer(() => {
const { const { cycleIssues: cycleIssueStore, cycleIssueCalendarView: cycleIssueCalendarViewStore } = useMobxStore();
cycleIssue: cycleIssueStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
cycleIssueCalendarView: cycleIssueCalendarViewStore,
} = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, cycleId } = router.query; const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
const onDragEnd = (result: DropResult) => { const issueActions = {
if (!result) return; [EIssueActions.UPDATE]: async (issue: IIssue) => {
// 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") => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
if (action === "update") { cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
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, 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 ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <BaseCalendarRoot
<DragDropContext onDragEnd={onDragEnd}> issueStore={cycleIssueStore}
<CalendarChart calendarViewStore={cycleIssueCalendarViewStore}
issues={issues as IIssueGroupedStructure | null} QuickActions={CycleIssueQuickActions}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} issueActions={issueActions}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false} viewId={cycleId}
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>
); );
}); });

View File

@ -1,82 +1,42 @@
import { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart, ModuleIssueQuickActions } from "components/issues"; import { ModuleIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../../types";
import { BaseCalendarRoot } from "../base-calendar-root";
export const ModuleCalendarLayout: React.FC = observer(() => { export const ModuleCalendarLayout: React.FC = observer(() => {
const { const { moduleIssues: moduleIssueStore, moduleIssueCalendarView: moduleIssueCalendarViewStore } = useMobxStore();
moduleIssue: moduleIssueStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
moduleIssueCalendarView: moduleIssueCalendarViewStore,
} = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, moduleId } = router.query; const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
const onDragEnd = (result: DropResult) => { const issueActions = {
if (!result) return; [EIssueActions.UPDATE]: (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
// return if not dropped on the correct place moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
if (!result.destination) return; },
[EIssueActions.DELETE]: (issue: IIssue) => {
// return if dropped on the same date if (!workspaceSlug || !moduleId) return;
if (result.destination.droppableId === result.source.droppableId) return; moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
},
moduleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination); [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 ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <BaseCalendarRoot
<DragDropContext onDragEnd={onDragEnd}> issueStore={moduleIssueStore}
<CalendarChart calendarViewStore={moduleIssueCalendarViewStore}
issues={issues as IIssueGroupedStructure | null} QuickActions={ModuleIssueQuickActions}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} issueActions={issueActions}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false} viewId={moduleId}
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>
); );
}); });

View File

@ -1,72 +1,42 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart, ProjectIssueQuickActions } from "components/issues"; import { ProjectIssueQuickActions } from "components/issues";
// types import { BaseCalendarRoot } from "../base-calendar-root";
import { IIssueGroupedStructure } from "store/issue"; import { EIssueActions } from "../../types";
import { IIssue } from "types"; import { IIssue } from "types";
import { useRouter } from "next/router";
export const CalendarLayout: React.FC = observer(() => { export const CalendarLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query as { workspaceSlug: string };
const { const {
issue: issueStore, projectIssues: issueStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
issueCalendarView: issueCalendarViewStore, issueCalendarView: issueCalendarViewStore,
issueDetail: issueDetailStore,
} = useMobxStore(); } = useMobxStore();
const router = useRouter(); const issueActions = {
const { workspaceSlug } = router.query; [EIssueActions.UPDATE]: async (issue: IIssue) => {
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") => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (action === "update") {
issueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
issueStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
}, },
[issueStore, issueDetailStore, workspaceSlug] [EIssueActions.DELETE]: async (issue: IIssue) => {
); if (!workspaceSlug) return;
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
},
};
return ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <BaseCalendarRoot
<DragDropContext onDragEnd={onDragEnd}> issueStore={issueStore}
<CalendarChart calendarViewStore={issueCalendarViewStore}
issues={issues as IIssueGroupedStructure | null} QuickActions={ProjectIssueQuickActions}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} issueActions={issueActions}
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>
); );
}); });

View File

@ -7,66 +7,39 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CalendarChart, ProjectIssueQuickActions } from "components/issues"; import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
// types // types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../../types";
import { BaseCalendarRoot } from "../base-calendar-root";
export const ProjectViewCalendarLayout: React.FC = observer(() => { export const ProjectViewCalendarLayout: React.FC = observer(() => {
const { const {
projectViewIssues: projectViewIssuesStore, viewIssues: projectViewIssuesStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
projectViewIssueCalendarView: projectViewIssueCalendarViewStore, projectViewIssueCalendarView: projectViewIssueCalendarViewStore,
} = useMobxStore(); } = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query as { workspaceSlug: string };
const onDragEnd = (result: DropResult) => { const issueActions = {
if (!result) return; [EIssueActions.UPDATE]: async (issue: IIssue) => {
// 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") => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (action === "update") {
projectViewIssuesStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
projectViewIssuesStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
}, },
[projectViewIssuesStore, issueDetailStore, workspaceSlug] [EIssueActions.DELETE]: async (issue: IIssue) => {
); if (!workspaceSlug) return;
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
},
};
return ( return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden"> <BaseCalendarRoot
<DragDropContext onDragEnd={onDragEnd}> issueStore={projectViewIssuesStore}
<CalendarChart calendarViewStore={projectViewIssueCalendarViewStore}
issues={issues as IIssueGroupedStructure | null} QuickActions={ProjectIssueQuickActions}
layout={issueFilterStore.userDisplayFilters.calendar?.layout} issueActions={issueActions}
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>
); );
}); });

View File

@ -8,19 +8,37 @@ import { CalendarDayTile } from "components/issues";
import { renderDateFormat } from "helpers/date-time.helper"; import { renderDateFormat } from "helpers/date-time.helper";
// types // types
import { ICalendarDate, ICalendarWeek } from "./types"; import { ICalendarDate, ICalendarWeek } from "./types";
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../types";
import { IGroupedIssues, IIssueResponse } from "store/issues/types";
type Props = { type Props = {
issues: IIssueGroupedStructure | null; issues: IIssueResponse | undefined;
groupedIssueIds: IGroupedIssues;
week: ICalendarWeek | undefined; 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; quickActions: (issue: IIssue) => React.ReactNode;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
}; };
export const CalendarWeekDays: React.FC<Props> = observer((props) => { 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(); const { issueFilter: issueFilterStore } = useMobxStore();
@ -43,9 +61,12 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
key={renderDateFormat(date.date)} key={renderDateFormat(date.date)}
date={date} date={date}
issues={issues} issues={issues}
groupedIssueIds={groupedIssueIds}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
); );
})} })}

View File

@ -6,61 +6,69 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { AppliedFiltersList } from "components/issues"; import { AppliedFiltersList } from "components/issues";
// types // types
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const CycleAppliedFiltersRoot: React.FC = observer(() => { export const CycleAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
const { const {
projectLabel: { projectLabels }, projectLabel: { projectLabels },
projectMember: { projectMembers },
cycleIssueFilter: cycleIssueFilterStore,
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers },
cycleIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const userFilters = cycleIssueFilterStore.cycleFilters; const userFilters = issueFilters?.filters;
// filters whose value not null or empty array // filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {}; const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters).forEach(([key, value]) => { Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return; if (!value) return;
if (Array.isArray(value) && value.length === 0) return; if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value; appliedFilters[key as keyof IIssueFilterOptions] = value;
}); });
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId) return;
// remove all values of the key if value is null
if (!value) { if (!value) {
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), { updateFilters(
workspaceSlug,
projectId,
EFilterType.FILTERS,
{
[key]: null, [key]: null,
}); },
cycleId
);
return; return;
} }
// remove the passed value from the key let newValues = issueFilters?.filters?.[key] ?? [];
let newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value); newValues = newValues.filter((val) => val !== value);
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), { updateFilters(
workspaceSlug,
projectId,
EFilterType.FILTERS,
{
[key]: newValues, [key]: newValues,
}); },
cycleId
);
}; };
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {}; const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => { Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null; newFilters[key as keyof IIssueFilterOptions] = null;
}); });
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, cycleId);
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId?.toString(), {
...newFilters,
});
}; };
// return if no filters are applied // return if no filters are applied
@ -74,7 +82,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[cycleId ?? ""]}
/> />
</div> </div>
); );

View File

@ -7,61 +7,69 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { AppliedFiltersList } from "components/issues"; import { AppliedFiltersList } from "components/issues";
// types // types
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const ModuleAppliedFiltersRoot: React.FC = observer(() => { export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
projectId: string;
moduleId: string;
};
const { const {
projectLabel: { projectLabels }, projectLabel: { projectLabels },
moduleFilter: moduleFilterStore,
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
moduleIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const userFilters = moduleFilterStore.moduleFilters; const userFilters = issueFilters?.filters;
// filters whose value not null or empty array // filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {}; const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters).forEach(([key, value]) => { Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return; if (!value) return;
if (Array.isArray(value) && value.length === 0) return; if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value; appliedFilters[key as keyof IIssueFilterOptions] = value;
}); });
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId) return;
// remove all values of the key if value is null
if (!value) { if (!value) {
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { updateFilters(
workspaceSlug,
projectId,
EFilterType.FILTERS,
{
[key]: null, [key]: null,
}); },
moduleId
);
return; return;
} }
// remove the passed value from the key let newValues = issueFilters?.filters?.[key] ?? [];
let newValues = moduleFilterStore.moduleFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value); newValues = newValues.filter((val) => val !== value);
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { updateFilters(
workspaceSlug,
projectId,
EFilterType.FILTERS,
{
[key]: newValues, [key]: newValues,
}); },
moduleId
);
}; };
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {}; const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => { Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null; newFilters[key as keyof IIssueFilterOptions] = null;
}); });
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, moduleId);
moduleFilterStore.updateModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId?.toString(), {
...newFilters,
});
}; };
// return if no filters are applied // return if no filters are applied
@ -75,7 +83,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[moduleId ?? ""]}
/> />
</div> </div>
); );

View File

@ -7,65 +7,53 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { AppliedFiltersList } from "components/issues"; import { AppliedFiltersList } from "components/issues";
// types // types
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const ProjectAppliedFiltersRoot: React.FC = observer(() => { export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { const {
issueFilter: issueFilterStore,
projectLabel: { projectLabels }, projectLabel: { projectLabels },
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
projectIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const userFilters = issueFilterStore.userFilters; const userFilters = issueFilters?.filters;
// filters whose value not null or empty array // filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {}; const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters).forEach(([key, value]) => { Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return; if (!value) return;
if (Array.isArray(value) && value.length === 0) return; if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value; appliedFilters[key as keyof IIssueFilterOptions] = value;
}); });
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
// remove all values of the key if value is null
if (!value) { if (!value) {
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
filters: {
[key]: null, [key]: null,
},
}); });
return; return;
} }
// remove the passed value from the key let newValues = issueFilters?.filters?.[key] ?? [];
let newValues = issueFilterStore.userFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value); newValues = newValues.filter((val) => val !== value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, {
filters: {
[key]: newValues, [key]: newValues,
},
}); });
}; };
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {}; const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => { Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null; newFilters[key as keyof IIssueFilterOptions] = null;
}); });
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters });
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: { ...newFilters },
});
}; };
// return if no filters are applied // return if no filters are applied

View File

@ -12,65 +12,78 @@ import { Button } from "@plane/ui";
import { areFiltersDifferent } from "helpers/filter.helper"; import { areFiltersDifferent } from "helpers/filter.helper";
// types // types
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions } from "types";
import { EFilterType } from "store/issues/types";
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query as {
workspaceSlug: string;
projectId: string;
viewId: string;
};
const { const {
projectLabel: { projectLabels }, projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers },
projectViews: projectViewsStore, projectViews: projectViewsStore,
projectViewFilters: projectViewFiltersStore, projectViewFilters: projectViewFiltersStore,
viewIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore(); } = useMobxStore();
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined; const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined; const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
const userFilters = issueFilters?.filters;
// filters whose value not null or empty array // filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {}; const appliedFilters: IIssueFilterOptions = {};
Object.entries(storedFilters ?? {}).forEach(([key, value]) => { Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return; if (!value) return;
if (Array.isArray(value) && value.length === 0) return; if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value; appliedFilters[key as keyof IIssueFilterOptions] = value;
}); });
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!viewId) return; if (!workspaceSlug || !projectId) return;
// remove all values of the key if value is null
if (!value) { if (!value) {
projectViewFiltersStore.updateStoredFilters(viewId.toString(), { updateFilters(
workspaceSlug,
projectId,
EFilterType.FILTERS,
{
[key]: null, [key]: null,
}); },
viewId
);
return; return;
} }
// remove the passed value from the key let newValues = issueFilters?.filters?.[key] ?? [];
let newValues = storedFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value); newValues = newValues.filter((val) => val !== value);
projectViewFiltersStore.updateStoredFilters(viewId.toString(), { updateFilters(
workspaceSlug,
projectId,
EFilterType.FILTERS,
{
[key]: newValues, [key]: newValues,
}); },
viewId
);
}; };
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !viewId) return; if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {}; const newFilters: IIssueFilterOptions = {};
Object.keys(storedFilters ?? {}).forEach((key) => { Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null; newFilters[key as keyof IIssueFilterOptions] = null;
}); });
updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, viewId);
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
...newFilters,
});
}; };
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;
const handleUpdateView = () => { const handleUpdateView = () => {
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return; 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 ( return (
<div className="flex items-center justify-between gap-4 p-4"> <div className="flex items-center justify-between gap-4 p-4">
<AppliedFiltersList <AppliedFiltersList

View File

@ -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>
</>
);
});

View File

@ -1,57 +1,15 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useProjectDetails from "hooks/use-project-details";
// components // components
import { IssueGanttBlock } from "components/issues"; import { BaseGanttRoot } from "./base-gantt-root";
import { import { useRouter } from "next/router";
GanttChartRoot,
IBlockUpdateData,
renderIssueBlocksStructure,
IssueGanttSidebar,
} from "components/gantt-chart";
// types
import { IIssueUnGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const CycleGanttLayout: React.FC = observer(() => { export const CycleGanttLayout: React.FC = observer(() => {
const router = useRouter(); 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(); return <BaseGanttRoot issueFiltersStore={cycleIssueFilterStore} issueStore={cycleIssueStore} viewId={cycleId} />;
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>
</>
);
}); });

View File

@ -1,6 +1,6 @@
export * from "./blocks"; export * from "./blocks";
export * from "./cycle-root"; export * from "./cycle-root";
export * from "./quick-add-issue-form";
export * from "./module-root"; export * from "./module-root";
export * from "./project-root";
export * from "./project-view-root"; export * from "./project-view-root";
export * from "./root";
export * from "./inline-create-issue-form";

View File

@ -1,57 +1,15 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useProjectDetails from "hooks/use-project-details";
// components // components
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; import { BaseGanttRoot } from "./base-gantt-root";
import { import { useRouter } from "next/router";
GanttChartRoot,
IBlockUpdateData,
renderIssueBlocksStructure,
IssueGanttSidebar,
} from "components/gantt-chart";
// types
import { IIssueUnGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const ModuleGanttLayout: React.FC = observer(() => { export const ModuleGanttLayout: React.FC = observer(() => {
const router = useRouter(); 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(); return <BaseGanttRoot issueFiltersStore={moduleIssueFilterStore} issueStore={moduleIssueStore} viewId={moduleId} />;
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>
</>
);
}); });

View 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} />;
});

View File

@ -1,59 +1,11 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components // components
import { IssueGanttBlock } from "components/issues"; import { BaseGanttRoot } from "./base-gantt-root";
import {
GanttChartRoot,
IBlockUpdateData,
renderIssueBlocksStructure,
ProjectViewGanttSidebar,
} from "components/gantt-chart";
// types
import { IIssueUnGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const ProjectViewGanttLayout: React.FC = observer(() => { export const ProjectViewGanttLayout: React.FC = observer(() => {
const router = useRouter(); const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore();
const { workspaceSlug, viewId } = router.query;
const { projectDetails } = useProjectDetails(); return <BaseGanttRoot issueFiltersStore={projectIssueViewFiltersStore} issueStore={projectIssueViewStore} />;
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>
</>
);
}); });

View File

@ -20,6 +20,13 @@ import { createIssuePayload } from "helpers/issue.helper";
type Props = { type Props = {
prePopulatedData?: Partial<IIssue>; prePopulatedData?: Partial<IIssue>;
onSuccess?: (data: IIssue) => Promise<void> | void; onSuccess?: (data: IIssue) => Promise<void> | void;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
}; };
const defaultValues: Partial<IIssue> = { const defaultValues: Partial<IIssue> = {
@ -47,14 +54,14 @@ const Inputs = (props: any) => {
}; };
export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => { export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const { prePopulatedData } = props; const { prePopulatedData, quickAddCallback, viewId } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store // store
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); const { workspace: workspaceStore } = useMobxStore();
const { projectDetails } = useProjectDetails(); const { projectDetails } = useProjectDetails();
@ -114,15 +121,7 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
}); });
try { try {
quickAddStore.createIssue( quickAddCallback && quickAddCallback(workspaceSlug, projectId, payload, viewId);
workspaceSlug.toString(),
projectId.toString(),
{
group_id: null,
sub_group_id: null,
},
payload
);
setToastAlert({ setToastAlert({
type: "success", type: "success",

View File

@ -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>
</>
);
});

View 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>
</>
);
});

View File

@ -5,6 +5,7 @@ import { Tooltip } from "@plane/ui";
import { IssuePeekOverview } from "components/issues/issue-peek-overview"; import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// types // types
import { IIssueDisplayProperties, IIssue } from "types"; import { IIssueDisplayProperties, IIssue } from "types";
import { EIssueActions } from "../types";
interface IssueBlockProps { interface IssueBlockProps {
sub_group_id: string; sub_group_id: string;
@ -13,14 +14,9 @@ interface IssueBlockProps {
issue: IIssue; issue: IIssue;
isDragDisabled: boolean; isDragDisabled: boolean;
showEmptyGroup: boolean; showEmptyGroup: boolean;
handleIssues: ( handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; 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) => { export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
@ -37,7 +33,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
} = props; } = props;
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => { 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 ( return (
@ -79,7 +75,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
!sub_group_id && sub_group_id === "null" ? null : sub_group_id, !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!columnId && columnId === "null" ? null : columnId, !columnId && columnId === "null" ? null : columnId,
{ ...issue, ...issueToUpdate }, { ...issue, ...issueToUpdate },
"update" EIssueActions.UPDATE
); );
}} }}
> >

View File

@ -1,21 +1,19 @@
// components // components
import { KanbanIssueBlock } from "components/issues"; import { KanbanIssueBlock } from "components/issues";
import { IIssueDisplayProperties, IIssue } from "types"; import { IIssueDisplayProperties, IIssue } from "types";
import { EIssueActions } from "../types";
import { IIssueResponse } from "store/issues/types";
interface IssueBlocksListProps { interface IssueBlocksListProps {
sub_group_id: string; sub_group_id: string;
columnId: string; columnId: string;
issues: IIssue[]; issues: IIssueResponse;
issueIds: string[];
isDragDisabled: boolean; isDragDisabled: boolean;
showEmptyGroup: boolean; showEmptyGroup: boolean;
handleIssues: ( handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; 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) => { export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
@ -23,6 +21,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
sub_group_id, sub_group_id,
columnId, columnId,
issues, issues,
issueIds,
showEmptyGroup, showEmptyGroup,
isDragDisabled, isDragDisabled,
handleIssues, handleIssues,
@ -32,9 +31,14 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
return ( return (
<> <>
{issues && issues.length > 0 ? ( {issueIds && issueIds.length > 0 ? (
<> <>
{issues.map((issue, index) => ( {issueIds.map((issueId, index) => {
if (!issues[issueId]) return null;
const issue = issues[issueId];
return (
<KanbanIssueBlock <KanbanIssueBlock
key={`kanban-issue-block-${issue.id}`} key={`kanban-issue-block-${issue.id}`}
index={index} index={index}
@ -47,7 +51,8 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
/> />
))} );
})}
</> </>
) : ( ) : (
!isDragDisabled && ( !isDragDisabled && (

View File

@ -5,40 +5,47 @@ import { Droppable } from "@hello-pangea/dnd";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues"; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "components/issues";
// types // types
import { IIssueDisplayProperties, IIssue } from "types"; import { IIssueDisplayProperties, IIssue, IState } from "types";
// constants // constants
import { getValueFromObject } from "constants/issue"; import { getValueFromObject } from "constants/issue";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { EIssueActions } from "../types";
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
export interface IGroupByKanBan { export interface IGroupByKanBan {
issues: any; issues: IIssueResponse;
issueIds: any;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
order_by: string | null; order_by: string | null;
sub_group_id: string; sub_group_id: string;
list: any; list: any;
listKey: string; listKey: string;
states: IState[] | null;
isDragDisabled: boolean; isDragDisabled: boolean;
handleIssues: ( handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
showEmptyGroup: boolean; showEmptyGroup: boolean;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
isDragStarted?: 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 GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const { const {
issues, issues,
issueIds,
sub_group_by, sub_group_by,
group_by, group_by,
order_by, order_by,
@ -54,6 +61,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleKanBanToggle, handleKanBanToggle,
enableQuickIssueCreate, enableQuickIssueCreate,
isDragStarted, isDragStarted,
quickAddCallback,
viewId,
} = props; } = props;
const verticalAlignPosition = (_list: any) => const verticalAlignPosition = (_list: any) =>
@ -74,7 +83,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
column_value={_list} column_value={_list}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={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} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
/> />
@ -102,7 +111,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
<KanbanIssueBlocksList <KanbanIssueBlocksList
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
columnId={getValueFromObject(_list, listKey) as string} columnId={getValueFromObject(_list, listKey) as string}
issues={issues[getValueFromObject(_list, listKey) as string]} issues={issues}
issueIds={issueIds?.[getValueFromObject(_list, listKey) as string] || []}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} 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]"> <div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky bottom-0 z-[0]">
{enableQuickIssueCreate && ( {enableQuickIssueCreate && (
<BoardInlineCreateIssueForm <KanBanQuickAddIssueForm
formKey="name"
groupId={getValueFromObject(_list, listKey) as string} groupId={getValueFromObject(_list, listKey) as string}
subGroupId={sub_group_id} subGroupId={sub_group_id}
prePopulatedData={{ prePopulatedData={{
...(group_by && { [group_by]: getValueFromObject(_list, listKey) }), ...(group_by && { [group_by]: getValueFromObject(_list, listKey) }),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }), ...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}} }}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
</div> </div>
@ -152,19 +165,15 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
}); });
export interface IKanBan { export interface IKanBan {
issues: any; issues: IIssueResponse;
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
order_by: string | null; order_by: string | null;
sub_group_id?: string; sub_group_id?: string;
handleIssues: ( handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
showEmptyGroup: boolean; showEmptyGroup: boolean;
@ -176,11 +185,19 @@ export interface IKanBan {
projects: any; projects: any;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
isDragStarted?: 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) => { export const KanBan: React.FC<IKanBan> = observer((props) => {
const { const {
issues, issues,
issueIds,
sub_group_by, sub_group_by,
group_by, group_by,
order_by, order_by,
@ -199,6 +216,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
projects, projects,
enableQuickIssueCreate, enableQuickIssueCreate,
isDragStarted, isDragStarted,
quickAddCallback,
viewId,
} = props; } = props;
const { issueKanBanView: issueKanBanViewStore } = useMobxStore(); const { issueKanBanView: issueKanBanViewStore } = useMobxStore();
@ -208,12 +227,14 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
{group_by && group_by === "project" && ( {group_by && group_by === "project" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={projects} list={projects}
listKey={`id`} listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -223,18 +244,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "state" && ( {group_by && group_by === "state" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={states} list={states}
listKey={`id`} listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -244,18 +269,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "state_detail.group" && ( {group_by && group_by === "state_detail.group" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={stateGroups} list={stateGroups}
listKey={`key`} listKey={`key`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -265,18 +294,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "priority" && ( {group_by && group_by === "priority" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={priorities} list={priorities}
listKey={`key`} listKey={`key`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -286,18 +319,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "labels" && ( {group_by && group_by === "labels" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={labels ? [...labels, { id: "None", name: "None" }] : labels} list={labels ? [...labels, { id: "None", name: "None" }] : labels}
listKey={`id`} listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -307,18 +344,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "assignees" && ( {group_by && group_by === "assignees" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={members ? [...members, { id: "None", display_name: "None" }] : members} list={members ? [...members, { id: "None", display_name: "None" }] : members}
listKey={`id`} listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -328,18 +369,22 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "created_by" && ( {group_by && group_by === "created_by" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
issueIds={issueIds}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={members} list={members}
listKey={`id`} listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues} handleIssues={handleIssues}
@ -349,6 +394,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
</div> </div>

View File

@ -1,4 +1,4 @@
export * from "./block"; export * from "./block";
export * from "./roots"; export * from "./roots";
export * from "./blocks-list"; export * from "./blocks-list";
export * from "./inline-create-issue-form"; export * from "./quick-add-issue-form";

View File

@ -17,7 +17,7 @@ export interface IKanBanProperties {
columnId: string; columnId: string;
issue: IIssue; issue: IIssue;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | null;
showEmptyGroup: boolean; showEmptyGroup: boolean;
} }
@ -87,7 +87,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{displayProperties && displayProperties?.state && ( {displayProperties && displayProperties?.state && (
<IssuePropertyState <IssuePropertyState
projectId={issue?.project_detail?.id || null} projectId={issue?.project_detail?.id || null}
value={issue?.state_detail || null} value={issue?.state || null}
onChange={handleState} onChange={handleState}
disabled={false} disabled={false}
hideDropdownArrow hideDropdownArrow
@ -105,7 +105,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)} )}
{/* label */} {/* label */}
{displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( {displayProperties && displayProperties?.labels && (
<IssuePropertyLabels <IssuePropertyLabels
projectId={issue?.project_detail?.id || null} projectId={issue?.project_detail?.id || null}
value={issue?.labels || null} value={issue?.labels || null}
@ -116,7 +116,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)} )}
{/* start date */} {/* start date */}
{displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && ( {displayProperties && displayProperties?.start_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.start_date || null} value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)} onChange={(date: string) => handleStartDate(date)}
@ -126,7 +126,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)} )}
{/* target/due date */} {/* target/due date */}
{displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && ( {displayProperties && displayProperties?.due_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.target_date || null} value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} 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 */} {/* estimates */}
{displayProperties && displayProperties?.estimate && ( {displayProperties && displayProperties?.estimate && (
<IssuePropertyEstimates <IssuePropertyEstimates
@ -176,18 +188,6 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
</div> </div>
</Tooltip> </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> </div>
); );
}); });

View File

@ -8,23 +8,11 @@ import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useKeypress from "hooks/use-keypress"; import useKeypress from "hooks/use-keypress";
import useProjectDetails from "hooks/use-project-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// helpers // helpers
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// types // types
import { IIssue } from "types"; import { IIssue, IProject } from "types";
type Props = {
groupId?: string;
subGroupId?: string;
prePopulatedData?: Partial<IIssue>;
onSuccess?: (data: IIssue) => Promise<void> | void;
};
const defaultValues: Partial<IIssue> = {
name: "",
};
const Inputs = (props: any) => { const Inputs = (props: any) => {
const { register, setFocus, projectDetails } = props; const { register, setFocus, projectDetails } = props;
@ -48,106 +36,117 @@ const Inputs = (props: any) => {
); );
}; };
export const BoardInlineCreateIssueForm: React.FC<Props> = observer((props) => { interface IKanBanQuickAddIssueForm {
const { prePopulatedData, groupId, subGroupId } = props; 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 // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store const { workspace: workspaceStore, project: projectStore } = useMobxStore();
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = 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); const ref = useRef<HTMLFormElement>(null);
// states
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setIsOpen(false);
useKeypress("Escape", handleClose);
useOutsideClickDetector(ref, handleClose);
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { projectDetails } = useProjectDetails();
const { const {
reset, reset,
handleSubmit, handleSubmit,
register,
setFocus, setFocus,
register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<IIssue>({ defaultValues }); } = useForm<IIssue>({ defaultValues });
const handleClose = () => {
setIsOpen(false);
};
useKeypress("Escape", handleClose);
useOutsideClickDetector(ref, handleClose);
// derived values
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
useEffect(() => { useEffect(() => {
if (!isOpen) reset({ ...defaultValues }); if (!isOpen) reset({ ...defaultValues });
}, [isOpen, reset]); }, [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) => { 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 });
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
const payload = createIssuePayload(workspaceDetail!, projectDetails!, { const payload = createIssuePayload(workspaceDetail, projectDetail, {
...(prePopulatedData ?? {}), ...(prePopulatedData ?? {}),
...formData, ...formData,
}); });
try { try {
quickAddStore.createIssue( quickAddCallback &&
workspaceSlug.toString(), (await quickAddCallback(
projectId.toString(), workspaceSlug,
projectId,
{ {
group_id: groupId ?? null, ...payload,
sub_group_id: subGroupId ?? null,
}, },
payload viewId
); ));
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
} catch (err: any) { } catch (err: any) {
Object.keys(err || {}).forEach((key) => { console.error(err);
const error = err?.[key];
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: errorTitle || "Some error occurred. Please try again.", message: err?.message || "Some error occurred. Please try again.",
});
}); });
} }
}; };
return ( return (
<div> <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 <form
ref={ref} ref={ref}
onSubmit={handleSubmit(onSubmitHandler)} 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" /> <PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span> <span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button> </button>
)} )} */}
</div> </div>
); );
}); });

View File

@ -1,179 +1,48 @@
import React, { useCallback, useState } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // ui
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { CycleIssueQuickActions } from "components/issues"; import { CycleIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants import { EIssueActions } from "../../types";
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; // components
import { BaseKanBanRoot } from "../base-kanban-root";
export interface ICycleKanBanLayout {} export interface ICycleKanBanLayout {}
export const CycleKanBanLayout: React.FC = observer(() => { export const CycleKanBanLayout: React.FC = observer(() => {
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, cycleId } = router.query; const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
// store // store
const { const { cycleIssues: cycleIssueStore, cycleIssueKanBanView: cycleIssueKanBanViewStore } = useMobxStore();
project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
cycleIssue: cycleIssueStore,
issueFilter: issueFilterStore,
cycleIssueKanBanView: cycleIssueKanBanViewStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const issues = cycleIssueStore?.getIssues; const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
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") => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
if (action === "update") { cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); },
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); [EIssueActions.DELETE]: async (issue: IIssue) => {
} if (!workspaceSlug || !cycleId) return;
if (action === "delete") cycleIssueStore.deleteIssue(group_by, sub_group_by, issue); cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
if (action === "remove" && issue.bridge_id) { },
cycleIssueStore.deleteIssue(group_by, sub_group_by, issue); [EIssueActions.REMOVE]: async (issue: IIssue) => {
cycleIssueStore.removeIssueFromCycle( if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
workspaceSlug.toString(), cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
issue.project,
cycleId.toString(),
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 ( return (
<> <BaseKanBanRoot
{cycleIssueStore.loader ? ( issueActions={issueActions}
<div className="w-full h-full flex justify-center items-center"> issueStore={cycleIssueStore}
<Spinner /> kanbanViewStore={cycleIssueKanBanViewStore}
</div> showLoader={true}
) : ( QuickActions={CycleIssueQuickActions}
<div className={`relative min-w-full min-h-full h-max bg-custom-background-90 px-3 horizontal-scroll-enable`}> viewId={cycleId}
<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>
)}
</>
); );
}); });

View File

@ -1,176 +1,74 @@
import React, { useCallback, useState } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { ModuleIssueQuickActions } from "components/issues"; import { ModuleIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { EIssueActions } from "../../types";
import { BaseKanBanRoot } from "../base-kanban-root";
export interface IModuleKanBanLayout {} export interface IModuleKanBanLayout {}
export const ModuleKanBanLayout: React.FC = observer(() => { export const ModuleKanBanLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, moduleId } = router.query; const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
// store // store
const { const {
project: { workspaceProjects }, moduleIssues: moduleIssueStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
moduleIssue: moduleIssueStore,
issueFilter: issueFilterStore,
moduleIssueKanBanView: moduleIssueKanBanViewStore, moduleIssueKanBanView: moduleIssueKanBanViewStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
} = useMobxStore(); } = 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 issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
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") => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
if (action === "update") { moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId);
moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); },
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); [EIssueActions.DELETE]: async (issue: IIssue) => {
} if (!workspaceSlug || !moduleId) return;
if (action === "delete") moduleIssueStore.deleteIssue(group_by, sub_group_by, issue); moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
if (action === "remove" && issue.bridge_id) { },
moduleIssueStore.deleteIssue(group_by, null, issue); [EIssueActions.REMOVE]: async (issue: IIssue) => {
moduleIssueStore.removeIssueFromModule( if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
workspaceSlug.toString(), moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, issue.id, moduleId, issue.bridge_id);
issue.project,
moduleId.toString(),
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 ( return (
<> <BaseKanBanRoot
{moduleIssueStore.loader ? ( issueActions={issueActions}
<div className="w-full h-full flex justify-center items-center"> issueStore={moduleIssueStore}
<Spinner /> kanbanViewStore={moduleIssueKanBanViewStore}
</div> showLoader={true}
) : ( QuickActions={ModuleIssueQuickActions}
<div className={`relative min-w-full min-h-full h-max bg-custom-background-90 px-3 horizontal-scroll-enable`}> viewId={moduleId}
<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>
)}
</>
); );
}); });

View File

@ -13,6 +13,7 @@ import { Spinner } from "@plane/ui";
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../../types";
export interface IProfileIssuesKanBanLayout {} export interface IProfileIssuesKanBanLayout {}
@ -71,14 +72,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
}; };
const handleIssues = useCallback( 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 (!workspaceSlug) return;
if (action === "update") { if (action === EIssueActions.UPDATE) {
profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue); profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, 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] [profileIssuesStore, issueDetailStore, workspaceSlug]
); );
@ -104,7 +105,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? ( {currentKanBanView === "default" ? (
<KanBan <KanBan
issues={issues} issues={{}}
issueIds={[]}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -112,8 +114,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
quickActions={(sub_group_by, group_by, issue) => ( quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions <ProjectIssueQuickActions
issue={issue} issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")} handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)}
/> />
)} )}
displayProperties={displayProperties} displayProperties={displayProperties}
@ -130,7 +132,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
/> />
) : ( ) : (
<KanBanSwimLanes <KanBanSwimLanes
issues={issues} issues={{}}
issueIds={[]}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -138,8 +141,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
quickActions={(sub_group_by, group_by, issue) => ( quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions <ProjectIssueQuickActions
issue={issue} issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")} handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)}
/> />
)} )}
displayProperties={displayProperties} displayProperties={displayProperties}

View File

@ -1,18 +1,14 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { DragDropContext } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { ProjectIssueQuickActions } from "components/issues"; import { ProjectIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { EIssueActions } from "../../types";
import { BaseKanBanRoot } from "../base-kanban-root";
export interface IKanBanLayout {} export interface IKanBanLayout {}
@ -21,149 +17,31 @@ export const KanBanLayout: React.FC = observer(() => {
const { workspaceSlug } = router.query as { workspaceSlug: string }; const { workspaceSlug } = router.query as { workspaceSlug: string };
const { const {
project: { workspaceProjects }, projectIssues: issueStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
issue: issueStore,
issueFilter: issueFilterStore,
issueKanBanView: issueKanBanViewStore, issueKanBanView: issueKanBanViewStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
} = useMobxStore(); } = useMobxStore();
const issues = issueStore?.getIssues; const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
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") => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (action === "update") { await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
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);
}, },
[issueStore, issueDetailStore, workspaceSlug] [EIssueActions.DELETE]: async (issue: IIssue) => {
); if (!workspaceSlug) return;
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id);
issueKanBanViewStore.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 ( return (
<> <BaseKanBanRoot
{issueStore.loader ? ( issueActions={issueActions}
<div className="w-full h-full flex justify-center items-center"> issueStore={issueStore}
<Spinner /> kanbanViewStore={issueKanBanViewStore}
</div> showLoader={true}
) : ( QuickActions={ProjectIssueQuickActions}
<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>
)}
</>
); );
}); });

View File

@ -1,107 +1,47 @@
import React from "react"; import React from "react";
import { DragDropContext } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components import { useRouter } from "next/router";
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; // constant
// constants import { IIssue } from "types";
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { EIssueActions } from "../../types";
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
// components
import { BaseKanBanRoot } from "../base-kanban-root";
export interface IViewKanBanLayout {} export interface IViewKanBanLayout {}
export const ProjectViewKanBanLayout: React.FC = observer(() => { export const ProjectViewKanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query as { workspaceSlug: string };
const { const {
project: projectStore, viewIssues: projectViewIssuesStore,
projectMember: { projectMembers }, issueKanBanView: projectViewIssueKanBanViewStore,
projectState: projectStateStore, issueDetail: issueDetailStore,
issue: issueStore, } = useMobxStore();
issueFilter: issueFilterStore,
issueKanBanView: issueKanBanViewStore,
}: RootStore = 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; await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id);
},
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);
}; };
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { return (
issueStore.updateIssueStructure(group_by, sub_group_by, issue); <BaseKanBanRoot
}; issueActions={issueActions}
issueStore={projectViewIssuesStore}
const states = projectStateStore?.projectStates || null; kanbanViewStore={projectViewIssueKanBanViewStore}
const priorities = ISSUE_PRIORITIES || null; showLoader={true}
// const labels = projectStore?.projectLabels || null; QuickActions={ProjectIssueQuickActions}
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>
// );
}); });

View File

@ -6,11 +6,14 @@ import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
import { KanBan } from "./default"; import { KanBan } from "./default";
// types // types
import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types"; import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
// constants // constants
import { getValueFromObject } from "constants/issue"; import { getValueFromObject } from "constants/issue";
import { EIssueActions } from "../types";
interface ISubGroupSwimlaneHeader { interface ISubGroupSwimlaneHeader {
issues: any; issues: IIssueResponse;
issueIds: any;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
list: any; list: any;
@ -20,6 +23,7 @@ interface ISubGroupSwimlaneHeader {
} }
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issues, issues,
issueIds,
sub_group_by, sub_group_by,
group_by, group_by,
list, list,
@ -29,9 +33,9 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
}) => { }) => {
const calculateIssueCount = (column_id: string) => { const calculateIssueCount = (column_id: string) => {
let issueCount = 0; let issueCount = 0;
issues && issueIds &&
Object.keys(issues)?.forEach((_issueKey: any) => { Object.keys(issueIds)?.forEach((_issueKey: any) => {
issueCount += issues?.[_issueKey]?.[column_id]?.length || 0; issueCount += issueIds?.[_issueKey]?.[column_id]?.length || 0;
}); });
return issueCount; return issueCount;
}; };
@ -58,6 +62,8 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
}; };
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: IIssueResponse;
issueIds: any;
order_by: string | null; order_by: string | null;
showEmptyGroup: boolean; showEmptyGroup: boolean;
states: IState[] | null; states: IState[] | null;
@ -66,15 +72,9 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
labels: IIssueLabel[] | null; labels: IIssueLabel[] | null;
members: IUserLite[] | null; members: IUserLite[] | null;
projects: IProject[] | null; projects: IProject[] | null;
issues: any; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
handleIssues: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
isDragStarted?: boolean; isDragStarted?: boolean;
@ -82,6 +82,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => { const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const { const {
issues, issues,
issueIds,
sub_group_by, sub_group_by,
group_by, group_by,
order_by, order_by,
@ -104,9 +105,9 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const calculateIssueCount = (column_id: string) => { const calculateIssueCount = (column_id: string) => {
let issueCount = 0; let issueCount = 0;
issues?.[column_id] && issueIds?.[column_id] &&
Object.keys(issues?.[column_id])?.forEach((_list: any) => { Object.keys(issueIds?.[column_id])?.forEach((_list: any) => {
issueCount += issues?.[column_id]?.[_list]?.length || 0; issueCount += issueIds?.[column_id]?.[_list]?.length || 0;
}); });
return issueCount; return issueCount;
}; };
@ -134,7 +135,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
{!kanBanToggle?.subgroupByIssuesVisibility.includes(getValueFromObject(_list, listKey) as string) && ( {!kanBanToggle?.subgroupByIssuesVisibility.includes(getValueFromObject(_list, listKey) as string) && (
<div className="relative"> <div className="relative">
<KanBan <KanBan
issues={issues?.[getValueFromObject(_list, listKey) as string]} issues={issues}
issueIds={issueIds?.[getValueFromObject(_list, listKey) as string]}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -163,18 +165,14 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
}); });
export interface IKanBanSwimLanes { export interface IKanBanSwimLanes {
issues: any; issues: IIssueResponse;
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
order_by: string | null; order_by: string | null;
handleIssues: ( handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | null;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
showEmptyGroup: boolean; showEmptyGroup: boolean;
@ -190,6 +188,7 @@ export interface IKanBanSwimLanes {
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => { export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const { const {
issues, issues,
issueIds,
sub_group_by, sub_group_by,
group_by, group_by,
order_by, order_by,
@ -214,6 +213,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{group_by && group_by === "project" && ( {group_by && group_by === "project" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={projects} list={projects}
@ -226,6 +226,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{group_by && group_by === "state" && ( {group_by && group_by === "state" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={states} list={states}
@ -238,6 +239,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{group_by && group_by === "state_detail.group" && ( {group_by && group_by === "state_detail.group" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={stateGroups} list={stateGroups}
@ -250,6 +252,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{group_by && group_by === "priority" && ( {group_by && group_by === "priority" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={priorities} list={priorities}
@ -262,6 +265,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{group_by && group_by === "labels" && ( {group_by && group_by === "labels" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={labels ? [...labels, { id: "None", name: "None" }] : labels} 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" && ( {group_by && group_by === "assignees" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={members ? [...members, { id: "None", display_name: "None" }] : members} 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" && ( {group_by && group_by === "created_by" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={members} list={members}
@ -299,6 +305,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && sub_group_by === "project" && ( {sub_group_by && sub_group_by === "project" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -323,6 +330,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && sub_group_by === "state" && ( {sub_group_by && sub_group_by === "state" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -347,6 +355,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && sub_group_by === "state" && ( {sub_group_by && sub_group_by === "state" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_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" && ( {sub_group_by && sub_group_by === "state_detail.group" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -395,6 +405,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && sub_group_by === "priority" && ( {sub_group_by && sub_group_by === "priority" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -419,6 +430,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && sub_group_by === "labels" && ( {sub_group_by && sub_group_by === "labels" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}
@ -443,6 +455,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
{sub_group_by && sub_group_by === "assignees" && ( {sub_group_by && sub_group_by === "assignees" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_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" && ( {sub_group_by && sub_group_by === "created_by" && (
<SubGroupSwimlane <SubGroupSwimlane
issues={issues} issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
order_by={order_by} order_by={order_by}

View 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>
)}
</>
);
});

View File

@ -1,26 +1,27 @@
// components // components
import { KanBanProperties } from "./properties"; import { ListProperties } from "./properties";
import { IssuePeekOverview } from "components/issues/issue-peek-overview"; import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Spinner, Tooltip } from "@plane/ui";
// types // types
import { IIssue, IIssueDisplayProperties } from "types"; import { IIssue, IIssueDisplayProperties } from "types";
import { EIssueActions } from "../types";
interface IssueBlockProps { interface IssueBlockProps {
columnId: string; columnId: string;
issue: IIssue; 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; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | undefined;
isReadonly?: boolean; isReadonly?: boolean;
showEmptyGroup?: boolean;
} }
export const IssueBlock: React.FC<IssueBlockProps> = (props) => { 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) => { const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
handleIssues(group_by, issueToUpdate, "update"); handleIssues(issueToUpdate, EIssueActions.UPDATE);
}; };
return ( return (
@ -31,16 +32,18 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
{issue?.project_detail?.identifier}-{issue.sequence_id} {issue?.project_detail?.identifier}-{issue.sequence_id}
</div> </div>
)} )}
{issue?.tempId !== undefined && ( {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="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
)} )}
<IssuePeekOverview <IssuePeekOverview
workspaceSlug={issue?.workspace_detail?.slug} workspaceSlug={issue?.workspace_detail?.slug}
projectId={issue?.project_detail?.id} projectId={issue?.project_detail?.id}
issueId={issue?.id} issueId={issue?.id}
isArchived={issue?.archived_at !== null} isArchived={issue?.archived_at !== null}
handleIssue={(issueToUpdate) => { handleIssue={(issueToUpdate) => {
handleIssues(!columnId && columnId === "null" ? null : columnId, issueToUpdate as IIssue, "update"); handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE);
}} }}
> >
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
@ -49,15 +52,22 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
</IssuePeekOverview> </IssuePeekOverview>
<div className="ml-auto flex-shrink-0 flex items-center gap-2"> <div className="ml-auto flex-shrink-0 flex items-center gap-2">
<KanBanProperties {!issue?.tempId ? (
<>
<ListProperties
columnId={columnId} columnId={columnId}
issue={issue} issue={issue}
isReadonly={isReadonly} isReadonly={isReadonly}
handleIssues={updateIssue} handleIssues={updateIssue}
displayProperties={displayProperties} displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
/> />
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)} {quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
</>
) : (
<div className="w-4 h-4">
<Spinner className="w-4 h-4" />
</div>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -3,33 +3,34 @@ import { FC } from "react";
import { IssueBlock } from "components/issues"; import { IssueBlock } from "components/issues";
// types // types
import { IIssue, IIssueDisplayProperties } from "types"; import { IIssue, IIssueDisplayProperties } from "types";
import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import { EIssueActions } from "../types";
interface Props { interface Props {
columnId: string; columnId: string;
issues: IIssue[]; issueIds: IGroupedIssues | TUnGroupedIssues | any;
issues: IIssueResponse;
isReadonly?: boolean; 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; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | undefined;
showEmptyGroup?: boolean;
} }
export const IssueBlocksList: FC<Props> = (props) => { 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 ( return (
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200"> <div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
{issues && issues.length > 0 ? ( {issueIds && issueIds.length > 0 ? (
issues.map((issue) => ( issueIds.map((issueId: string) => (
<IssueBlock <IssueBlock
key={issue.id} key={issues[issueId].id}
columnId={columnId} columnId={columnId}
issue={issue} issue={issues[issueId]}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
isReadonly={isReadonly} isReadonly={isReadonly}
displayProperties={displayProperties} displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
/> />
)) ))
) : ( ) : (

View File

@ -1,243 +1,321 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite";
// components // components
import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { ListGroupByHeaderRoot } from "./headers/group-by-root";
import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues"; import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
// types // 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 // constants
import { getValueFromObject } from "constants/issue"; import { getValueFromObject } from "constants/issue";
export interface IGroupByList { export interface IGroupByList {
issueIds: IGroupedIssues | TUnGroupedIssues | any;
issues: any; issues: any;
group_by: string | null; group_by: string | null;
list: any; list: any;
isReadonly?: boolean;
listKey: string; listKey: string;
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; states: IState[] | null;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties;
is_list?: boolean; 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; 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 { const {
issueIds,
issues, issues,
group_by, group_by,
list, list,
isReadonly,
listKey, listKey,
is_list = false,
states,
handleIssues, handleIssues,
quickActions, quickActions,
displayProperties, displayProperties,
is_list = false, enableIssueQuickAdd,
enableQuickIssueCreate,
showEmptyGroup, showEmptyGroup,
isReadonly,
quickAddCallback,
viewId,
} = props; } = 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 ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any) => ( 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 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"> <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 <ListGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string} column_id={getValueFromObject(_list, listKey) as string}
column_value={_list} column_value={_list}
group_by={group_by} group_by={group_by}
issues_count={ issues_count={
is_list ? issues?.length || 0 : issues?.[getValueFromObject(_list, listKey) as string]?.length || 0 is_list
? issueIds?.length || 0
: issueIds?.[getValueFromObject(_list, listKey) as string]?.length || 0
} }
/> />
</div> </div>
{issues && ( {issues && (
<IssueBlocksList <IssueBlocksList
columnId={getValueFromObject(_list, listKey) as string} columnId={getValueFromObject(_list, listKey) as string}
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]} issueIds={is_list ? issueIds || 0 : issueIds?.[getValueFromObject(_list, listKey) as string] || 0}
issues={issues}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
isReadonly={isReadonly} isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
/> />
)} )}
{enableQuickIssueCreate && (
<ListInlineCreateIssueForm {enableIssueQuickAdd && (
groupId={getValueFromObject(_list, listKey) as string} <div className="flex-shrink-0 w-full sticky bottom-0 z-[1]">
prePopulatedData={{ <ListQuickAddIssueForm
[group_by!]: getValueFromObject(_list, listKey), prePopulatedData={prePopulateQuickAddData(group_by, getValueFromObject(_list, listKey))}
}} quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
</div>
)} )}
</div> </div>
))} )
)}
</div> </div>
); );
}); };
// TODO: update all the types
export interface IList { export interface IList {
issues: any; issueIds: IGroupedIssues | TUnGroupedIssues | any;
issues: IIssueResponse | undefined;
group_by: string | null; group_by: string | null;
isReadonly?: boolean; handleIssues: (issue: IIssue, action: EIssueActions) => Promise<void>;
handleDragDrop?: (result: any) => void | undefined;
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | undefined;
showEmptyGroup: boolean;
enableIssueQuickAdd: boolean;
isReadonly: boolean;
states: IState[] | null; states: IState[] | null;
labels: IIssueLabel[] | null; labels: IIssueLabel[] | null;
members: IUserLite[] | null; members: IUserLite[] | null;
projects: IProject[] | null; projects: IProject[] | null;
stateGroups: any; stateGroups: any;
priorities: any; priorities: any;
enableQuickIssueCreate?: boolean; quickAddCallback?: (
estimates: IEstimatePoint[] | null; workspaceSlug: string,
showEmptyGroup?: boolean; 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 { const {
issueIds,
issues, issues,
group_by, group_by,
isReadonly,
handleIssues, handleIssues,
quickActions, quickActions,
quickAddCallback,
viewId,
displayProperties, displayProperties,
showEmptyGroup,
enableIssueQuickAdd,
isReadonly,
states, states,
stateGroups,
priorities,
labels, labels,
members, members,
projects, projects,
stateGroups,
priorities,
showEmptyGroup,
enableQuickIssueCreate,
} = props; } = props;
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
{group_by === null && ( {group_by === null && (
<GroupByList <GroupByList
issueIds={issueIds as TUnGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={[{ id: "null", title: "All Issues" }]} list={[{ id: `null`, title: `All Issues` }]}
listKey={`id`} listKey={`id`}
is_list={true}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
is_list enableIssueQuickAdd={enableIssueQuickAdd}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "project" && projects && ( {group_by && group_by === "project" && projects && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={projects} list={projects}
listKey={`id`} listKey={`id`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "state" && states && ( {group_by && group_by === "state" && states && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={states} list={states}
listKey={`id`} listKey={`id`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "state_detail.group" && stateGroups && ( {group_by && group_by === "state_detail.group" && stateGroups && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={stateGroups} list={stateGroups}
listKey={`key`} listKey={`key`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "priority" && priorities && ( {group_by && group_by === "priority" && priorities && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={priorities} list={priorities}
listKey={`key`} listKey={`key`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "labels" && labels && ( {group_by && group_by === "labels" && labels && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={[...labels, { id: "None", name: "None" }]} list={[...labels, { id: "None", name: "None" }]}
listKey={`id`} listKey={`id`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "assignees" && members && ( {group_by && group_by === "assignees" && members && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={[...members, { id: "None", display_name: "None" }]} list={[...members, { id: "None", display_name: "None" }]}
listKey={`id`} listKey={`id`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
{group_by && group_by === "created_by" && members && ( {group_by && group_by === "created_by" && members && (
<GroupByList <GroupByList
issueIds={issueIds as IGroupedIssues}
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
list={members} list={members}
listKey={`id`} listKey={`id`}
states={states}
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
enableIssueQuickAdd={enableIssueQuickAdd}
isReadonly={isReadonly}
quickAddCallback={quickAddCallback}
viewId={viewId}
/> />
)} )}
</div> </div>
); );
}); };

View File

@ -1,8 +1,5 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// services
import { ModuleService } from "services/module.service";
import { IssueService } from "services/issue";
// lucide icons // lucide icons
import { CircleDashed, Plus } from "lucide-react"; import { CircleDashed, Plus } from "lucide-react";
// components // components
@ -23,18 +20,15 @@ interface IHeaderGroupByCard {
issuePayload: Partial<IIssue>; issuePayload: Partial<IIssue>;
} }
const moduleService = new ModuleService();
const issueService = new IssueService();
export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }: IHeaderGroupByCard) => { export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }: IHeaderGroupByCard) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId, cycleId } = router.query; const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
const [isOpen, setIsOpen] = React.useState(false);
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const verticalAlignPosition = false; const [isOpen, setIsOpen] = React.useState(false);
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
const renderExistingIssueModal = moduleId || cycleId; const renderExistingIssueModal = moduleId || cycleId;
const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true }; 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), issues: data.map((i) => i.id),
}; };
await moduleService // await moduleService
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload) // .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user)
.catch(() => // .catch(() =>
setToastAlert({ // setToastAlert({
type: "error", // type: "error",
title: "Error!", // title: "Error!",
message: "Selected issues could not be added to the module. Please try again.", // message: "Selected issues could not be added to the module. Please try again.",
}) // })
); // );
}; };
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
@ -64,46 +58,27 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
issues: data.map((i) => i.id), issues: data.map((i) => i.id),
}; };
await issueService // await issueService
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload) // .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
.catch(() => { // .catch(() => {
setToastAlert({ // setToastAlert({
type: "error", // type: "error",
title: "Error!", // title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.", // message: "Selected issues could not be added to the cycle. Please try again.",
}); // });
}); // });
}; };
return ( return (
<> <>
<CreateUpdateIssueModal isOpen={isOpen} handleClose={() => setIsOpen(false)} prePopulateData={issuePayload} /> <div className="flex-shrink-0 relative flex gap-2 py-1.5 flex-row items-center w-full">
{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 w-5 h-5 rounded-sm overflow-hidden flex justify-center items-center"> <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} />} {icon ? icon : <CircleDashed className="h-3.5 w-3.5" strokeWidth={2} />}
</div> </div>
<div className={`flex items-center gap-1 ${verticalAlignPosition ? `flex-col` : `flex-row w-full`}`}> <div className="flex items-center gap-1 flex-row w-full">
<div <div className="font-medium line-clamp-1 text-custom-text-100">{title}</div>
className={`font-medium line-clamp-1 text-custom-text-100 ${verticalAlignPosition ? `vertical-lr` : ``}`} <div className="text-sm font-medium text-custom-text-300 pl-2">{count || 0}</div>
>
{title}
</div>
<div className={`text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}>
{count || 0}
</div>
</div> </div>
{renderExistingIssueModal ? ( {renderExistingIssueModal ? (
@ -130,6 +105,25 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
<Plus width={14} strokeWidth={2} /> <Plus width={14} strokeWidth={2} />
</div> </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> </div>
</> </>
); );

View File

@ -2,4 +2,4 @@ export * from "./roots";
export * from "./block"; export * from "./block";
export * from "./roots"; export * from "./roots";
export * from "./blocks-list"; export * from "./blocks-list";
export * from "./inline-create-issue-form"; export * from "./quick-add-issue-form";

View File

@ -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>
);
});

View File

@ -0,0 +1,6 @@
export interface IQuickActionProps {
issue: IIssue;
handleDelete: () => Promise<void>;
handleUpdate?: (data: IIssue) => Promise<void>;
handleRemoveFromView?: () => Promise<void>;
}

View File

@ -13,17 +13,16 @@ import { Tooltip } from "@plane/ui";
// types // types
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
export interface IKanBanProperties { export interface IListProperties {
columnId: string; columnId: string;
issue: IIssue; issue: IIssue;
handleIssues: (group_by: string | null, issue: IIssue) => void; handleIssues: (group_by: string | null, issue: IIssue) => void;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties | undefined;
isReadonly?: boolean; isReadonly?: boolean;
showEmptyGroup?: boolean;
} }
export const KanBanProperties: FC<IKanBanProperties> = observer((props) => { export const ListProperties: FC<IListProperties> = observer((props) => {
const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly, showEmptyGroup } = props; const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly } = props;
const handleState = (state: IState) => { const handleState = (state: IState) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); 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 && ( {displayProperties && displayProperties?.state && (
<IssuePropertyState <IssuePropertyState
projectId={issue?.project_detail?.id || null} projectId={issue?.project_detail?.id || null}
value={issue?.state_detail || null} value={issue?.state || null}
hideDropdownArrow hideDropdownArrow
onChange={handleState} onChange={handleState}
disabled={isReadonly} disabled={isReadonly}
@ -78,7 +77,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)} )}
{/* label */} {/* label */}
{displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( {displayProperties && displayProperties?.labels && (
<IssuePropertyLabels <IssuePropertyLabels
projectId={issue?.project_detail?.id || null} projectId={issue?.project_detail?.id || null}
value={issue?.labels || null} value={issue?.labels || null}
@ -89,7 +88,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)} )}
{/* assignee */} {/* assignee */}
{displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && ( {displayProperties && displayProperties?.assignee && (
<IssuePropertyAssignee <IssuePropertyAssignee
projectId={issue?.project_detail?.id || null} projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null} value={issue?.assignees || null}
@ -101,7 +100,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)} )}
{/* start date */} {/* start date */}
{displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && ( {displayProperties && displayProperties?.start_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.start_date || null} value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)} onChange={(date: string) => handleStartDate(date)}
@ -111,7 +110,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)} )}
{/* target/due date */} {/* target/due date */}
{displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && ( {displayProperties && displayProperties?.due_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.target_date || null} value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} onChange={(date: string) => handleTargetDate(date)}

View File

@ -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>
);
});

View File

@ -4,73 +4,42 @@ import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "../default";
import { ArchivedIssueQuickActions } from "components/issues"; import { ArchivedIssueQuickActions } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants // 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(() => { export const ArchivedIssueListLayout: FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { const { archivedIssues: archivedIssueStore, archivedIssueFilters: archivedIssueFiltersStore } = useMobxStore();
project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
projectEstimates: { projectEstimates },
archivedIssues: archivedIssueStore,
archivedIssueFilters: archivedIssueFiltersStore,
} = useMobxStore();
// derived values const issueActions = {
const issues = archivedIssueStore.getIssues; [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
const displayProperties = archivedIssueFiltersStore?.userDisplayProperties || null;
const group_by: string | null = archivedIssueFiltersStore?.userDisplayFilters?.group_by || null;
const showEmptyGroup = archivedIssueFiltersStore?.userDisplayFilters?.show_empty_groups || false;
const handleIssues = (group_by: string | null, issue: IIssue, action: "delete" | "update") => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
if (action === "delete") { archivedIssueStore.deleteArchivedIssue(group_by, null, issue);
archivedIssueStore.deleteArchivedIssue(group_by === "null" ? null : group_by, null, issue); },
archivedIssueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
}
}; };
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; return 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 ( // return (
<div className="relative w-full h-full bg-custom-background-90"> // <BaseListRoot
<List // issueFilterStore={archivedIssueFiltersStore}
issues={issues} // issueStore={archivedIssueStore}
group_by={group_by} // QuickActions={ArchivedIssueQuickActions}
isReadonly // issueActions={issueActions}
handleIssues={handleIssues} // getProjects={getProjects}
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>
);
}); });

View File

@ -1,96 +1,52 @@
import React, { useCallback } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "../default";
import { CycleIssueQuickActions } from "components/issues"; import { CycleIssueQuickActions } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants // 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 interface ICycleListLayout {}
export const CycleListLayout: React.FC = observer(() => { export const CycleListLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, cycleId } = router.query; const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
// store // store
const { const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
projectEstimates: { projectEstimates },
issueFilter: issueFilterStore,
cycleIssue: cycleIssueStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const { currentProjectDetails } = projectStore;
const issues = cycleIssueStore?.getIssues; const issueActions = {
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
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") => {
if (!workspaceSlug || !cycleId) return; if (!workspaceSlug || !cycleId) return;
cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
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, issueDetailStore, cycleId, workspaceSlug] [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
); if (!workspaceSlug || !cycleId) return;
cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
const states = projectStateStore?.projectStates || null; },
const priorities = ISSUE_PRIORITIES || null; [EIssueActions.REMOVE]: (group_by: string | null, issue: IIssue) => {
const stateGroups = ISSUE_STATE_GROUPS || null; if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
const estimates = },
currentProjectDetails?.estimate !== null };
? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null const getProjects = (projectStore: IProjectStore) => {
: null; if (!workspaceSlug) return null;
return projectStore?.projects[workspaceSlug] || null;
};
return ( return (
<div className={`relative w-full h-full bg-custom-background-90`}> <BaseListRoot
<List issueFilterStore={cycleIssueFilterStore}
issues={issues} issueStore={cycleIssueStore}
group_by={group_by} QuickActions={CycleIssueQuickActions}
handleIssues={handleIssues} issueActions={issueActions}
quickActions={(group_by, issue) => ( getProjects={getProjects}
<CycleIssueQuickActions viewId={cycleId}
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>
); );
}); });

View File

@ -1,96 +1,53 @@
import React, { useCallback } from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "../default";
import { ModuleIssueQuickActions } from "components/issues"; import { ModuleIssueQuickActions } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../../types";
// constants // 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 interface IModuleListLayout {}
export const ModuleListLayout: React.FC = observer(() => { export const ModuleListLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, moduleId } = router.query; const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
const { const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
projectEstimates: { projectEstimates },
issueFilter: issueFilterStore,
moduleIssue: moduleIssueStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const { currentProjectDetails } = projectStore;
const issues = moduleIssueStore?.getIssues; const issueActions = {
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
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") => {
if (!workspaceSlug || !moduleId) return; if (!workspaceSlug || !moduleId) return;
moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
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, 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 getProjects = (projectStore: IProjectStore) => {
const priorities = ISSUE_PRIORITIES || null; if (!workspaceSlug) return null;
const stateGroups = ISSUE_STATE_GROUPS || null; return projectStore?.projects[workspaceSlug] || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; };
const estimates =
currentProjectDetails?.estimate !== null
? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
: null;
return ( return (
<div className="relative w-full h-full bg-custom-background-90"> <BaseListRoot
<List issueFilterStore={moduleIssueFilterStore}
issues={issues} issueStore={moduleIssueStore}
group_by={group_by} QuickActions={ModuleIssueQuickActions}
handleIssues={handleIssues} issueActions={issueActions}
quickActions={(group_by, issue) => ( getProjects={getProjects}
<ModuleIssueQuickActions viewId={moduleId}
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>
); );
}); });

View File

@ -1,24 +1,19 @@
import { FC, useCallback } from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "../default";
import { ProjectIssueQuickActions } from "components/issues"; import { ProjectIssueQuickActions } from "components/issues";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants import { EIssueActions } from "../../types";
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; import { IProjectStore } from "store/project";
//components
export interface IProfileIssuesListLayout {} import { BaseListRoot } from "../base-list-root";
export const ProfileIssuesListLayout: FC = observer(() => { export const ProfileIssuesListLayout: FC = observer(() => {
const { const {
workspace: workspaceStore,
projectState: projectStateStore,
project: projectStore,
projectMember: { projectMembers },
profileIssueFilters: profileIssueFiltersStore, profileIssueFilters: profileIssueFiltersStore,
profileIssues: profileIssuesStore, profileIssues: profileIssuesStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
@ -27,53 +22,29 @@ export const ProfileIssuesListLayout: FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const issues = profileIssuesStore?.getIssues; const issueActions = {
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
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") => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (action === "update") {
profileIssuesStore.updateIssueStructure(group_by, null, issue); profileIssuesStore.updateIssueStructure(group_by, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
}
if (action === "delete") profileIssuesStore.deleteIssue(group_by, null, issue);
}, },
[profileIssuesStore, issueDetailStore, workspaceSlug] [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => {
); profileIssuesStore.deleteIssue(group_by, null, issue);
},
};
const states = projectStateStore?.projectStates || null; const getProjects = (projectStore: IProjectStore) => projectStore?.workspaceProjects || null;
const priorities = ISSUE_PRIORITIES || null;
const labels = workspaceStore.workspaceLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = projectStore?.workspaceProjects || null;
return ( return null;
<div className={`relative w-full h-full bg-custom-background-90`}>
<List // return (
issues={issues} // <BaseListRoot
group_by={group_by} // issueFilterStore={profileIssueFiltersStore}
handleIssues={handleIssues} // issueStore={profileIssuesStore}
quickActions={(group_by, issue) => ( // QuickActions={ProjectIssueQuickActions}
<ProjectIssueQuickActions // issueActions={issueActions}
issue={issue} // getProjects={getProjects}
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>
);
}); });

View File

@ -1,95 +1,46 @@
import { FC, useCallback } from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { List } from "../default";
import { ProjectIssueQuickActions } from "components/issues"; import { ProjectIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { EIssueActions } from "../../types";
// constants // 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(() => { export const ListLayout: FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
if (!workspaceSlug || !projectId) return null;
// store // store
const { const { projectIssuesFilter: projectIssuesFilterStore, projectIssues: projectIssuesStore } = useMobxStore();
project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
projectEstimates: { projectEstimates },
issue: issueStore,
issueDetail: issueDetailStore,
issueFilter: issueFilterStore,
} = useMobxStore();
const { currentProjectDetails } = projectStore;
const issues = issueStore?.getIssues; const issueActions = {
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; if (!workspaceSlug || !projectId) return;
const group_by: string | null = userDisplayFilters?.group_by || null; projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue);
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);
}, },
[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 getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects;
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;
return ( return (
<> <BaseListRoot
{issueStore.loader ? ( issueFilterStore={projectIssuesFilterStore}
<div className="w-full h-full flex justify-center items-center"> issueStore={projectIssuesStore}
<Spinner /> QuickActions={ProjectIssueQuickActions}
</div> issueActions={issueActions}
) : ( getProjects={getProjects}
<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>
)}
</>
); );
}); });

View File

@ -1,57 +1,49 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components
import { List } from "../default";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
// constants // 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 interface IViewListLayout {}
export const ProjectViewListLayout: React.FC = observer(() => { export const ProjectViewListLayout: React.FC = observer(() => {
const { const { viewIssues: projectViewIssueStore, viewIssuesFilter: projectViewIssueFilterStore }: RootStore =
project: projectStore, useMobxStore();
issue: issueStore,
issueFilter: issueFilterStore,
projectState: projectStateStore,
}: 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 issueActions = {
[EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => {
const updateIssue = (group_by: string | null, issue: any) => { if (!workspaceSlug || !projectId) return;
issueStore.updateIssueStructure(group_by, null, issue); 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 getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects;
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 (
<BaseListRoot
// return ( issueFilterStore={projectViewIssueFilterStore}
// <div className={`relative w-full h-full bg-custom-background-90`}> issueStore={projectViewIssueStore}
// <List QuickActions={ProjectIssueQuickActions}
// issues={issues} issueActions={issueActions}
// group_by={group_by} getProjects={getProjects}
// handleIssues={updateIssue} />
// display_properties={display_properties} );
// states={states}
// stateGroups={stateGroups}
// priorities={priorities}
// labels={labels}
// members={members}
// projects={projects}
// estimates={estimates}
// />
// </div>
// );
}); });

View File

@ -17,7 +17,7 @@ import { RootStore } from "store/root";
export interface IIssuePropertyState { export interface IIssuePropertyState {
view?: "profile" | "workspace" | "project"; view?: "profile" | "workspace" | "project";
projectId: string | null; projectId: string | null;
value: IState; value: any | string | null;
onChange: (state: IState) => void; onChange: (state: IState) => void;
disabled?: boolean; disabled?: boolean;
hideDropdownArrow?: boolean; hideDropdownArrow?: boolean;
@ -62,6 +62,9 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
projectStateStore.fetchProjectStates(workspaceSlug, projectId).then(() => setIsLoading(false)); 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) => ({ const dropdownOptions = projectStates?.map((state) => ({
value: state.id, value: state.id,
query: state.name, query: state.name,
@ -91,10 +94,10 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
: dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); : dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const label = ( 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"> <div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
{value && <StateGroupIcon stateGroup={value.group} color={value.color} />} {selectedOption && <StateGroupIcon stateGroup={selectedOption?.group as any} color={selectedOption?.color} />}
<span className="truncate line-clamp-1 inline-block w-auto max-w-[100px]">{value?.name ?? "State"}</span> <span className="truncate line-clamp-1 inline-block">{selectedOption?.name ?? "State"}</span>
</div> </div>
</Tooltip> </Tooltip>
); );
@ -104,8 +107,8 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
{workspaceSlug && projectId && ( {workspaceSlug && projectId && (
<Combobox <Combobox
as="div" as="div"
className={`text-left w-auto max-w-full ${className}`} className={`flex-shrink-0 text-left w-auto max-w-full ${className}`}
value={value.id} value={selectedOption?.id}
onChange={(data: string) => { onChange={(data: string) => {
const selectedState = projectStates?.find((state) => state.id === data); const selectedState = projectStates?.find((state) => state.id === data);
if (selectedState) onChange(selectedState); if (selectedState) onChange(selectedState);

View File

@ -9,14 +9,9 @@ import { DeleteArchivedIssueModal } from "components/issues";
// helpers // helpers
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IQuickActionProps } from "../list/list-view-types";
type Props = { export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
issue: IIssue;
handleDelete: () => Promise<void>;
};
export const ArchivedIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete } = props; const { issue, handleDelete } = props;
const router = useRouter(); const router = useRouter();

View File

@ -10,16 +10,10 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { IQuickActionProps } from "../list/list-view-types";
type Props = { export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
issue: IIssue; const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props;
handleDelete: () => Promise<void>;
handleUpdate: (data: IIssue) => Promise<void>;
handleRemoveFromCycle: () => Promise<void>;
};
export const CycleIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromCycle } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -59,7 +53,7 @@ export const CycleIssueQuickActions: React.FC<Props> = (props) => {
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
data={issueToEdit} data={issueToEdit}
onSubmit={async (data) => { onSubmit={async (data) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data });
}} }}
/> />
<CustomMenu placement="bottom-start" ellipsis> <CustomMenu placement="bottom-start" ellipsis>
@ -92,7 +86,7 @@ export const CycleIssueQuickActions: React.FC<Props> = (props) => {
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
handleRemoveFromCycle(); handleRemoveFromView && handleRemoveFromView();
}} }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -10,16 +10,10 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { IQuickActionProps } from "../list/list-view-types";
type Props = { export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
issue: IIssue; const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props;
handleDelete: () => Promise<void>;
handleUpdate: (data: IIssue) => Promise<void>;
handleRemoveFromModule: () => Promise<void>;
};
export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate, handleRemoveFromModule } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -59,7 +53,7 @@ export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
data={issueToEdit} data={issueToEdit}
onSubmit={async (data) => { onSubmit={async (data) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data });
}} }}
/> />
<CustomMenu placement="bottom-start" ellipsis> <CustomMenu placement="bottom-start" ellipsis>
@ -92,7 +86,7 @@ export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
handleRemoveFromModule(); handleRemoveFromView && handleRemoveFromView();
}} }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -10,14 +10,9 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { IQuickActionProps } from "../list/list-view-types";
type Props = { export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
issue: IIssue;
handleDelete: () => Promise<void>;
handleUpdate: (data: IIssue) => Promise<void>;
};
export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleDelete, handleUpdate } = props; const { issue, handleDelete, handleUpdate } = props;
const router = useRouter(); const router = useRouter();
@ -58,7 +53,7 @@ export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}}
data={issueToEdit} data={issueToEdit}
onSubmit={async (data) => { onSubmit={async (data) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data });
}} }}
/> />
<CustomMenu placement="bottom-start" ellipsis> <CustomMenu placement="bottom-start" ellipsis>

View File

@ -24,28 +24,29 @@ export const CycleLayoutRoot: React.FC = observer(() => {
const [transferIssuesModal, setTransferIssuesModal] = useState(false); const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query as {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
const { const {
issueFilter: issueFilterStore,
cycle: cycleStore, cycle: cycleStore,
cycleIssue: cycleIssueStore, cycleIssues: { loader, getIssues, fetchIssues },
cycleIssueFilter: cycleIssueFilterStore, cycleIssuesFilter: { issueFilters, fetchFilters },
} = useMobxStore(); } = useMobxStore();
useSWR(workspaceSlug && projectId && cycleId ? `CYCLE_FILTERS_AND_ISSUES_${cycleId.toString()}` : null, async () => { useSWR(
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
async () => {
if (workspaceSlug && projectId && cycleId) { if (workspaceSlug && projectId && cycleId) {
// fetching the project display filters and display properties await fetchFilters(workspaceSlug, projectId, cycleId);
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", cycleId);
// 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());
} }
}); }
);
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined; const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined;
const cycleStatus = const cycleStatus =
@ -53,29 +54,22 @@ export const CycleLayoutRoot: React.FC = observer(() => {
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft"; : "draft";
const issueCount = cycleIssueStore.getIssuesCount;
if (!cycleIssueStore.getIssues)
return (
<div className="h-full w-full grid place-items-center">
<Spinner />
</div>
);
return ( return (
<> <>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> <TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
<div className="relative w-full h-full flex flex-col overflow-hidden"> <div className="relative w-full h-full flex flex-col overflow-hidden">
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />} {cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot /> <CycleAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<CycleEmptyState {loader === "init-loader" ? (
workspaceSlug={workspaceSlug?.toString()} <div className="w-full h-full flex justify-center items-center">
projectId={projectId?.toString()} <Spinner />
cycleId={cycleId?.toString()} </div>
/>
) : ( ) : (
<div className="w-full h-full overflow-auto"> <>
{/* <CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} /> */}
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? ( {activeLayout === "list" ? (
<CycleListLayout /> <CycleListLayout />
) : activeLayout === "kanban" ? ( ) : activeLayout === "kanban" ? (
@ -88,6 +82,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
<CycleSpreadsheetLayout /> <CycleSpreadsheetLayout />
) : null} ) : null}
</div> </div>
</>
)} )}
</div> </div>
</> </>

View File

@ -27,47 +27,33 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
}; };
const { const {
issueFilter: issueFilterStore, moduleIssues: { loader, getIssues, fetchIssues },
moduleIssue: moduleIssueStore, moduleIssuesFilter: { issueFilters, fetchFilters },
moduleFilter: moduleIssueFilterStore,
} = useMobxStore(); } = useMobxStore();
useSWR( useSWR(
workspaceSlug && projectId && moduleId ? `MODULE_FILTERS_AND_ISSUES_${moduleId.toString()}` : null, workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null,
async () => { async () => {
if (workspaceSlug && projectId && moduleId) { if (workspaceSlug && projectId && moduleId) {
// fetching the project display filters and display properties await fetchFilters(workspaceSlug, projectId, moduleId);
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId); await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", moduleId);
// fetching the module filters
await moduleIssueFilterStore.fetchModuleFilters(workspaceSlug, projectId, moduleId);
// fetching the module issues
await moduleIssueStore.fetchIssues(workspaceSlug, projectId, moduleId);
} }
} }
); );
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilters?.displayFilters?.layout || undefined;
const issueCount = moduleIssueStore.getIssuesCount;
if (!moduleIssueStore.getIssues)
return (
<div className="h-full w-full grid place-items-center">
<Spinner />
</div>
);
return ( return (
<div className="relative w-full h-full flex flex-col overflow-hidden"> <div className="relative w-full h-full flex flex-col overflow-hidden">
<ModuleAppliedFiltersRoot /> <ModuleAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ModuleEmptyState {loader === "init-loader" ? (
workspaceSlug={workspaceSlug?.toString()} <div className="w-full h-full flex justify-center items-center">
projectId={projectId?.toString()} <Spinner />
moduleId={moduleId?.toString()} </div>
/>
) : ( ) : (
<>
{/* <ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} /> */}
<div className="h-full w-full overflow-auto"> <div className="h-full w-full overflow-auto">
{activeLayout === "list" ? ( {activeLayout === "list" ? (
<ModuleListLayout /> <ModuleListLayout />
@ -81,6 +67,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
<ModuleSpreadsheetLayout /> <ModuleSpreadsheetLayout />
) : null} ) : null}
</div> </div>
</>
)} )}
</div> </div>
); );

View File

@ -17,36 +17,36 @@ import {
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
export const ProjectLayoutRoot: React.FC = observer(() => { export const ProjectLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter(); 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) { if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); await fetchFilters(workspaceSlug, projectId);
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString()); await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
} }
}); });
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const issueCount = issueStore.getIssuesCount;
if (!issueStore.getIssues)
return (
<div className="h-full w-full grid place-items-center">
<Spinner />
</div>
);
return ( return (
<div className="relative w-full h-full flex flex-col overflow-hidden"> <div className="relative w-full h-full flex flex-col overflow-hidden">
<ProjectAppliedFiltersRoot /> <ProjectAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ProjectEmptyState /> {loader === "init-loader" ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : ( ) : (
<div className="w-full h-full overflow-auto"> <>
{/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && <ProjectEmptyState />} */}
<div className="w-full h-full relative overflow-auto">
{activeLayout === "list" ? ( {activeLayout === "list" ? (
<ListLayout /> <ListLayout />
) : activeLayout === "kanban" ? ( ) : activeLayout === "kanban" ? (
@ -59,6 +59,7 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
<ProjectSpreadsheetLayout /> <ProjectSpreadsheetLayout />
) : null} ) : null}
</div> </div>
</>
)} )}
</div> </div>
); );

View File

@ -19,53 +19,38 @@ import { Spinner } from "@plane/ui";
export const ProjectViewLayoutRoot: React.FC = observer(() => { export const ProjectViewLayoutRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query as {
workspaceSlug: string;
projectId: string;
viewId?: string;
};
const { const {
issueFilter: issueFilterStore, viewIssues: { loader, getIssues, fetchIssues },
projectViews: projectViewsStore, viewIssuesFilter: { issueFilters, fetchFilters },
projectViewIssues: projectViewIssuesStore,
projectViewFilters: projectViewFiltersStore,
} = useMobxStore(); } = useMobxStore();
useSWR( useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_FILTERS_AND_ISSUES_${viewId.toString()}` : null,
async () => {
if (workspaceSlug && projectId && viewId) { if (workspaceSlug && projectId && viewId) {
// fetching the project display filters and display properties await fetchFilters(workspaceSlug, projectId, viewId);
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); // await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
// 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()] ?? {}
);
} }
} });
);
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const issueCount = projectViewIssuesStore.getIssuesCount;
if (!projectViewIssuesStore.getIssues)
return (
<div className="h-full w-full grid place-items-center">
<Spinner />
</div>
);
return ( return (
<div className="relative h-full w-full flex flex-col overflow-hidden"> <div className="relative h-full w-full flex flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot /> <ProjectViewAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ProjectViewEmptyState /> {loader === "init-loader" ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : ( ) : (
<div className="h-full w-full overflow-y-auto"> <>
{/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && <ProjectViewEmptyState />} */}
<div className="w-full h-full relative overflow-auto">
{activeLayout === "list" ? ( {activeLayout === "list" ? (
<ModuleListLayout /> <ModuleListLayout />
) : activeLayout === "kanban" ? ( ) : activeLayout === "kanban" ? (
@ -78,6 +63,7 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
<ProjectViewSpreadsheetLayout /> <ProjectViewSpreadsheetLayout />
) : null} ) : null}
</div> </div>
</>
)} )}
</div> </div>
); );

View File

@ -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
/>
);
});

View File

@ -15,7 +15,7 @@ type Props = {
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIssues }) => { export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIssues }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1; 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 ( return (
<> <>

View File

@ -2,4 +2,4 @@ export * from "./columns";
export * from "./roots"; export * from "./roots";
export * from "./spreadsheet-column"; export * from "./spreadsheet-column";
export * from "./spreadsheet-view"; export * from "./spreadsheet-view";
export * from "./inline-create-issue-form"; export * from "./quick-add-issue-form";

View File

@ -6,19 +6,26 @@ import { PlusIcon } from "lucide-react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useKeypress from "hooks/use-keypress"; import useKeypress from "hooks/use-keypress";
import useProjectDetails from "hooks/use-project-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// store // store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// helpers // helpers
import { createIssuePayload } from "helpers/issue.helper"; import { createIssuePayload } from "helpers/issue.helper";
// types // types
import { IIssue } from "types"; import { IIssue, IProject } from "types";
type Props = { type Props = {
formKey: keyof IIssue;
groupId?: string; groupId?: string;
subGroupId?: string | null;
prePopulatedData?: Partial<IIssue>; 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> = { const defaultValues: Partial<IIssue> = {
@ -48,17 +55,15 @@ const Inputs = (props: any) => {
); );
}; };
export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props) => { export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { prePopulatedData, groupId } = props; const { formKey, groupId, subGroupId = null, prePopulatedData, quickAddCallback, viewId } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store // store
const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); const { workspace: workspaceStore, project: projectStore } = useMobxStore();
const { projectDetails } = useProjectDetails();
const { const {
reset, reset,
@ -82,7 +87,9 @@ export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// derived values // 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(() => { useEffect(() => {
setFocus("name"); setFocus("name");
@ -106,43 +113,70 @@ export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props
}); });
}, [errors, setToastAlert]); }, [errors, setToastAlert]);
const onSubmitHandler = async (formData: IIssue) => { // const onSubmitHandler = async (formData: IIssue) => {
if (isSubmitting || !workspaceSlug || !projectId) return; // 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 }); reset({ ...defaultValues });
const payload = createIssuePayload(workspaceDetail!, projectDetails!, { const payload = createIssuePayload(workspaceDetail, projectDetail, {
...(prePopulatedData ?? {}), ...(prePopulatedData ?? {}),
...formData, ...formData,
}); });
try { try {
quickAddStore.createIssue( quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload } as IIssue, viewId));
workspaceSlug.toString(),
projectId.toString(),
{
group_id: groupId ?? null,
sub_group_id: null,
},
payload
);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
} catch (err: any) { } catch (err: any) {
Object.keys(err || {}).forEach((key) => { console.error(err);
const error = err?.[key];
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: errorTitle || "Some error occurred. Please try again.", message: err?.message || "Some error occurred. Please try again.",
});
}); });
} }
}; };
@ -156,7 +190,7 @@ export const SpreadsheetInlineCreateIssueForm: React.FC<Props> = observer((props
onSubmit={handleSubmit(onSubmitHandler)} 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" 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> </form>
</div> </div>
)} )}

View File

@ -1,70 +1,18 @@
import React, { useCallback } from "react"; import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { SpreadsheetView } from "components/issues"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
// types import { useRouter } from "next/router";
import { IIssue, IIssueDisplayFilterOptions } from "types";
// constants
import { IIssueUnGroupedStructure } from "store/issue";
export const CycleSpreadsheetLayout: React.FC = observer(() => { export const CycleSpreadsheetLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { cycleId } = router.query as { cycleId: string };
const { const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
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]
);
return ( return (
<SpreadsheetView <BaseSpreadsheetRoot issueStore={cycleIssueStore} issueFiltersStore={cycleIssueFilterStore} viewId={cycleId} />
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}
/>
); );
}); });

View File

@ -1,71 +1,18 @@
import React, { useCallback } from "react"; import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { SpreadsheetView } from "components/issues"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
// types import { useRouter } from "next/router";
import { IIssue, IIssueDisplayFilterOptions } from "types";
// constants
import { IIssueUnGroupedStructure } from "store/issue";
export const ModuleSpreadsheetLayout: React.FC = observer(() => { export const ModuleSpreadsheetLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { moduleId } = router.query as { moduleId: string };
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 { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
return ( return (
<SpreadsheetView <BaseSpreadsheetRoot issueStore={moduleIssueStore} issueFiltersStore={moduleIssueFilterStore} viewId={moduleId} />
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}
/>
); );
}); });

View File

@ -1,86 +1,11 @@
import React, { useCallback } from "react"; import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
import { SpreadsheetView } from "components/issues";
// types
import { IIssue, IIssueDisplayFilterOptions } from "types";
// constants
import { IIssueUnGroupedStructure } from "store/issue";
export const ProjectSpreadsheetLayout: React.FC = observer(() => { export const ProjectSpreadsheetLayout: React.FC = observer(() => {
const router = useRouter(); const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
const { workspaceSlug, projectId } = router.query; return <BaseSpreadsheetRoot issueStore={projectIssuesStore} issueFiltersStore={projectIssueFiltersStore} />;
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
/>
);
}); });

View File

@ -1,70 +1,11 @@
import React, { useCallback } from "react"; import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { SpreadsheetView } from "components/issues"; import { BaseSpreadsheetRoot } from "../base-spreadsheet-root";
// types
import { IIssue, IIssueDisplayFilterOptions } from "types";
// constants
import { IIssueUnGroupedStructure } from "store/issue";
export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
const router = useRouter(); const { viewIssues: projectViewIssuesStore, viewIssuesFilter: projectViewIssueFiltersStore } = useMobxStore();
const { workspaceSlug, projectId } = router.query; return <BaseSpreadsheetRoot issueStore={projectViewIssuesStore} issueFiltersStore={projectViewIssueFiltersStore} />;
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}
/>
);
}); });

View File

@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetInlineCreateIssueForm } from "components/issues"; import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
import { IssuePeekOverview } from "components/issues/issue-peek-overview"; import { IssuePeekOverview } from "components/issues/issue-peek-overview";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// types // types
@ -19,6 +19,13 @@ type Props = {
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void; handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
disableUserActions: boolean; disableUserActions: boolean;
enableQuickCreateIssue?: boolean; enableQuickCreateIssue?: boolean;
}; };
@ -34,7 +41,8 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
states, states,
handleIssueAction, handleIssueAction,
handleUpdateIssue, handleUpdateIssue,
openIssuesListModal, quickAddCallback,
viewId,
disableUserActions, disableUserActions,
enableQuickCreateIssue, enableQuickCreateIssue,
} = props; } = props;
@ -132,7 +140,9 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
<div className="border-t border-custom-border-100"> <div className="border-t border-custom-border-100">
<div className="mb-3 z-50 sticky bottom-0 left-0"> <div className="mb-3 z-50 sticky bottom-0 left-0">
{enableQuickCreateIssue && <SpreadsheetInlineCreateIssueForm />} {enableQuickCreateIssue && (
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
)}
</div> </div>
{/* {!disableUserActions && {/* {!disableUserActions &&

View File

@ -0,0 +1,5 @@
export enum EIssueActions {
UPDATE = "update",
DELETE = "delete",
REMOVE = "remove",
}

View File

@ -109,7 +109,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const handleDeleteIssue = async () => { const handleDeleteIssue = async () => {
if (isArchived) await archivedIssuesStore.deleteArchivedIssue(workspaceSlug, projectId, issue!); 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; const { query } = router;
if (query.peekIssueId) { if (query.peekIssueId) {
issueDetailStore.setPeekId(null); issueDetailStore.setPeekId(null);

View File

@ -39,12 +39,21 @@ export interface IssuesModalProps {
| "cycle" | "cycle"
)[]; )[];
onSubmit?: (data: Partial<IIssue>) => Promise<void>; onSubmit?: (data: Partial<IIssue>) => Promise<void>;
handleSubmit?: (data: Partial<IIssue>) => Promise<void>;
} }
const issueDraftService = new IssueDraftService(); const issueDraftService = new IssueDraftService();
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => { 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 // states
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);
@ -186,6 +195,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
await issueDetailStore await issueDetailStore
.createIssue(workspaceSlug.toString(), activeProject, payload) .createIssue(workspaceSlug.toString(), activeProject, payload)
.then(async (res) => { .then(async (res) => {
if (handleSubmit) {
await handleSubmit(res);
} else {
issueStore.fetchIssues(workspaceSlug.toString(), activeProject); issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
@ -198,6 +210,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
}); });
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
}
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({

View File

@ -173,12 +173,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
key={issue.id} key={issue.id}
value={issue} value={issue}
className={({ active, selected }) => 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" : "" active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "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 <span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full" className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{ style={{
@ -188,12 +188,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
<span className="flex-shrink-0 text-xs"> <span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id} {issue.project__identifier}-{issue.sequence_id}
</span>{" "} </span>{" "}
{issue.name} <span className="truncate">{issue.name}</span>
</div> </div>
<a <a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`} href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank" 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" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >

View File

@ -49,9 +49,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
const createModule = async (payload: Partial<IModule>) => { const createModule = async (payload: Partial<IModule>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const selectedProjectId = payload.project ?? projectId.toString();
await moduleStore await moduleStore
.createModule(workspaceSlug.toString(), projectId.toString(), payload) .createModule(workspaceSlug.toString(), selectedProjectId, payload)
.then(() => { .then(() => {
handleClose(); handleClose();
@ -72,9 +72,9 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
const updateModule = async (payload: Partial<IModule>) => { const updateModule = async (payload: Partial<IModule>) => {
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data) return;
const selectedProjectId = payload.project ?? projectId.toString();
await moduleStore await moduleStore
.updateModuleDetails(workspaceSlug.toString(), projectId.toString(), data.id, payload) .updateModuleDetails(workspaceSlug.toString(), selectedProjectId, data.id, payload)
.then(() => { .then(() => {
handleClose(); handleClose();
@ -99,7 +99,6 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
const payload: Partial<IModule> = { const payload: Partial<IModule> = {
...formData, ...formData,
}; };
if (!data) await createModule(payload); if (!data) await createModule(payload);
else await updateModule(payload); else await updateModule(payload);
}; };

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