pull from branch and resolve conflicts

This commit is contained in:
rahulramesha 2023-12-11 20:26:54 +05:30
commit 3269b5a0cf
76 changed files with 1608 additions and 427 deletions

View File

@ -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(
{ {

View File

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

View File

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

View File

@ -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";

View File

@ -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>
</> </>

View File

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

View File

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

View File

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

View File

@ -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";

View File

@ -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>
</> </>

View File

@ -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>

View File

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

View File

@ -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">

View File

@ -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">

View File

@ -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>

View File

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

View File

@ -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"

View File

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

View File

@ -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"

View File

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

View File

@ -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>
</> </>

View File

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

View File

@ -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>

View File

@ -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(

View File

@ -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(),

View File

@ -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,

View File

@ -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(),

View File

@ -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(),

View File

@ -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!",

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}`} }`}
> >

View File

@ -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",

View File

@ -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>

View File

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

View File

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

View File

@ -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) => (

View File

@ -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 (

View File

@ -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",

View File

@ -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" />

View File

@ -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" />

View File

@ -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",

View File

@ -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" />

View File

@ -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

View File

@ -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",

View File

@ -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>

View File

@ -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 (

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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