mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
pull from branch and resolve conflicts
This commit is contained in:
commit
3269b5a0cf
@ -167,12 +167,6 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not GOOGLE_CLIENT_ID or not GITHUB_CLIENT_ID:
|
|
||||||
return Response(
|
|
||||||
{"error": "Github or Google login is not configured"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not medium or not id_token:
|
if not medium or not id_token:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
@ -8,12 +8,12 @@ import { useTheme } from "next-themes";
|
|||||||
import githubBlackImage from "public/logos/github-black.svg";
|
import githubBlackImage from "public/logos/github-black.svg";
|
||||||
import githubWhiteImage from "public/logos/github-white.svg";
|
import githubWhiteImage from "public/logos/github-white.svg";
|
||||||
|
|
||||||
export interface GithubLoginButtonProps {
|
type Props = {
|
||||||
handleSignIn: React.Dispatch<string>;
|
handleSignIn: React.Dispatch<string>;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
export const GitHubSignInButton: FC<Props> = (props) => {
|
||||||
const { handleSignIn, clientId } = props;
|
const { handleSignIn, clientId } = props;
|
||||||
// states
|
// states
|
||||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
@ -1,12 +1,12 @@
|
|||||||
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
export interface IGoogleLoginButton {
|
type Props = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
handleSignIn: React.Dispatch<any>;
|
handleSignIn: React.Dispatch<any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
export const GoogleSignInButton: FC<Props> = (props) => {
|
||||||
const { handleSignIn, clientId } = props;
|
const { handleSignIn, clientId } = props;
|
||||||
// refs
|
// refs
|
||||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||||
@ -30,6 +30,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
size: "large",
|
size: "large",
|
||||||
logo_alignment: "center",
|
logo_alignment: "center",
|
||||||
text: "signin_with",
|
text: "signin_with",
|
||||||
|
width: 384,
|
||||||
} as GsiButtonConfiguration // customization attributes
|
} as GsiButtonConfiguration // customization attributes
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -53,7 +54,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||||
<div className="w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
<div className="!w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
export * from "./github-login-button";
|
export * from "./github-sign-in";
|
||||||
export * from "./google-login";
|
export * from "./google-sign-in";
|
||||||
export * from "./onboarding-form";
|
export * from "./onboarding-form";
|
||||||
export * from "./user-logged-in";
|
export * from "./user-logged-in";
|
||||||
export * from "./sign-in-forms";
|
export * from "./sign-in-forms";
|
||||||
|
@ -7,7 +7,7 @@ import { AppConfigService } from "services/app-config.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { GithubLoginButton, GoogleLoginButton } from "components/accounts";
|
import { GitHubSignInButton, GoogleSignInButton } from "components/accounts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleSignInRedirection: () => Promise<void>;
|
handleSignInRedirection: () => Promise<void>;
|
||||||
@ -73,12 +73,12 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
|||||||
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
|
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
|
||||||
<hr className="w-full border-onboarding-border-100" />
|
<hr className="w-full border-onboarding-border-100" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto flex flex-col items-center gap-2 overflow-hidden pt-7 sm:w-96 sm:flex-row">
|
<div className="mx-auto space-y-4 overflow-hidden pt-7 sm:w-96">
|
||||||
{envConfig?.google_client_id && (
|
{envConfig?.google_client_id && (
|
||||||
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
<GoogleSignInButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||||
)}
|
)}
|
||||||
{envConfig?.github_client_id && (
|
{envConfig?.github_client_id && (
|
||||||
<GithubLoginButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
<GitHubSignInButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -20,7 +20,11 @@ export const LatestFeatureBlock = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 bg-onboarding-background-100 object-cover sm:h-52 sm:w-96">
|
<div
|
||||||
|
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${
|
||||||
|
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="h-[90%]">
|
<div className="h-[90%]">
|
||||||
<Image
|
<Image
|
||||||
src={latestFeatures}
|
src={latestFeatures}
|
||||||
|
@ -9,12 +9,12 @@ import { useTheme } from "next-themes";
|
|||||||
import githubLightModeImage from "/public/logos/github-black.png";
|
import githubLightModeImage from "/public/logos/github-black.png";
|
||||||
import githubDarkModeImage from "/public/logos/github-dark.svg";
|
import githubDarkModeImage from "/public/logos/github-dark.svg";
|
||||||
|
|
||||||
export interface GithubLoginButtonProps {
|
type Props = {
|
||||||
handleSignIn: React.Dispatch<string>;
|
handleSignIn: React.Dispatch<string>;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = (props) => {
|
export const GitHubSignInButton: FC<Props> = (props) => {
|
||||||
const { handleSignIn, clientId } = props;
|
const { handleSignIn, clientId } = props;
|
||||||
// states
|
// states
|
||||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
@ -1,12 +1,12 @@
|
|||||||
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
import { FC, useEffect, useRef, useCallback, useState } from "react";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
export interface IGoogleLoginButton {
|
type Props = {
|
||||||
handleSignIn: React.Dispatch<any>;
|
handleSignIn: React.Dispatch<any>;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
export const GoogleSignInButton: FC<Props> = (props) => {
|
||||||
const { handleSignIn, clientId } = props;
|
const { handleSignIn, clientId } = props;
|
||||||
// refs
|
// refs
|
||||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||||
@ -30,6 +30,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
size: "large",
|
size: "large",
|
||||||
logo_alignment: "center",
|
logo_alignment: "center",
|
||||||
text: "signin_with",
|
text: "signin_with",
|
||||||
|
width: 384,
|
||||||
} as GsiButtonConfiguration // customization attributes
|
} as GsiButtonConfiguration // customization attributes
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -53,7 +54,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||||
<div className="w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
<div className="!w-full overflow-hidden rounded" id="googleSignInButton" ref={googleSignInButton} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
export * from "./sign-in-forms";
|
export * from "./sign-in-forms";
|
||||||
export * from "./deactivate-account-modal";
|
export * from "./deactivate-account-modal";
|
||||||
export * from "./github-login-button";
|
export * from "./github-sign-in";
|
||||||
export * from "./google-login";
|
export * from "./google-sign-in";
|
||||||
export * from "./email-signup-form";
|
export * from "./email-signup-form";
|
||||||
|
@ -6,7 +6,7 @@ import { AuthService } from "services/auth.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { GithubLoginButton, GoogleLoginButton } from "components/account";
|
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleSignInRedirection: () => Promise<void>;
|
handleSignInRedirection: () => Promise<void>;
|
||||||
@ -73,12 +73,12 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
|
|||||||
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
|
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">Or continue with</p>
|
||||||
<hr className="w-full border-onboarding-border-100" />
|
<hr className="w-full border-onboarding-border-100" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto flex flex-col items-center gap-2 overflow-hidden pt-7 sm:w-96 sm:flex-row">
|
<div className="mx-auto mt-7 space-y-4 overflow-hidden sm:w-96">
|
||||||
{envConfig?.google_client_id && (
|
{envConfig?.google_client_id && (
|
||||||
<GoogleLoginButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
<GoogleSignInButton clientId={envConfig?.google_client_id} handleSignIn={handleGoogleSignIn} />
|
||||||
)}
|
)}
|
||||||
{envConfig?.github_client_id && (
|
{envConfig?.github_client_id && (
|
||||||
<GithubLoginButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
<GitHubSignInButton clientId={envConfig?.github_client_id} handleSignIn={handleGitHubSignIn} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -20,14 +20,18 @@ export const LatestFeatureBlock = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 bg-onboarding-background-100 object-cover sm:h-52 sm:w-96">
|
<div
|
||||||
|
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${
|
||||||
|
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="h-[90%]">
|
<div className="h-[90%]">
|
||||||
<Image
|
<Image
|
||||||
src={latestFeatures}
|
src={latestFeatures}
|
||||||
alt="Plane Issues"
|
alt="Plane Issues"
|
||||||
className={`-mt-2 ml-10 h-full rounded-md ${
|
className={`-mt-2 ml-10 h-full rounded-md ${
|
||||||
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
|
||||||
} `}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IPageBlock } from "types";
|
import { IIssue, IPageBlock } from "types";
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
|||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
const responseRef = useRef<any>(null);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -115,6 +116,10 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
|||||||
editorRef.current?.setEditorValue(htmlContent ?? `<p>${content}</p>`);
|
editorRef.current?.setEditorValue(htmlContent ?? `<p>${content}</p>`);
|
||||||
}, [htmlContent, editorRef, content]);
|
}, [htmlContent, editorRef, content]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
responseRef.current?.setEditorValue(`<p>${response}</p>`);
|
||||||
|
}, [response, responseRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${inset} z-20 flex w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
className={`absolute ${inset} z-20 flex w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||||
@ -137,11 +142,12 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
|
|||||||
{response !== "" && (
|
{response !== "" && (
|
||||||
<div className="page-block-section text-sm">
|
<div className="page-block-section text-sm">
|
||||||
Response:
|
Response:
|
||||||
<RichReadOnlyEditor
|
<RichReadOnlyEditorWithRef
|
||||||
value={`<p>${response}</p>`}
|
value={`<p>${response}</p>`}
|
||||||
customClassName="-mx-3 -my-3"
|
customClassName="-mx-3 -my-3"
|
||||||
noBorder
|
noBorder
|
||||||
borderOnFocus={false}
|
borderOnFocus={false}
|
||||||
|
ref={responseRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -23,6 +23,7 @@ import { ICycle } from "types";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export interface ICyclesBoardCard {
|
export interface ICyclesBoardCard {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -36,6 +37,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
const {
|
const {
|
||||||
cycle: cycleStore,
|
cycle: cycleStore,
|
||||||
trackEvent: { setTrackElement },
|
trackEvent: { setTrackElement },
|
||||||
|
user: userStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -49,6 +51,9 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
const startDate = new Date(cycle.start_date ?? "");
|
const startDate = new Date(cycle.start_date ?? "");
|
||||||
const isDateValid = cycle.start_date || cycle.end_date;
|
const isDateValid = cycle.start_date || cycle.end_date;
|
||||||
|
|
||||||
|
const { currentProjectRole } = userStore;
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||||
@ -68,8 +73,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
? cycleTotalIssues === 0
|
? cycleTotalIssues === 0
|
||||||
? "0 Issue"
|
? "0 Issue"
|
||||||
: cycleTotalIssues === cycle.completed_issues
|
: cycleTotalIssues === cycle.completed_issues
|
||||||
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
|
||||||
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
|
||||||
: "0 Issue";
|
: "0 Issue";
|
||||||
|
|
||||||
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -235,17 +240,18 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
|||||||
<span className="text-xs text-custom-text-400">No due date</span>
|
<span className="text-xs text-custom-text-400">No due date</span>
|
||||||
)}
|
)}
|
||||||
<div className="z-10 flex items-center gap-1.5">
|
<div className="z-10 flex items-center gap-1.5">
|
||||||
{cycle.is_favorite ? (
|
{isEditingAllowed &&
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
(cycle.is_favorite ? (
|
||||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||||
) : (
|
</button>
|
||||||
<button type="button" onClick={handleAddToFavorites}>
|
) : (
|
||||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
)}
|
</button>
|
||||||
|
))}
|
||||||
<CustomMenu width="auto" ellipsis className="z-10">
|
<CustomMenu width="auto" ellipsis className="z-10">
|
||||||
{!isCompleted && (
|
{!isCompleted && isEditingAllowed && (
|
||||||
<>
|
<>
|
||||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
@ -24,6 +24,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
|
|||||||
import { ICycle } from "types";
|
import { ICycle } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "constants/cycle";
|
import { CYCLE_STATUS } from "constants/cycle";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type TCyclesListItem = {
|
type TCyclesListItem = {
|
||||||
cycle: ICycle;
|
cycle: ICycle;
|
||||||
@ -41,6 +42,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
const {
|
const {
|
||||||
cycle: cycleStore,
|
cycle: cycleStore,
|
||||||
trackEvent: { setTrackElement },
|
trackEvent: { setTrackElement },
|
||||||
|
user: userStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// toast
|
// toast
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -53,6 +55,9 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
const endDate = new Date(cycle.end_date ?? "");
|
const endDate = new Date(cycle.end_date ?? "");
|
||||||
const startDate = new Date(cycle.start_date ?? "");
|
const startDate = new Date(cycle.start_date ?? "");
|
||||||
|
|
||||||
|
const { currentProjectRole } = userStore;
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const cycleTotalIssues =
|
const cycleTotalIssues =
|
||||||
@ -226,19 +231,19 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{isEditingAllowed &&
|
||||||
{cycle.is_favorite ? (
|
(cycle.is_favorite ? (
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" onClick={handleAddToFavorites}>
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
))}
|
||||||
|
|
||||||
<CustomMenu width="auto" ellipsis>
|
<CustomMenu width="auto" ellipsis>
|
||||||
{!isCompleted && (
|
{!isCompleted && isEditingAllowed && (
|
||||||
<>
|
<>
|
||||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
@ -97,7 +97,7 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
id="cycle_description"
|
id="cycle_description"
|
||||||
name="description"
|
name="description"
|
||||||
placeholder="Description..."
|
placeholder="Description..."
|
||||||
className="h-24 w-full resize-none text-sm"
|
className="!h-24 w-full resize-none text-sm"
|
||||||
hasError={Boolean(errors?.description)}
|
hasError={Boolean(errors?.description)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -135,18 +135,12 @@ export const CycleForm: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-100 pt-5 ">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||||
{data
|
{data ? (isSubmitting ? "Updating" : "Update cycle") : isSubmitting ? "Creating" : "Create cycle"}
|
||||||
? isSubmitting
|
|
||||||
? "Updating Cycle..."
|
|
||||||
: "Update Cycle"
|
|
||||||
: isSubmitting
|
|
||||||
? "Creating Cycle..."
|
|
||||||
: "Create Cycle"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -49,11 +49,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Error in creating cycle. Please try again.",
|
message: err.detail ?? "Error in creating cycle. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker("CYCLE_CREATE", {
|
postHogEventTracker("CYCLE_CREATE", {
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
@ -73,11 +73,11 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
message: "Cycle updated successfully.",
|
message: "Cycle updated successfully.",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Error in updating cycle. Please try again.",
|
message: err.detail ?? "Error in updating cycle. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,8 @@ 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 { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
import { EFilterType } from "store_legacy/issues/types";
|
import { EFilterType } from "store_legacy/issues/types";
|
||||||
import { EProjectStore } from "store_legacy/command-palette.store";
|
import { EProjectStore } from "store_legacy/command-palette.store";
|
||||||
|
|
||||||
@ -42,6 +44,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
commandPalette: commandPaletteStore,
|
commandPalette: commandPaletteStore,
|
||||||
trackEvent: { setTrackElement },
|
trackEvent: { setTrackElement },
|
||||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||||
|
user: { currentProjectRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
|
const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout;
|
||||||
@ -99,6 +102,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
const cyclesList = cycleStore.projectCycles;
|
const cyclesList = cycleStore.projectCycles;
|
||||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||||
|
|
||||||
|
const canUserCreateIssue =
|
||||||
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -190,16 +196,18 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{canUserCreateIssue && (
|
||||||
onClick={() => {
|
<Button
|
||||||
setTrackElement("CYCLE_PAGE_HEADER");
|
onClick={() => {
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
setTrackElement("CYCLE_PAGE_HEADER");
|
||||||
}}
|
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||||
size="sm"
|
}}
|
||||||
prependIcon={<Plus />}
|
size="sm"
|
||||||
>
|
prependIcon={<Plus />}
|
||||||
Add Issue
|
>
|
||||||
</Button>
|
Add Issue
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||||
|
@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export const CyclesHeader: FC = observer(() => {
|
export const CyclesHeader: FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
@ -16,11 +17,15 @@ export const CyclesHeader: FC = observer(() => {
|
|||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
|
user: { currentProjectRole },
|
||||||
commandPalette: commandPaletteStore,
|
commandPalette: commandPaletteStore,
|
||||||
trackEvent: { setTrackElement },
|
trackEvent: { setTrackElement },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const { currentProjectDetails } = projectStore;
|
const { currentProjectDetails } = projectStore;
|
||||||
|
|
||||||
|
const canUserCreateCycle =
|
||||||
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row 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 z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row 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 w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
@ -50,19 +55,21 @@ export const CyclesHeader: FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
{canUserCreateCycle && (
|
||||||
<Button
|
<div className="flex items-center gap-3">
|
||||||
variant="primary"
|
<Button
|
||||||
size="sm"
|
variant="primary"
|
||||||
prependIcon={<Plus />}
|
size="sm"
|
||||||
onClick={() => {
|
prependIcon={<Plus />}
|
||||||
setTrackElement("CYCLES_PAGE_HEADER");
|
onClick={() => {
|
||||||
commandPaletteStore.toggleCreateCycleModal(true);
|
setTrackElement("CYCLES_PAGE_HEADER");
|
||||||
}}
|
commandPaletteStore.toggleCreateCycleModal(true);
|
||||||
>
|
}}
|
||||||
Add Cycle
|
>
|
||||||
</Button>
|
Add Cycle
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,8 @@ 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 { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
// store
|
||||||
import { EFilterType } from "store_legacy/issues/types";
|
import { EFilterType } from "store_legacy/issues/types";
|
||||||
import { EProjectStore } from "store_legacy/command-palette.store";
|
import { EProjectStore } from "store_legacy/command-palette.store";
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
trackEvent: { setTrackElement },
|
trackEvent: { setTrackElement },
|
||||||
projectLabel: { projectLabels },
|
projectLabel: { projectLabels },
|
||||||
moduleIssuesFilter: { issueFilters, updateFilters },
|
moduleIssuesFilter: { issueFilters, updateFilters },
|
||||||
|
user: { currentProjectRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { currentProjectDetails } = projectStore;
|
const { currentProjectDetails } = projectStore;
|
||||||
@ -100,6 +103,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
const modulesList = projectId ? moduleStore.modules[projectId.toString()] : undefined;
|
||||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||||
|
|
||||||
|
const canUserCreateIssue =
|
||||||
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -191,16 +197,18 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{canUserCreateIssue && (
|
||||||
onClick={() => {
|
<Button
|
||||||
setTrackElement("MODULE_PAGE_HEADER");
|
onClick={() => {
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
setTrackElement("MODULE_PAGE_HEADER");
|
||||||
}}
|
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||||
size="sm"
|
}}
|
||||||
prependIcon={<Plus />}
|
size="sm"
|
||||||
>
|
prependIcon={<Plus />}
|
||||||
Add Issue
|
>
|
||||||
</Button>
|
Add Issue
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
|
||||||
|
@ -11,17 +11,25 @@ import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
|
|||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
import { MODULE_VIEW_LAYOUTS } from "constants/module";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export const ModulesListHeader: React.FC = observer(() => {
|
export const ModulesListHeader: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// store
|
// store
|
||||||
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
commandPalette: commandPaletteStore,
|
||||||
|
user: { currentProjectRole },
|
||||||
|
} = useMobxStore();
|
||||||
const { currentProjectDetails } = projectStore;
|
const { currentProjectDetails } = projectStore;
|
||||||
|
|
||||||
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");
|
||||||
|
|
||||||
|
const canUserCreateModule =
|
||||||
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row 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 z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row 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 w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
@ -72,14 +80,16 @@ export const ModulesListHeader: React.FC = observer(() => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{canUserCreateModule && (
|
||||||
variant="primary"
|
<Button
|
||||||
size="sm"
|
variant="primary"
|
||||||
prependIcon={<Plus />}
|
size="sm"
|
||||||
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
prependIcon={<Plus />}
|
||||||
>
|
onClick={() => commandPaletteStore.toggleCreateModuleModal(true)}
|
||||||
Add Module
|
>
|
||||||
</Button>
|
Add Module
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
|
|||||||
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 { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
// helper
|
// helper
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
import { EFilterType } from "store_legacy/issues/types";
|
import { EFilterType } from "store_legacy/issues/types";
|
||||||
@ -36,6 +37,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
// issue filters
|
// issue filters
|
||||||
projectIssuesFilter: { issueFilters, updateFilters },
|
projectIssuesFilter: { issueFilters, updateFilters },
|
||||||
projectIssues: {},
|
projectIssues: {},
|
||||||
|
user: { currentProjectRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
@ -87,6 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
|
|
||||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||||
|
|
||||||
|
const canUserCreateIssue =
|
||||||
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectAnalyticsModal
|
<ProjectAnalyticsModal
|
||||||
@ -200,16 +205,18 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{canUserCreateIssue && (
|
||||||
onClick={() => {
|
<Button
|
||||||
setTrackElement("PROJECT_PAGE_HEADER");
|
onClick={() => {
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
setTrackElement("PROJECT_PAGE_HEADER");
|
||||||
}}
|
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||||
size="sm"
|
}}
|
||||||
prependIcon={<Plus />}
|
size="sm"
|
||||||
>
|
prependIcon={<Plus />}
|
||||||
Add Issue
|
>
|
||||||
</Button>
|
Add Issue
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from "react";
|
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 { Plus } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
@ -14,9 +15,10 @@ 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 { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
import { EFilterType } from "store_legacy/issues/types";
|
import { EFilterType } from "store_legacy/issues/types";
|
||||||
import { EProjectStore } from "store_legacy/command-palette.store";
|
import { EProjectStore } from "store_legacy/command-palette.store";
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
|
|
||||||
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -35,6 +37,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
viewIssuesFilter: { issueFilters, updateFilters },
|
viewIssuesFilter: { issueFilters, updateFilters },
|
||||||
commandPalette: commandPaletteStore,
|
commandPalette: commandPaletteStore,
|
||||||
trackEvent: { setTrackElement },
|
trackEvent: { setTrackElement },
|
||||||
|
user: { currentProjectRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
@ -85,6 +88,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||||
|
|
||||||
|
const canUserCreateIssue =
|
||||||
|
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-[3.75rem] w-full 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 z-10 flex h-[3.75rem] w-full 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">
|
<div className="flex items-center gap-2">
|
||||||
@ -170,16 +176,18 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
<Button
|
{
|
||||||
onClick={() => {
|
<Button
|
||||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
onClick={() => {
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||||
}}
|
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
|
||||||
size="sm"
|
}}
|
||||||
prependIcon={<Plus />}
|
size="sm"
|
||||||
>
|
prependIcon={<Plus />}
|
||||||
Add Issue
|
>
|
||||||
</Button>
|
Add Issue
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -226,10 +226,9 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
|
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
project: projectId,
|
|
||||||
...initialData,
|
...initialData,
|
||||||
});
|
});
|
||||||
}, [setFocus, initialData, projectId, reset]);
|
}, [setFocus, initialData, reset]);
|
||||||
|
|
||||||
// update projectId in form when projectId changes
|
// update projectId in form when projectId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -629,8 +628,8 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
? "Updating Issue..."
|
? "Updating Issue..."
|
||||||
: "Update Issue"
|
: "Update Issue"
|
||||||
: isSubmitting
|
: isSubmitting
|
||||||
? "Adding Issue..."
|
? "Adding Issue..."
|
||||||
: "Add Issue"}
|
: "Add Issue"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,8 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
|
||||||
// components
|
// components
|
||||||
import { CalendarChart, IssuePeekOverview } from "components/issues";
|
import { CalendarChart, IssuePeekOverview } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import {
|
import {
|
||||||
@ -34,7 +36,7 @@ interface IBaseCalendarRoot {
|
|||||||
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
|
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
|
||||||
};
|
};
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => void;
|
handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||||
@ -44,12 +46,15 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
const displayFilters = issuesFilterStore.issueFilters?.displayFilters;
|
||||||
|
|
||||||
const issues = issueStore.getIssues;
|
const issues = issueStore.getIssues;
|
||||||
const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues;
|
const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues;
|
||||||
|
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = async (result: DropResult) => {
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
// return if not dropped on the correct place
|
// return if not dropped on the correct place
|
||||||
@ -58,7 +63,15 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
|||||||
// return if dropped on the same date
|
// return if dropped on the same date
|
||||||
if (result.destination.droppableId === result.source.droppableId) return;
|
if (result.destination.droppableId === result.source.droppableId) return;
|
||||||
|
|
||||||
if (handleDragDrop) handleDragDrop(result.source, result.destination, issues, groupedIssueIds);
|
if (handleDragDrop) {
|
||||||
|
await handleDragDrop(result.source, result.destination, issues, groupedIssueIds).catch((err) => {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: err.detail ?? "Failed to perform this action",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIssues = useCallback(
|
const handleIssues = useCallback(
|
||||||
|
@ -41,9 +41,9 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||||
if (workspaceSlug && projectId && cycleId)
|
if (workspaceSlug && projectId && cycleId)
|
||||||
handleCalenderDragDrop(
|
await handleCalenderDragDrop(
|
||||||
source,
|
source,
|
||||||
destination,
|
destination,
|
||||||
workspaceSlug.toString(),
|
workspaceSlug.toString(),
|
||||||
|
@ -38,8 +38,8 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||||
handleCalenderDragDrop(
|
await handleCalenderDragDrop(
|
||||||
source,
|
source,
|
||||||
destination,
|
destination,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
@ -31,9 +31,9 @@ export const CalendarLayout: React.FC = observer(() => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||||
if (workspaceSlug && projectId)
|
if (workspaceSlug && projectId)
|
||||||
handleCalenderDragDrop(
|
await handleCalenderDragDrop(
|
||||||
source,
|
source,
|
||||||
destination,
|
destination,
|
||||||
workspaceSlug.toString(),
|
workspaceSlug.toString(),
|
||||||
|
@ -32,9 +32,9 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragDrop = (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => {
|
||||||
if (workspaceSlug && projectId)
|
if (workspaceSlug && projectId)
|
||||||
handleCalenderDragDrop(
|
await handleCalenderDragDrop(
|
||||||
source,
|
source,
|
||||||
destination,
|
destination,
|
||||||
workspaceSlug.toString(),
|
workspaceSlug.toString(),
|
||||||
|
@ -121,8 +121,9 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
quickAddCallback && quickAddCallback(workspaceSlug, projectId, payload, viewId);
|
if (quickAddCallback) {
|
||||||
|
await quickAddCallback(workspaceSlug, projectId, payload, viewId);
|
||||||
|
}
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
@ -147,7 +147,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
setIsDragStarted(true);
|
setIsDragStarted(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = async (result: DropResult) => {
|
||||||
setIsDragStarted(false);
|
setIsDragStarted(false);
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
@ -171,7 +171,15 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
});
|
});
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
} else {
|
} else {
|
||||||
handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds);
|
await handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds).catch(
|
||||||
|
(err) => {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: err.detail ?? "Failed to perform this action",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -124,7 +124,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
|||||||
value={issue?.start_date || null}
|
value={issue?.start_date || null}
|
||||||
onChange={(date: string) => handleStartDate(date)}
|
onChange={(date: string) => handleStartDate(date)}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
placeHolder="Start date"
|
type="start_date"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
|||||||
value={issue?.target_date || null}
|
value={issue?.target_date || null}
|
||||||
onChange={(date: string) => handleTargetDate(date)}
|
onChange={(date: string) => handleTargetDate(date)}
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
placeHolder="Target date"
|
type="target_date"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
|||||||
value={issue?.start_date || null}
|
value={issue?.start_date || null}
|
||||||
onChange={(date: string) => handleStartDate(date)}
|
onChange={(date: string) => handleStartDate(date)}
|
||||||
disabled={isReadonly}
|
disabled={isReadonly}
|
||||||
placeHolder="Start date"
|
type="start_date"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
|||||||
value={issue?.target_date || null}
|
value={issue?.target_date || null}
|
||||||
onChange={(date: string) => handleTargetDate(date)}
|
onChange={(date: string) => handleTargetDate(date)}
|
||||||
disabled={isReadonly}
|
disabled={isReadonly}
|
||||||
placeHolder="Target date"
|
type="target_date"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { Check, ChevronDown, Search, User2 } from "lucide-react";
|
import { Check, ChevronDown, CircleUser, Search } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, AvatarGroup, Tooltip } from "@plane/ui";
|
import { Avatar, AvatarGroup, Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -110,8 +110,8 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
|||||||
})}
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
<span className="h-5 w-5 grid place-items-center">
|
||||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
<CircleUser className="h-4 w-4" strokeWidth={1.5} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -140,7 +140,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
onClick={() => !projectMembers && getWorkspaceMembers()}
|
onClick={() => !projectMembers && getWorkspaceMembers()}
|
||||||
>
|
>
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Popover } from "@headlessui/react";
|
import { Popover } from "@headlessui/react";
|
||||||
// lucide icons
|
// lucide icons
|
||||||
import { Calendar, X } from "lucide-react";
|
import { CalendarCheck2, CalendarClock, X } from "lucide-react";
|
||||||
// react date picker
|
// react date picker
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
// mobx
|
// mobx
|
||||||
@ -18,11 +18,24 @@ export interface IIssuePropertyDate {
|
|||||||
value: any;
|
value: any;
|
||||||
onChange: (date: any) => void;
|
onChange: (date: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeHolder?: string;
|
type: "start_date" | "target_date";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DATE_OPTIONS = {
|
||||||
|
start_date: {
|
||||||
|
key: "start_date",
|
||||||
|
placeholder: "Start date",
|
||||||
|
icon: CalendarClock,
|
||||||
|
},
|
||||||
|
target_date: {
|
||||||
|
key: "target_date",
|
||||||
|
placeholder: "Target date",
|
||||||
|
icon: CalendarCheck2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props) => {
|
export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props) => {
|
||||||
const { value, onChange, disabled, placeHolder } = props;
|
const { value, onChange, disabled, type } = props;
|
||||||
|
|
||||||
const dropdownBtn = React.useRef<any>(null);
|
const dropdownBtn = React.useRef<any>(null);
|
||||||
const dropdownOptions = React.useRef<any>(null);
|
const dropdownOptions = React.useRef<any>(null);
|
||||||
@ -31,6 +44,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
|||||||
|
|
||||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||||
|
|
||||||
|
const dateOptionDetails = DATE_OPTIONS[type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover as="div" className="relative">
|
<Popover as="div" className="relative">
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
@ -49,10 +64,10 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2 overflow-hidden">
|
<div className="flex items-center justify-center gap-2 overflow-hidden">
|
||||||
<Calendar className="h-3 w-3" strokeWidth={2} />
|
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
||||||
{value && (
|
{value && (
|
||||||
<>
|
<>
|
||||||
<Tooltip tooltipHeading={placeHolder} tooltipContent={value ?? "None"}>
|
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}>
|
||||||
<div className="text-xs">{value}</div>
|
<div className="text-xs">{value}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { usePopper } from "react-popper";
|
|||||||
// components
|
// components
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { Check, ChevronDown, Search } from "lucide-react";
|
import { Check, ChevronDown, Search, Tags } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { Placement } from "@popperjs/core";
|
import { Placement } from "@popperjs/core";
|
||||||
import { RootStore } from "store_legacy/root";
|
import { RootStore } from "store_legacy/root";
|
||||||
@ -25,6 +25,7 @@ export interface IIssuePropertyLabels {
|
|||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
maxRender?: number;
|
maxRender?: number;
|
||||||
noLabelBorder?: boolean;
|
noLabelBorder?: boolean;
|
||||||
|
placeholderText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
||||||
@ -41,6 +42,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
placement,
|
placement,
|
||||||
maxRender = 2,
|
maxRender = 2,
|
||||||
noLabelBorder = false,
|
noLabelBorder = false,
|
||||||
|
placeholderText,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -144,11 +146,12 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`flex h-full items-center justify-center rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||||
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Select labels
|
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||||
|
{placeholderText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -171,8 +174,8 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
disabled
|
disabled
|
||||||
? "cursor-not-allowed text-custom-text-200"
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
: value.length <= maxRender
|
: value.length <= maxRender
|
||||||
? "cursor-pointer"
|
? "cursor-pointer"
|
||||||
: "cursor-pointer hover:bg-custom-background-80"
|
: "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
onClick={() => !storeLabels && fetchLabels()}
|
onClick={() => !storeLabels && fetchLabels()}
|
||||||
>
|
>
|
||||||
|
@ -94,7 +94,7 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
|||||||
|
|
||||||
const label = (
|
const label = (
|
||||||
<Tooltip tooltipHeading="State" tooltipContent={selectedOption?.name ?? ""} position="top">
|
<Tooltip tooltipHeading="State" tooltipContent={selectedOption?.name ?? ""} position="top">
|
||||||
<div className="flex w-full cursor-pointer items-center gap-2 text-custom-text-200">
|
<div className="flex w-full items-center gap-2 text-custom-text-200">
|
||||||
{selectedOption && <StateGroupIcon stateGroup={selectedOption?.group as any} color={selectedOption?.color} />}
|
{selectedOption && <StateGroupIcon stateGroup={selectedOption?.group as any} color={selectedOption?.color} />}
|
||||||
<span className="line-clamp-1 inline-block truncate">{selectedOption?.name ?? "State"}</span>
|
<span className="line-clamp-1 inline-block truncate">{selectedOption?.name ?? "State"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,8 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
@ -12,6 +14,8 @@ import { copyUrlToClipboard } from "helpers/string.helper";
|
|||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import { IQuickActionProps } from "../list/list-view-types";
|
import { IQuickActionProps } from "../list/list-view-types";
|
||||||
import { EProjectStore } from "store_legacy/command-palette.store";
|
import { EProjectStore } from "store_legacy/command-palette.store";
|
||||||
|
// constant
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
||||||
@ -24,6 +28,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
|
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
|
||||||
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
|
const { currentProjectRole } = userStore;
|
||||||
|
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleCopyIssueLink = () => {
|
const handleCopyIssueLink = () => {
|
||||||
@ -71,43 +81,47 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
Copy link
|
Copy link
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
{isEditingAllowed && (
|
||||||
onClick={(e) => {
|
<>
|
||||||
e.preventDefault();
|
<CustomMenu.MenuItem
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
setIssueToEdit(issue);
|
e.preventDefault();
|
||||||
setCreateUpdateIssueModal(true);
|
e.stopPropagation();
|
||||||
}}
|
setIssueToEdit(issue);
|
||||||
>
|
setCreateUpdateIssueModal(true);
|
||||||
<div className="flex items-center gap-2">
|
}}
|
||||||
<Pencil className="h-3 w-3" />
|
>
|
||||||
Edit issue
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Pencil className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
Edit issue
|
||||||
<CustomMenu.MenuItem
|
</div>
|
||||||
onClick={(e) => {
|
</CustomMenu.MenuItem>
|
||||||
e.preventDefault();
|
<CustomMenu.MenuItem
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
setCreateUpdateIssueModal(true);
|
e.preventDefault();
|
||||||
}}
|
e.stopPropagation();
|
||||||
>
|
setCreateUpdateIssueModal(true);
|
||||||
<div className="flex items-center gap-2">
|
}}
|
||||||
<Copy className="h-3 w-3" />
|
>
|
||||||
Make a copy
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Copy className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
Make a copy
|
||||||
<CustomMenu.MenuItem
|
</div>
|
||||||
onClick={(e) => {
|
</CustomMenu.MenuItem>
|
||||||
e.preventDefault();
|
<CustomMenu.MenuItem
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
setDeleteIssueModal(true);
|
e.preventDefault();
|
||||||
}}
|
e.stopPropagation();
|
||||||
>
|
setDeleteIssueModal(true);
|
||||||
<div className="flex items-center gap-2">
|
}}
|
||||||
<Trash2 className="h-3 w-3" />
|
>
|
||||||
Delete issue
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Trash2 className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
Delete issue
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -27,7 +27,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
|||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
|
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
|
||||||
onChange={(data) => onChange({ assignees: data })}
|
onChange={(data) => onChange({ assignees: data })}
|
||||||
className="h-full w-full"
|
className="h-11 w-full"
|
||||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||||
noLabelBorder
|
noLabelBorder
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
@ -40,14 +40,16 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue) => (
|
subIssues.map((subIssue) => (
|
||||||
<SpreadsheetAssigneeColumn
|
<div className={`h-11`}>
|
||||||
key={subIssue.id}
|
<SpreadsheetAssigneeColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
expandedIssues={expandedIssues}
|
onChange={onChange}
|
||||||
members={members}
|
expandedIssues={expandedIssues}
|
||||||
disabled={disabled}
|
members={members}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs">
|
||||||
{issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
|
{issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -27,7 +27,9 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetAttachmentColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
<div className={`h-11`}>
|
||||||
|
<SpreadsheetAttachmentColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full items-center justify-center text-xs">
|
<div className="flex h-11 w-full items-center justify-center text-xs">
|
||||||
{renderLongDetailDateFormat(issue.created_at)}
|
{renderLongDetailDateFormat(issue.created_at)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -28,7 +28,9 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
<div className="h-11">
|
||||||
|
<SpreadsheetCreatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, exp
|
|||||||
<ViewDueDateSelect
|
<ViewDueDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(val) => onChange({ target_date: val })}
|
onChange={(val) => onChange({ target_date: val })}
|
||||||
className="flex !h-full !w-full max-w-full items-center px-2.5 py-1"
|
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1"
|
||||||
noBorder
|
noBorder
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
@ -34,13 +34,15 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, exp
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetDueDateColumn
|
<div className={`h-11`}>
|
||||||
key={subIssue.id}
|
<SpreadsheetDueDateColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
expandedIssues={expandedIssues}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
expandedIssues={expandedIssues}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
|||||||
projectId={issue.project_detail?.id ?? null}
|
projectId={issue.project_detail?.id ?? null}
|
||||||
value={issue.estimate_point}
|
value={issue.estimate_point}
|
||||||
onChange={(data) => onChange({ estimate_point: data })}
|
onChange={(data) => onChange({ estimate_point: data })}
|
||||||
className="h-full w-full"
|
className="h-11 w-full"
|
||||||
buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
|
buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -36,13 +36,15 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetEstimateColumn
|
<div className={`h-11`}>
|
||||||
key={subIssue.id}
|
<SpreadsheetEstimateColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
expandedIssues={expandedIssues}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
expandedIssues={expandedIssues}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -29,12 +29,12 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
|||||||
value={issue.labels}
|
value={issue.labels}
|
||||||
defaultOptions={issue?.label_details ? issue.label_details : []}
|
defaultOptions={issue?.label_details ? issue.label_details : []}
|
||||||
onChange={(data) => onChange({ labels: data })}
|
onChange={(data) => onChange({ labels: data })}
|
||||||
className="h-full w-full"
|
className="h-11 w-full"
|
||||||
buttonClassName="px-2.5 h-full"
|
buttonClassName="px-2.5 h-full"
|
||||||
noLabelBorder
|
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
maxRender={1}
|
maxRender={1}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
placeholderText="Select labels"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isExpanded &&
|
{isExpanded &&
|
||||||
@ -42,14 +42,16 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetLabelColumn
|
<div className={`h-11`}>
|
||||||
key={subIssue.id}
|
<SpreadsheetLabelColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
labels={labels}
|
onChange={onChange}
|
||||||
expandedIssues={expandedIssues}
|
labels={labels}
|
||||||
disabled={disabled}
|
expandedIssues={expandedIssues}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs">
|
||||||
{issue.link_count} {issue.link_count === 1 ? "link" : "links"}
|
{issue.link_count} {issue.link_count === 1 ? "link" : "links"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -27,7 +27,9 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetLinkColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
<div className={`h-11`}>
|
||||||
|
<SpreadsheetLinkColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,7 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, ex
|
|||||||
<PrioritySelect
|
<PrioritySelect
|
||||||
value={issue.priority}
|
value={issue.priority}
|
||||||
onChange={(data) => onChange({ priority: data })}
|
onChange={(data) => onChange({ priority: data })}
|
||||||
className="h-full w-full"
|
className="h-11 w-full"
|
||||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||||
showTitle
|
showTitle
|
||||||
highlightUrgentPriority={false}
|
highlightUrgentPriority={false}
|
||||||
@ -37,13 +37,15 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, ex
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetPriorityColumn
|
<div className={`h-11`}>
|
||||||
key={subIssue.id}
|
<SpreadsheetPriorityColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
expandedIssues={expandedIssues}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
expandedIssues={expandedIssues}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, e
|
|||||||
<ViewStartDateSelect
|
<ViewStartDateSelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(val) => onChange({ start_date: val })}
|
onChange={(val) => onChange({ start_date: val })}
|
||||||
className="flex !h-full !w-full max-w-full items-center px-2.5 py-1"
|
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1"
|
||||||
noBorder
|
noBorder
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
@ -34,13 +34,15 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, e
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetStartDateColumn
|
<div className={`h-11`}>
|
||||||
key={subIssue.id}
|
<SpreadsheetStartDateColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
expandedIssues={expandedIssues}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
expandedIssues={expandedIssues}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
|||||||
value={issue.state}
|
value={issue.state}
|
||||||
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
||||||
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||||
className="h-full w-full"
|
className="w-full !h-11"
|
||||||
buttonClassName="!shadow-none !border-0 h-full w-full"
|
buttonClassName="!shadow-none !border-0 h-full w-full"
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -40,14 +40,16 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue) => (
|
subIssues.map((subIssue) => (
|
||||||
<SpreadsheetStateColumn
|
<div className="h-11">
|
||||||
key={subIssue.id}
|
<SpreadsheetStateColumn
|
||||||
issue={subIssue}
|
key={subIssue.id}
|
||||||
onChange={onChange}
|
issue={subIssue}
|
||||||
states={states}
|
onChange={onChange}
|
||||||
expandedIssues={expandedIssues}
|
states={states}
|
||||||
disabled={disabled}
|
expandedIssues={expandedIssues}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full items-center px-2.5 py-1 text-xs">
|
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs">
|
||||||
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -27,7 +27,9 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetSubIssueColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
<div className={`h-11`}>
|
||||||
|
<SpreadsheetSubIssueColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full w-full items-center justify-center text-xs">
|
<div className="flex h-11 w-full items-center justify-center text-xs">
|
||||||
{renderLongDetailDateFormat(issue.updated_at)}
|
{renderLongDetailDateFormat(issue.updated_at)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -30,7 +30,9 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
<div className={`h-11`}>
|
||||||
|
<SpreadsheetUpdatedOnColumn key={subIssue.id} issue={subIssue} expandedIssues={expandedIssues} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -159,13 +159,13 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full min-w-[8rem]">
|
<div className="h-full w-full divide-y-[0.5px] border-b-[0.5px] min-w-[8rem]">
|
||||||
{issues?.map((issue) => {
|
{issues?.map((issue) => {
|
||||||
const disableUserActions = !canEditProperties(issue.project);
|
const disableUserActions = !canEditProperties(issue.project);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${property}-${issue.id}`}
|
key={`${property}-${issue.id}`}
|
||||||
className={`h-11 border-b-[0.5px] border-custom-border-200 ${
|
className={`h-fit divide-y-[0.5px] border-custom-border-200 ${
|
||||||
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
|
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -266,11 +266,11 @@ 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((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Issue could not be created. Please try again.",
|
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker(
|
postHogEventTracker(
|
||||||
"ISSUE_CREATED",
|
"ISSUE_CREATED",
|
||||||
@ -312,11 +312,11 @@ 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((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Issue could not be created. Please try again.",
|
message: err.detail ?? "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -347,11 +347,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Issue could not be updated. Please try again.",
|
message: err.detail ?? "Issue could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker(
|
postHogEventTracker(
|
||||||
"ISSUE_UPDATED",
|
"ISSUE_UPDATED",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { FieldError } from "react-hook-form";
|
import type { FieldError } from "react-hook-form";
|
||||||
// mobx store
|
// mobx store
|
||||||
@ -23,9 +22,6 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
|||||||
const { value, onChange } = props;
|
const { value, onChange } = props;
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@ -33,13 +29,13 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
|||||||
placement: "bottom-start",
|
placement: "bottom-start",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { project: projectStore } = useMobxStore();
|
const {
|
||||||
|
project: { joinedProjects },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
const selectedProject = joinedProjects?.find((i) => i.id === value);
|
||||||
|
|
||||||
const selectedProject = projects?.find((i) => i.id === value);
|
const options = joinedProjects?.map((project) => ({
|
||||||
|
|
||||||
const options = projects?.map((project) => ({
|
|
||||||
value: project.id,
|
value: project.id,
|
||||||
query: project.name,
|
query: project.name,
|
||||||
content: (
|
content: (
|
||||||
@ -61,8 +57,8 @@ export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = observer((p
|
|||||||
{selectedProject.emoji
|
{selectedProject.emoji
|
||||||
? renderEmoji(selectedProject.emoji)
|
? renderEmoji(selectedProject.emoji)
|
||||||
: selectedProject.icon_prop
|
: selectedProject.icon_prop
|
||||||
? renderEmoji(selectedProject.icon_prop)
|
? renderEmoji(selectedProject.icon_prop)
|
||||||
: null}
|
: null}
|
||||||
</span>
|
</span>
|
||||||
<div className="truncate">{selectedProject.identifier}</div>
|
<div className="truncate">{selectedProject.identifier}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { CalendarDays } from "lucide-react";
|
import { CalendarCheck } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import {
|
import {
|
||||||
findHowManyDaysLeft,
|
findHowManyDaysLeft,
|
||||||
@ -51,8 +51,8 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
|||||||
issue.target_date === null
|
issue.target_date === null
|
||||||
? ""
|
? ""
|
||||||
: issue.target_date < new Date().toISOString()
|
: issue.target_date < new Date().toISOString()
|
||||||
? "text-red-600"
|
? "text-red-600"
|
||||||
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
@ -67,7 +67,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{issue.target_date ? (
|
{issue.target_date ? (
|
||||||
<>
|
<>
|
||||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{areYearsEqual
|
{areYearsEqual
|
||||||
? renderShortDate(issue.target_date ?? "", "_ _")
|
? renderShortDate(issue.target_date ?? "", "_ _")
|
||||||
@ -76,7 +76,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span>Due Date</span>
|
<span>Due Date</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { CalendarDays } from "lucide-react";
|
import { CalendarClock } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDate, renderShortDateWithYearFormat, renderShortMonthDate } from "helpers/date-time.helper";
|
import { renderShortDate, renderShortDateWithYearFormat, renderShortMonthDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -55,7 +55,7 @@ export const ViewStartDateSelect: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{issue?.start_date ? (
|
{issue?.start_date ? (
|
||||||
<>
|
<>
|
||||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{areYearsEqual
|
{areYearsEqual
|
||||||
? renderShortDate(issue?.start_date, "_ _")
|
? renderShortDate(issue?.start_date, "_ _")
|
||||||
@ -64,7 +64,7 @@ export const ViewStartDateSelect: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0" />
|
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span>Start Date</span>
|
<span>Start Date</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -77,7 +77,6 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
|||||||
<Droppable
|
<Droppable
|
||||||
key={`label.group.droppable.${label.id}`}
|
key={`label.group.droppable.${label.id}`}
|
||||||
droppableId={`label.group.droppable.${label.id}`}
|
droppableId={`label.group.droppable.${label.id}`}
|
||||||
isCombineEnabled={!groupDragSnapshot.isDragging && !isUpdating}
|
|
||||||
isDropDisabled={groupDragSnapshot.isDragging || isUpdating || isDropDisabled}
|
isDropDisabled={groupDragSnapshot.isDragging || isUpdating || isDropDisabled}
|
||||||
>
|
>
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
|
@ -10,11 +10,17 @@ import {
|
|||||||
DropResult,
|
DropResult,
|
||||||
Droppable,
|
Droppable,
|
||||||
} from "@hello-pangea/dnd";
|
} from "@hello-pangea/dnd";
|
||||||
|
|
||||||
// store
|
// store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup } from "components/labels";
|
import {
|
||||||
|
CreateUpdateLabelInline,
|
||||||
|
DeleteLabelModal,
|
||||||
|
ProjectSettingLabelGroup,
|
||||||
|
ProjectSettingLabelItem,
|
||||||
|
} from "components/labels";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Loader } from "@plane/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
@ -22,9 +28,6 @@ import { EmptyState } from "components/common";
|
|||||||
import emptyLabel from "public/empty-state/label.svg";
|
import emptyLabel from "public/empty-state/label.svg";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabel } from "types";
|
import { IIssueLabel } from "types";
|
||||||
//component
|
|
||||||
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
|
||||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
|
||||||
|
|
||||||
const LABELS_ROOT = "labels.root";
|
const LABELS_ROOT = "labels.root";
|
||||||
|
|
||||||
@ -137,7 +140,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
isDropDisabled={isUpdating}
|
isDropDisabled={isUpdating}
|
||||||
>
|
>
|
||||||
{(droppableProvided, droppableSnapshot) => (
|
{(droppableProvided, droppableSnapshot) => (
|
||||||
<div className={`mt-3`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
<div className="mt-3" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||||
{projectLabelsTree.map((label, index) => {
|
{projectLabelsTree.map((label, index) => {
|
||||||
if (label.children && label.children.length) {
|
if (label.children && label.children.length) {
|
||||||
return (
|
return (
|
||||||
|
@ -69,11 +69,11 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Module could not be created. Please try again.",
|
message: err.detail ?? "Module could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker("MODULE_CREATED", {
|
postHogEventTracker("MODULE_CREATED", {
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
@ -99,11 +99,11 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
|
|||||||
state: "SUCCESS",
|
state: "SUCCESS",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Module could not be updated. Please try again.",
|
message: err.detail ?? "Module could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker("MODULE_UPDATED", {
|
postHogEventTracker("MODULE_UPDATED", {
|
||||||
state: "FAILED",
|
state: "FAILED",
|
||||||
|
@ -19,6 +19,7 @@ import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"
|
|||||||
import { IModule } from "types";
|
import { IModule } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
module: IModule;
|
module: IModule;
|
||||||
@ -35,7 +36,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { module: moduleStore } = useMobxStore();
|
const { module: moduleStore, user: userStore } = useMobxStore();
|
||||||
|
|
||||||
|
const { currentProjectRole } = userStore;
|
||||||
|
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const moduleTotalIssues =
|
const moduleTotalIssues =
|
||||||
module.backlog_issues +
|
module.backlog_issues +
|
||||||
@ -59,8 +64,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||||
? "0 Issue"
|
? "0 Issue"
|
||||||
: moduleTotalIssues === module.completed_issues
|
: moduleTotalIssues === module.completed_issues
|
||||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
||||||
: `${module.completed_issues}/${moduleTotalIssues} Issues`
|
: `${module.completed_issues}/${moduleTotalIssues} Issues`
|
||||||
: "0 Issue";
|
: "0 Issue";
|
||||||
|
|
||||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -217,28 +222,34 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="z-10 flex items-center gap-1.5">
|
<div className="z-10 flex items-center gap-1.5">
|
||||||
{module.is_favorite ? (
|
{isEditingAllowed &&
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
(module.is_favorite ? (
|
||||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||||
) : (
|
</button>
|
||||||
<button type="button" onClick={handleAddToFavorites}>
|
) : (
|
||||||
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
)}
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
<CustomMenu width="auto" ellipsis className="z-10">
|
<CustomMenu width="auto" ellipsis className="z-10">
|
||||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
{isEditingAllowed && (
|
||||||
<span className="flex items-center justify-start gap-2">
|
<>
|
||||||
<Pencil className="h-3 w-3" />
|
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||||
<span>Edit module</span>
|
<span className="flex items-center justify-start gap-2">
|
||||||
</span>
|
<Pencil className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
<span>Edit module</span>
|
||||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
</span>
|
||||||
<span className="flex items-center justify-start gap-2">
|
</CustomMenu.MenuItem>
|
||||||
<Trash2 className="h-3 w-3" />
|
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||||
<span>Delete module</span>
|
<span className="flex items-center justify-start gap-2">
|
||||||
</span>
|
<Trash2 className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
<span>Delete module</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-3 w-3" />
|
<LinkIcon className="h-3 w-3" />
|
||||||
|
@ -19,6 +19,7 @@ import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"
|
|||||||
import { IModule } from "types";
|
import { IModule } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { MODULE_STATUS } from "constants/module";
|
import { MODULE_STATUS } from "constants/module";
|
||||||
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
module: IModule;
|
module: IModule;
|
||||||
@ -35,7 +36,11 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { module: moduleStore } = useMobxStore();
|
const { module: moduleStore, user: userStore } = useMobxStore();
|
||||||
|
|
||||||
|
const { currentProjectRole } = userStore;
|
||||||
|
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100;
|
||||||
|
|
||||||
@ -194,29 +199,34 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{module.is_favorite ? (
|
{isEditingAllowed &&
|
||||||
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
|
(module.is_favorite ? (
|
||||||
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
<button type="button" onClick={handleRemoveFromFavorites} className="z-[1]">
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 fill-current text-amber-500" />
|
||||||
) : (
|
</button>
|
||||||
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
|
) : (
|
||||||
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
<button type="button" onClick={handleAddToFavorites} className="z-[1]">
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
)}
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
<CustomMenu width="auto" verticalEllipsis buttonClassName="z-[1]">
|
<CustomMenu width="auto" verticalEllipsis buttonClassName="z-[1]">
|
||||||
<CustomMenu.MenuItem onClick={handleEditModule}>
|
{isEditingAllowed && (
|
||||||
<span className="flex items-center justify-start gap-2">
|
<>
|
||||||
<Pencil className="h-3 w-3" />
|
<CustomMenu.MenuItem onClick={handleEditModule}>
|
||||||
<span>Edit module</span>
|
<span className="flex items-center justify-start gap-2">
|
||||||
</span>
|
<Pencil className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
<span>Edit module</span>
|
||||||
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
</span>
|
||||||
<span className="flex items-center justify-start gap-2">
|
</CustomMenu.MenuItem>
|
||||||
<Trash2 className="h-3 w-3" />
|
<CustomMenu.MenuItem onClick={handleDeleteModule}>
|
||||||
<span>Delete module</span>
|
<span className="flex items-center justify-start gap-2">
|
||||||
</span>
|
<Trash2 className="h-3 w-3" />
|
||||||
</CustomMenu.MenuItem>
|
<span>Delete module</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-3 w-3" />
|
<LinkIcon className="h-3 w-3" />
|
||||||
|
@ -60,11 +60,11 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Page could not be created. Please try again.",
|
message: err.detail ?? "Page could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker(
|
postHogEventTracker(
|
||||||
"PAGE_CREATED",
|
"PAGE_CREATED",
|
||||||
@ -104,11 +104,11 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Page could not be updated. Please try again.",
|
message: err.detail ?? "Page could not be updated. Please try again.",
|
||||||
});
|
});
|
||||||
postHogEventTracker(
|
postHogEventTracker(
|
||||||
"PAGE_UPDATED",
|
"PAGE_UPDATED",
|
||||||
|
@ -154,6 +154,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
const userCanChangeAccess = isCurrentUserOwner;
|
const userCanChangeAccess = isCurrentUserOwner;
|
||||||
const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
const userCanDelete = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
const userCanDelete = isCurrentUserOwner || currentProjectRole === EUserWorkspaceRoles.ADMIN;
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -208,17 +209,19 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
<p className="text-sm text-custom-text-200">{render24HourFormatTime(page.updated_at)}</p>
|
<p className="text-sm text-custom-text-200">{render24HourFormatTime(page.updated_at)}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip tooltipContent={`${page.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
{isEditingAllowed && (
|
||||||
{page.is_favorite ? (
|
<Tooltip tooltipContent={`${page.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
|
||||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
{page.is_favorite ? (
|
||||||
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||||
</button>
|
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
|
||||||
) : (
|
</button>
|
||||||
<button type="button" onClick={handleAddToFavorites}>
|
) : (
|
||||||
<Star className="h-3.5 w-3.5" />
|
<button type="button" onClick={handleAddToFavorites}>
|
||||||
</button>
|
<Star className="h-3.5 w-3.5" />
|
||||||
)}
|
</button>
|
||||||
</Tooltip>
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{userCanChangeAccess && (
|
{userCanChangeAccess && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltipContent={`${
|
tooltipContent={`${
|
||||||
@ -255,7 +258,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
{userCanDelete && (
|
{userCanDelete && isEditingAllowed && (
|
||||||
<CustomMenu.MenuItem onClick={handleDeletePage}>
|
<CustomMenu.MenuItem onClick={handleDeletePage}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
@ -266,7 +269,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{userCanEdit && (
|
{userCanEdit && isEditingAllowed && (
|
||||||
<CustomMenu.MenuItem onClick={handleEditPage}>
|
<CustomMenu.MenuItem onClick={handleEditPage}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
@ -274,7 +277,7 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
{userCanArchive && (
|
{userCanArchive && isEditingAllowed && (
|
||||||
<CustomMenu.MenuItem onClick={handleArchivePage}>
|
<CustomMenu.MenuItem onClick={handleArchivePage}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Archive className="h-3 w-3" />
|
<Archive className="h-3 w-3" />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { ChevronDown, PenSquare, Search } from "lucide-react";
|
import { ChevronUp, PenSquare, Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
@ -37,7 +37,6 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
|||||||
}}
|
}}
|
||||||
fieldsToShow={["all"]}
|
fieldsToShow={["all"]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`mt-4 flex w-full cursor-pointer items-center justify-between px-4 ${
|
className={`mt-4 flex w-full cursor-pointer items-center justify-between px-4 ${
|
||||||
isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
||||||
@ -74,10 +73,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
|
|||||||
isSidebarCollapsed ? "hidden" : "block"
|
isSidebarCollapsed ? "hidden" : "block"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ChevronDown
|
<ChevronUp className="h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-0" />
|
||||||
size={16}
|
|
||||||
className="rotate-0 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-180"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { TIssueOrderByOptions } from "types";
|
import { TIssueOrderByOptions } from "types";
|
||||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip } from "lucide-react";
|
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
ascendingOrderTitle: "New",
|
ascendingOrderTitle: "New",
|
||||||
descendingOrderKey: "target_date",
|
descendingOrderKey: "target_date",
|
||||||
descendingOrderTitle: "Old",
|
descendingOrderTitle: "Old",
|
||||||
icon: CalendarDays,
|
icon: CalendarCheck,
|
||||||
},
|
},
|
||||||
estimate: {
|
estimate: {
|
||||||
title: "Estimate",
|
title: "Estimate",
|
||||||
@ -68,7 +68,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
ascendingOrderTitle: "New",
|
ascendingOrderTitle: "New",
|
||||||
descendingOrderKey: "start_date",
|
descendingOrderKey: "start_date",
|
||||||
descendingOrderTitle: "Old",
|
descendingOrderTitle: "Old",
|
||||||
icon: CalendarDays,
|
icon: CalendarClock,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
title: "State",
|
title: "State",
|
||||||
|
@ -156,7 +156,9 @@ const ProfileActivityPage: NextPageWithLayout = () => {
|
|||||||
{activityItem.actor_detail.first_name} Bot
|
{activityItem.actor_detail.first_name} Bot
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
<Link
|
||||||
|
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||||
|
>
|
||||||
<span className="text-gray font-medium">
|
<span className="text-gray font-medium">
|
||||||
{activityItem.actor_detail.display_name}
|
{activityItem.actor_detail.display_name}
|
||||||
</span>
|
</span>
|
||||||
|
@ -33,7 +33,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
const [isPageLoading, setIsPageLoading] = useState(true);
|
const [isPageLoading, setIsPageLoading] = useState(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
appConfig: { envConfig },
|
user: { currentUser },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -74,20 +74,11 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!envConfig) return;
|
if (!currentUser) return;
|
||||||
|
|
||||||
const enableEmailPassword =
|
if (currentUser.is_password_autoset) router.push("/profile");
|
||||||
envConfig?.email_password_login ||
|
|
||||||
!(
|
|
||||||
envConfig?.email_password_login ||
|
|
||||||
envConfig?.magic_login ||
|
|
||||||
envConfig?.google_client_id ||
|
|
||||||
envConfig?.github_client_id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!enableEmailPassword) router.push("/profile");
|
|
||||||
else setIsPageLoading(false);
|
else setIsPageLoading(false);
|
||||||
}, [envConfig, router]);
|
}, [currentUser, router]);
|
||||||
|
|
||||||
if (isPageLoading)
|
if (isPageLoading)
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "./root";
|
import { RootStore } from "../root.store";
|
||||||
import { IAppConfig } from "types/app";
|
import { IAppConfig } from "types/app";
|
||||||
// services
|
// services
|
||||||
import { AppConfigService } from "services/app_config.service";
|
import { AppConfigService } from "services/app_config.service";
|
||||||
@ -11,7 +11,7 @@ export interface IAppConfigStore {
|
|||||||
fetchAppConfig: () => Promise<any>;
|
fetchAppConfig: () => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppConfigStore implements IAppConfigStore {
|
export class AppConfigStore implements IAppConfigStore {
|
||||||
// observables
|
// observables
|
||||||
envConfig: IAppConfig | null = null;
|
envConfig: IAppConfig | null = null;
|
||||||
|
|
||||||
@ -43,5 +43,3 @@ class AppConfigStore implements IAppConfigStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppConfigStore;
|
|
||||||
|
@ -0,0 +1,198 @@
|
|||||||
|
import { observable, action, makeObservable, computed } from "mobx";
|
||||||
|
// types
|
||||||
|
import { RootStore } from "../root.store";
|
||||||
|
// services
|
||||||
|
import { ProjectService } from "services/project";
|
||||||
|
import { PageService } from "services/page.service";
|
||||||
|
|
||||||
|
export enum EProjectStore {
|
||||||
|
PROJECT = "ProjectStore",
|
||||||
|
PROJECT_VIEW = "ProjectViewStore",
|
||||||
|
PROFILE = "ProfileStore",
|
||||||
|
MODULE = "ModuleStore",
|
||||||
|
CYCLE = "CycleStore",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalData {
|
||||||
|
store: EProjectStore;
|
||||||
|
viewId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICommandPaletteStore {
|
||||||
|
isCommandPaletteOpen: boolean;
|
||||||
|
isShortcutModalOpen: boolean;
|
||||||
|
isCreateProjectModalOpen: boolean;
|
||||||
|
isCreateCycleModalOpen: boolean;
|
||||||
|
isCreateModuleModalOpen: boolean;
|
||||||
|
isCreateViewModalOpen: boolean;
|
||||||
|
isCreatePageModalOpen: boolean;
|
||||||
|
isCreateIssueModalOpen: boolean;
|
||||||
|
isDeleteIssueModalOpen: boolean;
|
||||||
|
isBulkDeleteIssueModalOpen: boolean;
|
||||||
|
|
||||||
|
// computed
|
||||||
|
isAnyModalOpen: boolean;
|
||||||
|
|
||||||
|
toggleCommandPaletteModal: (value?: boolean) => void;
|
||||||
|
toggleShortcutModal: (value?: boolean) => void;
|
||||||
|
toggleCreateProjectModal: (value?: boolean) => void;
|
||||||
|
toggleCreateCycleModal: (value?: boolean) => void;
|
||||||
|
toggleCreateViewModal: (value?: boolean) => void;
|
||||||
|
toggleCreatePageModal: (value?: boolean) => void;
|
||||||
|
toggleCreateIssueModal: (value?: boolean, storeType?: EProjectStore) => void;
|
||||||
|
toggleCreateModuleModal: (value?: boolean) => void;
|
||||||
|
toggleDeleteIssueModal: (value?: boolean) => void;
|
||||||
|
toggleBulkDeleteIssueModal: (value?: boolean) => void;
|
||||||
|
|
||||||
|
createIssueStoreType: EProjectStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandPaletteStore implements ICommandPaletteStore {
|
||||||
|
isCommandPaletteOpen: boolean = false;
|
||||||
|
isShortcutModalOpen: boolean = false;
|
||||||
|
isCreateProjectModalOpen: boolean = false;
|
||||||
|
isCreateCycleModalOpen: boolean = false;
|
||||||
|
isCreateModuleModalOpen: boolean = false;
|
||||||
|
isCreateViewModalOpen: boolean = false;
|
||||||
|
isCreatePageModalOpen: boolean = false;
|
||||||
|
isCreateIssueModalOpen: boolean = false;
|
||||||
|
isDeleteIssueModalOpen: boolean = false;
|
||||||
|
isBulkDeleteIssueModalOpen: boolean = false;
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
// service
|
||||||
|
projectService;
|
||||||
|
pageService;
|
||||||
|
|
||||||
|
createIssueStoreType: EProjectStore = EProjectStore.PROJECT;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
isCommandPaletteOpen: observable.ref,
|
||||||
|
isShortcutModalOpen: observable.ref,
|
||||||
|
isCreateProjectModalOpen: observable.ref,
|
||||||
|
isCreateCycleModalOpen: observable.ref,
|
||||||
|
isCreateModuleModalOpen: observable.ref,
|
||||||
|
isCreateViewModalOpen: observable.ref,
|
||||||
|
isCreatePageModalOpen: observable.ref,
|
||||||
|
isCreateIssueModalOpen: observable.ref,
|
||||||
|
isDeleteIssueModalOpen: observable.ref,
|
||||||
|
isBulkDeleteIssueModalOpen: observable.ref,
|
||||||
|
// computed
|
||||||
|
isAnyModalOpen: computed,
|
||||||
|
// projectPages: computed,
|
||||||
|
// action
|
||||||
|
toggleCommandPaletteModal: action,
|
||||||
|
toggleShortcutModal: action,
|
||||||
|
toggleCreateProjectModal: action,
|
||||||
|
toggleCreateCycleModal: action,
|
||||||
|
toggleCreateViewModal: action,
|
||||||
|
toggleCreatePageModal: action,
|
||||||
|
toggleCreateIssueModal: action,
|
||||||
|
toggleCreateModuleModal: action,
|
||||||
|
toggleDeleteIssueModal: action,
|
||||||
|
toggleBulkDeleteIssueModal: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
this.projectService = new ProjectService();
|
||||||
|
this.pageService = new PageService();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAnyModalOpen() {
|
||||||
|
return Boolean(
|
||||||
|
this.isCreateIssueModalOpen ||
|
||||||
|
this.isCreateCycleModalOpen ||
|
||||||
|
this.isCreatePageModalOpen ||
|
||||||
|
this.isCreateProjectModalOpen ||
|
||||||
|
this.isCreateModuleModalOpen ||
|
||||||
|
this.isCreateViewModalOpen ||
|
||||||
|
this.isShortcutModalOpen ||
|
||||||
|
this.isBulkDeleteIssueModalOpen ||
|
||||||
|
this.isDeleteIssueModalOpen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCommandPaletteModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCommandPaletteOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isCommandPaletteOpen = !this.isCommandPaletteOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleShortcutModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isShortcutModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isShortcutModalOpen = !this.isShortcutModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleCreateProjectModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCreateProjectModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isCreateProjectModalOpen = !this.isCreateProjectModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleCreateCycleModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCreateCycleModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isCreateCycleModalOpen = !this.isCreateCycleModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleCreateViewModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCreateViewModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isCreateViewModalOpen = !this.isCreateViewModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleCreatePageModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCreatePageModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isCreatePageModalOpen = !this.isCreatePageModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleCreateIssueModal = (value?: boolean, storeType?: EProjectStore) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCreateIssueModalOpen = value;
|
||||||
|
this.createIssueStoreType = storeType || EProjectStore.PROJECT;
|
||||||
|
} else {
|
||||||
|
this.isCreateIssueModalOpen = !this.isCreateIssueModalOpen;
|
||||||
|
this.createIssueStoreType = EProjectStore.PROJECT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleDeleteIssueModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isDeleteIssueModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isDeleteIssueModalOpen = !this.isDeleteIssueModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleCreateModuleModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isCreateModuleModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isCreateModuleModalOpen = !this.isCreateModuleModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleBulkDeleteIssueModal = (value?: boolean) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.isBulkDeleteIssueModalOpen = value;
|
||||||
|
} else {
|
||||||
|
this.isBulkDeleteIssueModalOpen = !this.isBulkDeleteIssueModalOpen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
import posthog from "posthog-js";
|
||||||
|
// stores
|
||||||
|
import { RootStore } from "../root.store";
|
||||||
|
|
||||||
|
export interface IEventTrackerStore {
|
||||||
|
trackElement: string;
|
||||||
|
setTrackElement: (element: string) => void;
|
||||||
|
postHogEventTracker: (
|
||||||
|
eventName: string,
|
||||||
|
payload: object | [] | null,
|
||||||
|
group?: { isGrouping: boolean | null; groupType: string | null; gorupId: string | null } | null
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventTrackerStore implements IEventTrackerStore {
|
||||||
|
trackElement: string = "";
|
||||||
|
rootStore;
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
trackElement: observable,
|
||||||
|
setTrackElement: action,
|
||||||
|
postHogEventTracker: action,
|
||||||
|
});
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrackElement = (element: string) => {
|
||||||
|
this.trackElement = element;
|
||||||
|
};
|
||||||
|
|
||||||
|
postHogEventTracker = (
|
||||||
|
eventName: string,
|
||||||
|
payload: object | [] | null,
|
||||||
|
group?: { isGrouping: boolean | null; groupType: string | null; gorupId: string | null } | null
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let extras: any = {
|
||||||
|
workspace_name: this.rootStore.workspace.currentWorkspace?.name ?? "",
|
||||||
|
workspace_id: this.rootStore.workspace.currentWorkspace?.id ?? "",
|
||||||
|
workspace_slug: this.rootStore.workspace.currentWorkspace?.slug ?? "",
|
||||||
|
project_name: this.rootStore.project.currentProjectDetails?.name ?? "",
|
||||||
|
project_id: this.rootStore.project.currentProjectDetails?.id ?? "",
|
||||||
|
project_identifier: this.rootStore.project.currentProjectDetails?.identifier ?? "",
|
||||||
|
};
|
||||||
|
if (["PROJECT_CREATED", "PROJECT_UPDATED"].includes(eventName)) {
|
||||||
|
const project_details: any = payload as object;
|
||||||
|
extras = {
|
||||||
|
...extras,
|
||||||
|
project_name: project_details?.name ?? "",
|
||||||
|
project_id: project_details?.id ?? "",
|
||||||
|
project_identifier: project_details?.identifier ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group && group!.isGrouping === true) {
|
||||||
|
posthog?.group(group!.groupType!, group!.gorupId!, {
|
||||||
|
date: new Date(),
|
||||||
|
workspace_id: group!.gorupId,
|
||||||
|
});
|
||||||
|
posthog?.capture(eventName, {
|
||||||
|
...payload,
|
||||||
|
extras: extras,
|
||||||
|
element: this.trackElement ?? "",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
posthog?.capture(eventName, {
|
||||||
|
...payload,
|
||||||
|
extras: extras,
|
||||||
|
element: this.trackElement ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.setTrackElement("");
|
||||||
|
};
|
||||||
|
}
|
@ -1,15 +1,25 @@
|
|||||||
export class AppRootStore {
|
import { RootStore } from "../root.store";
|
||||||
config;
|
import { AppConfigStore } from "./app-config.store";
|
||||||
commandPalette;
|
import { CommandPaletteStore } from "./command-palette.store";
|
||||||
eventTracker;
|
import { EventTrackerStore } from "./event-tracker.store";
|
||||||
instance;
|
import { InstanceStore } from "./instance.store";
|
||||||
theme;
|
import { RouterStore } from "./router.store";
|
||||||
|
import { ThemeStore } from "./theme.store";
|
||||||
|
|
||||||
constructor() {
|
export class AppRootStore {
|
||||||
this.config = new ConfigStore();
|
config: AppConfigStore;
|
||||||
this.commandPalette = new CommandPaletteStore();
|
commandPalette: CommandPaletteStore;
|
||||||
this.eventTracker = new EventTrackerStore();
|
eventTracker: EventTrackerStore;
|
||||||
this.instance = new InstanceStore();
|
instance: InstanceStore;
|
||||||
this.theme = new ThemeStore();
|
theme: ThemeStore;
|
||||||
|
router: RouterStore;
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
this.config = new AppConfigStore(rootStore);
|
||||||
|
this.commandPalette = new CommandPaletteStore(rootStore);
|
||||||
|
this.eventTracker = new EventTrackerStore(rootStore);
|
||||||
|
this.instance = new InstanceStore(rootStore);
|
||||||
|
this.theme = new ThemeStore(rootStore);
|
||||||
|
this.router = new RouterStore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,178 @@
|
|||||||
|
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||||
|
// store
|
||||||
|
import { RootStore } from "../root.store";
|
||||||
|
// types
|
||||||
|
import { IInstance, IInstanceConfiguration, IFormattedInstanceConfiguration, IInstanceAdmin } from "types/instance";
|
||||||
|
// services
|
||||||
|
import { InstanceService } from "services/instance.service";
|
||||||
|
|
||||||
|
export interface IInstanceStore {
|
||||||
|
loader: boolean;
|
||||||
|
error: any | null;
|
||||||
|
// issues
|
||||||
|
instance: IInstance | null;
|
||||||
|
instanceAdmins: IInstanceAdmin[] | null;
|
||||||
|
configurations: IInstanceConfiguration[] | null;
|
||||||
|
// computed
|
||||||
|
formattedConfig: IFormattedInstanceConfiguration | null;
|
||||||
|
// action
|
||||||
|
fetchInstanceInfo: () => Promise<IInstance>;
|
||||||
|
fetchInstanceAdmins: () => Promise<IInstanceAdmin[]>;
|
||||||
|
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance>;
|
||||||
|
fetchInstanceConfigurations: () => Promise<any>;
|
||||||
|
updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstanceStore implements IInstanceStore {
|
||||||
|
loader: boolean = false;
|
||||||
|
error: any | null = null;
|
||||||
|
instance: IInstance | null = null;
|
||||||
|
instanceAdmins: IInstanceAdmin[] | null = null;
|
||||||
|
configurations: IInstanceConfiguration[] | null = null;
|
||||||
|
// service
|
||||||
|
instanceService;
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
loader: observable.ref,
|
||||||
|
error: observable.ref,
|
||||||
|
instance: observable,
|
||||||
|
instanceAdmins: observable,
|
||||||
|
configurations: observable,
|
||||||
|
// computed
|
||||||
|
formattedConfig: computed,
|
||||||
|
// actions
|
||||||
|
fetchInstanceInfo: action,
|
||||||
|
fetchInstanceAdmins: action,
|
||||||
|
updateInstanceInfo: action,
|
||||||
|
fetchInstanceConfigurations: action,
|
||||||
|
updateInstanceConfigurations: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
this.instanceService = new InstanceService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* computed value for instance configurations data for forms.
|
||||||
|
* @returns configurations in the form of {key, value} pair.
|
||||||
|
*/
|
||||||
|
get formattedConfig() {
|
||||||
|
if (!this.configurations) return null;
|
||||||
|
|
||||||
|
return this.configurations?.reduce((formData: IFormattedInstanceConfiguration, config) => {
|
||||||
|
formData[config.key] = config.value;
|
||||||
|
return formData;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch instance info from API
|
||||||
|
*/
|
||||||
|
fetchInstanceInfo = async () => {
|
||||||
|
try {
|
||||||
|
const instance = await this.instanceService.getInstanceInfo();
|
||||||
|
runInAction(() => {
|
||||||
|
this.instance = instance;
|
||||||
|
});
|
||||||
|
return instance;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error while fetching the instance info");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch instance admins from API
|
||||||
|
*/
|
||||||
|
fetchInstanceAdmins = async () => {
|
||||||
|
try {
|
||||||
|
const instanceAdmins = await this.instanceService.getInstanceAdmins();
|
||||||
|
runInAction(() => {
|
||||||
|
this.instanceAdmins = instanceAdmins;
|
||||||
|
});
|
||||||
|
return instanceAdmins;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error while fetching the instance admins");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update instance info
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
updateInstanceInfo = async (data: Partial<IInstance>) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.instanceService.updateInstanceInfo(data);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = null;
|
||||||
|
this.instance = response;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch instace configurations from API
|
||||||
|
*/
|
||||||
|
fetchInstanceConfigurations = async () => {
|
||||||
|
try {
|
||||||
|
const configurations = await this.instanceService.getInstanceConfigurations();
|
||||||
|
runInAction(() => {
|
||||||
|
this.configurations = configurations;
|
||||||
|
});
|
||||||
|
return configurations;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error while fetching the instance configurations");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update instance configurations
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.instanceService.updateInstanceConfigurations(data);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = null;
|
||||||
|
this.configurations = this.configurations ? [...this.configurations, ...response] : response;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
21
web/store/application/router.store.ts
Normal file
21
web/store/application/router.store.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
|
||||||
|
export interface IRouterStore {
|
||||||
|
query: any;
|
||||||
|
setQuery: (query: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RouterStore implements IRouterStore {
|
||||||
|
query = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeObservable(this, {
|
||||||
|
query: observable,
|
||||||
|
setQuery: action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuery(query: any) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
// mobx
|
||||||
|
import { action, observable, makeObservable } from "mobx";
|
||||||
|
// helper
|
||||||
|
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
||||||
|
|
||||||
|
export interface IThemeStore {
|
||||||
|
theme: string | null;
|
||||||
|
sidebarCollapsed: boolean | undefined;
|
||||||
|
toggleSidebar: (collapsed?: boolean) => void;
|
||||||
|
setTheme: (theme: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThemeStore implements IThemeStore {
|
||||||
|
sidebarCollapsed: boolean | undefined = undefined;
|
||||||
|
theme: string | null = null;
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: any | null = null) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
sidebarCollapsed: observable.ref,
|
||||||
|
theme: observable.ref,
|
||||||
|
// action
|
||||||
|
toggleSidebar: action,
|
||||||
|
setTheme: action,
|
||||||
|
// computed
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar = (collapsed?: boolean) => {
|
||||||
|
if (collapsed === undefined) {
|
||||||
|
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||||
|
} else {
|
||||||
|
this.sidebarCollapsed = collapsed;
|
||||||
|
}
|
||||||
|
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
setTheme = async (_theme: { theme: any }) => {
|
||||||
|
try {
|
||||||
|
const currentTheme: string = _theme?.theme?.theme?.toString();
|
||||||
|
|
||||||
|
// updating the local storage theme value
|
||||||
|
localStorage.setItem("theme", currentTheme);
|
||||||
|
// updating the mobx theme value
|
||||||
|
this.theme = currentTheme;
|
||||||
|
|
||||||
|
// applying the theme to platform if the selected theme is custom
|
||||||
|
if (currentTheme === "custom") {
|
||||||
|
const themeSettings = this.rootStore.user.currentUserSettings || null;
|
||||||
|
applyTheme(
|
||||||
|
themeSettings?.theme?.palette !== ",,,,"
|
||||||
|
? themeSettings?.theme?.palette
|
||||||
|
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||||
|
themeSettings?.theme?.darkPalette
|
||||||
|
);
|
||||||
|
} else unsetCustomCssVariables();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("setting user theme error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -5,11 +5,12 @@ import { ProjectRootStore } from "./project";
|
|||||||
import { CycleStore } from "./cycle.store";
|
import { CycleStore } from "./cycle.store";
|
||||||
import { ProjectViewsStore } from "./project-view.store";
|
import { ProjectViewsStore } from "./project-view.store";
|
||||||
import { ModulesStore } from "./module.store";
|
import { ModulesStore } from "./module.store";
|
||||||
|
import { UserStore } from "./user";
|
||||||
|
|
||||||
enableStaticRendering(typeof window === "undefined");
|
enableStaticRendering(typeof window === "undefined");
|
||||||
|
|
||||||
export class RootStore {
|
export class RootStore {
|
||||||
app;
|
app: AppRootStore;
|
||||||
user;
|
user;
|
||||||
workspace;
|
workspace;
|
||||||
project;
|
project;
|
||||||
@ -18,8 +19,8 @@ export class RootStore {
|
|||||||
projectView;
|
projectView;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = new AppRootStore();
|
this.app = new AppRootStore(this);
|
||||||
// this.user = new UserRootStore();
|
this.user = new UserStore(this);
|
||||||
// this.workspace = new WorkspaceRootStore();
|
// this.workspace = new WorkspaceRootStore();
|
||||||
this.project = new ProjectRootStore(this);
|
this.project = new ProjectRootStore(this);
|
||||||
this.cycle = new CycleStore(this);
|
this.cycle = new CycleStore(this);
|
||||||
|
252
web/store/user/index.ts
Normal file
252
web/store/user/index.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||||
|
// services
|
||||||
|
import { UserService } from "services/user.service";
|
||||||
|
import { AuthService } from "services/auth.service";
|
||||||
|
// interfaces
|
||||||
|
import { IUser, IUserSettings } from "types/users";
|
||||||
|
// store
|
||||||
|
import { RootStore } from "../root.store";
|
||||||
|
import { UserMembershipStore } from "./user-membership.store";
|
||||||
|
|
||||||
|
export interface IUserStore {
|
||||||
|
loader: boolean;
|
||||||
|
currentUserError: any;
|
||||||
|
|
||||||
|
isUserLoggedIn: boolean | null;
|
||||||
|
currentUser: IUser | null;
|
||||||
|
isUserInstanceAdmin: boolean | null;
|
||||||
|
currentUserSettings: IUserSettings | null;
|
||||||
|
|
||||||
|
dashboardInfo: any;
|
||||||
|
|
||||||
|
fetchCurrentUser: () => Promise<IUser>;
|
||||||
|
fetchCurrentUserInstanceAdminStatus: () => Promise<boolean>;
|
||||||
|
fetchCurrentUserSettings: () => Promise<IUserSettings>;
|
||||||
|
|
||||||
|
fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise<any>;
|
||||||
|
|
||||||
|
updateUserOnBoard: () => Promise<void>;
|
||||||
|
updateTourCompleted: () => Promise<void>;
|
||||||
|
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser>;
|
||||||
|
updateCurrentUserTheme: (theme: string) => Promise<IUser>;
|
||||||
|
|
||||||
|
deactivateAccount: () => Promise<void>;
|
||||||
|
signOut: () => Promise<void>;
|
||||||
|
|
||||||
|
membership: UserMembershipStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserStore implements IUserStore {
|
||||||
|
loader: boolean = false;
|
||||||
|
currentUserError: any = null;
|
||||||
|
|
||||||
|
isUserLoggedIn: boolean | null = null;
|
||||||
|
currentUser: IUser | null = null;
|
||||||
|
isUserInstanceAdmin: boolean | null = null;
|
||||||
|
currentUserSettings: IUserSettings | null = null;
|
||||||
|
|
||||||
|
dashboardInfo: any = null;
|
||||||
|
membership: UserMembershipStore;
|
||||||
|
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
// services
|
||||||
|
userService;
|
||||||
|
authService;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
loader: observable.ref,
|
||||||
|
isUserLoggedIn: observable.ref,
|
||||||
|
currentUser: observable,
|
||||||
|
isUserInstanceAdmin: observable.ref,
|
||||||
|
currentUserSettings: observable,
|
||||||
|
dashboardInfo: observable,
|
||||||
|
// action
|
||||||
|
fetchCurrentUser: action,
|
||||||
|
fetchCurrentUserInstanceAdminStatus: action,
|
||||||
|
fetchCurrentUserSettings: action,
|
||||||
|
fetchUserDashboardInfo: action,
|
||||||
|
updateUserOnBoard: action,
|
||||||
|
updateTourCompleted: action,
|
||||||
|
updateCurrentUser: action,
|
||||||
|
updateCurrentUserTheme: action,
|
||||||
|
deactivateAccount: action,
|
||||||
|
signOut: action,
|
||||||
|
});
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
this.userService = new UserService();
|
||||||
|
this.authService = new AuthService();
|
||||||
|
this.membership = new UserMembershipStore(_rootStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await this.userService.currentUser();
|
||||||
|
if (response) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUserError = null;
|
||||||
|
this.currentUser = response;
|
||||||
|
this.isUserLoggedIn = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUserError = error;
|
||||||
|
this.isUserLoggedIn = false;
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCurrentUserInstanceAdminStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await this.userService.currentUserInstanceAdminStatus();
|
||||||
|
if (response) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.isUserInstanceAdmin = response.is_instance_admin;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.is_instance_admin;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.isUserInstanceAdmin = false;
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCurrentUserSettings = async () => {
|
||||||
|
try {
|
||||||
|
const response = await this.userService.currentUserSettings();
|
||||||
|
if (response) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUserSettings = response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserDashboardInfo = async (workspaceSlug: string, month: number) => {
|
||||||
|
try {
|
||||||
|
const response = await this.userService.userWorkspaceDashboard(workspaceSlug, month);
|
||||||
|
runInAction(() => {
|
||||||
|
this.dashboardInfo = response;
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateUserOnBoard = async () => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUser = {
|
||||||
|
...this.currentUser,
|
||||||
|
is_onboarded: true,
|
||||||
|
} as IUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = this.currentUser ?? undefined;
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
await this.userService.updateUserOnBoard();
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchCurrentUser();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTourCompleted = async () => {
|
||||||
|
try {
|
||||||
|
if (this.currentUser) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUser = {
|
||||||
|
...this.currentUser,
|
||||||
|
is_tour_completed: true,
|
||||||
|
} as IUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.userService.updateUserTourCompleted();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCurrentUser = async (data: Partial<IUser>) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUser = {
|
||||||
|
...this.currentUser,
|
||||||
|
...data,
|
||||||
|
} as IUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.userService.updateUser(data);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUser = response;
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchCurrentUser();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCurrentUserTheme = async (theme: string) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUser = {
|
||||||
|
...this.currentUser,
|
||||||
|
theme: {
|
||||||
|
...this.currentUser?.theme,
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
} as IUser;
|
||||||
|
});
|
||||||
|
const response = await this.userService.updateUser({
|
||||||
|
theme: { ...this.currentUser?.theme, theme },
|
||||||
|
} as IUser);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deactivateAccount = async () => {
|
||||||
|
try {
|
||||||
|
await this.userService.deactivateAccount();
|
||||||
|
this.currentUserError = null;
|
||||||
|
this.currentUser = null;
|
||||||
|
this.isUserLoggedIn = false;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
signOut = async () => {
|
||||||
|
try {
|
||||||
|
await this.authService.signOut();
|
||||||
|
runInAction(() => {
|
||||||
|
this.currentUserError = null;
|
||||||
|
this.currentUser = null;
|
||||||
|
this.isUserLoggedIn = false;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
229
web/store/user/user-membership.store.ts
Normal file
229
web/store/user/user-membership.store.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
// mobx
|
||||||
|
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||||
|
// services
|
||||||
|
import { ProjectMemberService, ProjectService } from "services/project";
|
||||||
|
import { UserService } from "services/user.service";
|
||||||
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
|
import { AuthService } from "services/auth.service";
|
||||||
|
// interfaces
|
||||||
|
import { IUser, IUserSettings } from "types/users";
|
||||||
|
import { IWorkspaceMemberMe, IProjectMember, TUserProjectRole, TUserWorkspaceRole } from "types";
|
||||||
|
import { RootStore } from "../root.store";
|
||||||
|
|
||||||
|
export interface IUserMembershipStore {
|
||||||
|
workspaceMemberInfo: {
|
||||||
|
[workspaceSlug: string]: IWorkspaceMemberMe;
|
||||||
|
};
|
||||||
|
hasPermissionToWorkspace: {
|
||||||
|
[workspaceSlug: string]: boolean | null;
|
||||||
|
};
|
||||||
|
projectMemberInfo: {
|
||||||
|
[projectId: string]: IProjectMember;
|
||||||
|
};
|
||||||
|
hasPermissionToProject: {
|
||||||
|
[projectId: string]: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
currentProjectMemberInfo: IProjectMember | undefined;
|
||||||
|
currentWorkspaceMemberInfo: IWorkspaceMemberMe | undefined;
|
||||||
|
currentProjectRole: TUserProjectRole | undefined;
|
||||||
|
currentWorkspaceRole: TUserWorkspaceRole | undefined;
|
||||||
|
|
||||||
|
hasPermissionToCurrentWorkspace: boolean | undefined;
|
||||||
|
hasPermissionToCurrentProject: boolean | undefined;
|
||||||
|
|
||||||
|
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
|
||||||
|
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<IProjectMember>;
|
||||||
|
|
||||||
|
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||||
|
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<any>;
|
||||||
|
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserMembershipStore implements IUserMembershipStore {
|
||||||
|
workspaceMemberInfo: {
|
||||||
|
[workspaceSlug: string]: IWorkspaceMemberMe;
|
||||||
|
} = {};
|
||||||
|
hasPermissionToWorkspace: {
|
||||||
|
[workspaceSlug: string]: boolean;
|
||||||
|
} = {};
|
||||||
|
projectMemberInfo: {
|
||||||
|
[projectId: string]: IProjectMember;
|
||||||
|
} = {};
|
||||||
|
hasPermissionToProject: {
|
||||||
|
[projectId: string]: boolean;
|
||||||
|
} = {};
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
// services
|
||||||
|
userService;
|
||||||
|
workspaceService;
|
||||||
|
projectService;
|
||||||
|
projectMemberService;
|
||||||
|
authService;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// observable
|
||||||
|
workspaceMemberInfo: observable.ref,
|
||||||
|
hasPermissionToWorkspace: observable.ref,
|
||||||
|
projectMemberInfo: observable.ref,
|
||||||
|
hasPermissionToProject: observable.ref,
|
||||||
|
// action
|
||||||
|
fetchUserWorkspaceInfo: action,
|
||||||
|
fetchUserProjectInfo: action,
|
||||||
|
leaveWorkspace: action,
|
||||||
|
joinProject: action,
|
||||||
|
leaveProject: action,
|
||||||
|
// computed
|
||||||
|
currentProjectMemberInfo: computed,
|
||||||
|
currentWorkspaceMemberInfo: computed,
|
||||||
|
currentProjectRole: computed,
|
||||||
|
currentWorkspaceRole: computed,
|
||||||
|
hasPermissionToCurrentWorkspace: computed,
|
||||||
|
hasPermissionToCurrentProject: computed,
|
||||||
|
});
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
this.userService = new UserService();
|
||||||
|
this.workspaceService = new WorkspaceService();
|
||||||
|
this.projectService = new ProjectService();
|
||||||
|
this.projectMemberService = new ProjectMemberService();
|
||||||
|
this.authService = new AuthService();
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentWorkspaceMemberInfo() {
|
||||||
|
if (!this.rootStore.workspace.workspaceSlug) return;
|
||||||
|
return this.workspaceMemberInfo[this.rootStore.workspace.workspaceSlug];
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentWorkspaceRole() {
|
||||||
|
if (!this.rootStore.workspace.workspaceSlug) return;
|
||||||
|
return this.workspaceMemberInfo[this.rootStore.workspace.workspaceSlug]?.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentProjectMemberInfo() {
|
||||||
|
if (!this.rootStore.project.projectId) return;
|
||||||
|
return this.projectMemberInfo[this.rootStore.project.projectId];
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentProjectRole() {
|
||||||
|
if (!this.rootStore.project.projectId) return;
|
||||||
|
return this.projectMemberInfo[this.rootStore.project.projectId]?.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPermissionToCurrentWorkspace() {
|
||||||
|
if (!this.rootStore.workspace.workspaceSlug) return;
|
||||||
|
return this.hasPermissionToWorkspace[this.rootStore.workspace.workspaceSlug];
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPermissionToCurrentProject() {
|
||||||
|
if (!this.rootStore.project.projectId) return;
|
||||||
|
return this.hasPermissionToProject[this.rootStore.project.projectId];
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserWorkspaceInfo = async (workspaceSlug: string) => {
|
||||||
|
try {
|
||||||
|
const response = await this.workspaceService.workspaceMemberMe(workspaceSlug);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.workspaceMemberInfo = {
|
||||||
|
...this.workspaceMemberInfo,
|
||||||
|
[workspaceSlug]: response,
|
||||||
|
};
|
||||||
|
this.hasPermissionToWorkspace = {
|
||||||
|
...this.hasPermissionToWorkspace,
|
||||||
|
[workspaceSlug]: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.hasPermissionToWorkspace = {
|
||||||
|
...this.hasPermissionToWorkspace,
|
||||||
|
[workspaceSlug]: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserProjectInfo = async (workspaceSlug: string, projectId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await this.projectMemberService.projectMemberMe(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.projectMemberInfo = {
|
||||||
|
...this.projectMemberInfo,
|
||||||
|
[projectId]: response,
|
||||||
|
};
|
||||||
|
this.hasPermissionToProject = {
|
||||||
|
...this.hasPermissionToProject,
|
||||||
|
[projectId]: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.hasPermissionToProject = {
|
||||||
|
...this.hasPermissionToProject,
|
||||||
|
[projectId]: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
leaveWorkspace = async (workspaceSlug: string) => {
|
||||||
|
try {
|
||||||
|
await this.userService.leaveWorkspace(workspaceSlug);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
delete this.workspaceMemberInfo[workspaceSlug];
|
||||||
|
delete this.hasPermissionToWorkspace[workspaceSlug];
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
joinProject = async (workspaceSlug: string, projectIds: string[]) => {
|
||||||
|
const newPermissions: { [projectId: string]: boolean } = {};
|
||||||
|
projectIds.forEach((projectId) => {
|
||||||
|
newPermissions[projectId] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.userService.joinProject(workspaceSlug, projectIds);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.hasPermissionToProject = {
|
||||||
|
...this.hasPermissionToProject,
|
||||||
|
...newPermissions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
leaveProject = async (workspaceSlug: string, projectId: string) => {
|
||||||
|
const newPermissions: { [projectId: string]: boolean } = {};
|
||||||
|
newPermissions[projectId] = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.userService.leaveProject(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.hasPermissionToProject = {
|
||||||
|
...this.hasPermissionToProject,
|
||||||
|
...newPermissions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -45,8 +45,8 @@ export class CalendarHelpers implements ICalendarHelpers {
|
|||||||
target_date: destinationColumnId,
|
target_date: destinationColumnId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (viewId) store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId);
|
if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId);
|
||||||
else store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue);
|
else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user