Merge branch 'preview' of https://github.com/makeplane/plane into feat/bulk-operations

This commit is contained in:
Aaryan Khandelwal 2024-05-24 13:19:59 +05:30
commit f1e8b1769e
23 changed files with 169 additions and 123 deletions

View File

@ -33,6 +33,7 @@ class UserSerializer(BaseSerializer):
"is_bot",
"is_password_autoset",
"is_email_verified",
"is_active",
]
extra_kwargs = {"password": {"write_only": True}}

View File

@ -1,3 +1,6 @@
# Python imports
# import uuid
# Django imports
from django.db.models import Case, Count, IntegerField, Q, When
from django.contrib.auth import logout
@ -26,6 +29,7 @@ from plane.db.models import (
User,
WorkspaceMember,
WorkspaceMemberInvite,
Session,
)
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
@ -160,12 +164,13 @@ class UserEndpoint(BaseViewSet):
email=user.email,
).delete()
# Deactivate the user
user.is_active = False
# Delete all sessions
Session.objects.filter(user_id=request.user.id).delete()
# Profile updates
profile = Profile.objects.get(user=user)
# Reset onboarding
profile.last_workspace_id = None
profile.is_tour_completed = False
profile.is_onboarded = False
@ -177,7 +182,12 @@ class UserEndpoint(BaseViewSet):
}
profile.save()
# User log out
# Reset password
# user.is_password_autoset = True
# user.set_password(uuid.uuid4().hex)
# Deactivate the user
user.is_active = False
user.last_logout_ip = user_ip(request=request)
user.last_logout_time = timezone.now()
user.save()

View File

@ -85,5 +85,6 @@ class OauthAdapter(Adapter):
"refresh_token_expired_at"
),
"last_connected_at": timezone.now(),
"id_token": self.token_data.get("id_token", ""),
},
)

View File

@ -100,6 +100,7 @@ class GitHubOAuthProvider(OauthAdapter):
if token_response.get("refresh_token_expired_at")
else None
),
"id_token": token_response.get("id_token", ""),
}
)

View File

@ -98,6 +98,7 @@ class GoogleOAuthProvider(OauthAdapter):
if token_response.get("refresh_token_expired_at")
else None
),
"id_token": token_response.get("id_token", ""),
}
)

View File

@ -0,0 +1,58 @@
# Generated by Django 4.2.11 on 2024-05-22 15:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0065_auto_20240415_0937"),
]
operations = [
migrations.AddField(
model_name="account",
name="id_token",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="cycle",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="module",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="issueview",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="inbox",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="dashboard",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="widget",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="issue",
name="description_binary",
field=models.BinaryField(null=True),
),
migrations.AddField(
model_name="team",
name="logo_props",
field=models.JSONField(default=dict),
),
]

View File

@ -70,6 +70,7 @@ class Cycle(ProjectBaseModel):
external_id = models.CharField(max_length=255, blank=True, null=True)
progress_snapshot = models.JSONField(default=dict)
archived_at = models.DateTimeField(null=True)
logo_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"

View File

@ -31,6 +31,7 @@ class Dashboard(BaseModel):
verbose_name="Dashboard Type",
default="home",
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the dashboard"""
@ -53,6 +54,7 @@ class Widget(TimeAuditModel):
)
key = models.CharField(max_length=255)
filters = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the widget"""

View File

@ -12,6 +12,7 @@ class Inbox(ProjectBaseModel):
)
is_default = models.BooleanField(default=False)
view_props = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the Inbox"""

View File

@ -128,6 +128,7 @@ class Issue(ProjectBaseModel):
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
description_binary = models.BinaryField(null=True)
priority = models.CharField(
max_length=30,
choices=PRIORITY_CHOICES,

View File

@ -93,6 +93,7 @@ class Module(ProjectBaseModel):
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
archived_at = models.DateTimeField(null=True)
logo_props = models.JSONField(default=dict)
class Meta:
unique_together = ["name", "project"]

View File

@ -189,6 +189,7 @@ class Account(TimeAuditModel):
refresh_token = models.TextField(null=True, blank=True)
refresh_token_expired_at = models.DateTimeField(null=True)
last_connected_at = models.DateTimeField(default=timezone.now)
id_token = models.TextField(blank=True)
metadata = models.JSONField(default=dict)
class Meta:

View File

@ -52,6 +52,7 @@ def get_default_display_properties():
}
# DEPRECATED TODO: - Remove in next release
class GlobalView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
@ -87,7 +88,6 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>"
# DEPRECATED TODO: - Remove in next release
class IssueView(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
@ -101,6 +101,7 @@ class IssueView(WorkspaceBaseModel):
default=1, choices=((0, "Private"), (1, "Public"))
)
sort_order = models.FloatField(default=65535)
logo_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue View"

View File

@ -244,6 +244,7 @@ class Team(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="workspace_team"
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the team"""

View File

@ -44,9 +44,9 @@ export const buttonStyling: IButtonStyling = {
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
},
"accent-primary": {
default: `bg-custom-primary-10 text-custom-primary-100`,
hover: `hover:bg-custom-primary-20 hover:text-custom-primary-200`,
pressed: `focus:bg-custom-primary-20`,
default: `bg-custom-primary-100/20 text-custom-primary-100`,
hover: `hover:bg-custom-primary-100/10 hover:text-custom-primary-200`,
pressed: `focus:bg-custom-primary-100/10`,
disabled: `cursor-not-allowed !text-custom-primary-60`,
},
"outline-primary": {

View File

@ -55,6 +55,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
const additionalPath = activeLayout ?? "list";
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
return (
<>
@ -70,6 +71,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
<EmptyState
type={emptyStateType}
additionalPath={additionalPath}
size={emptyStateSize}
primaryButtonOnClick={
isEmptyFilters
? undefined

View File

@ -6,7 +6,7 @@ import { useTheme } from "next-themes";
// types
import { IWorkspaceMemberInvitation, TOnboardingSteps } from "@plane/types";
// components
import { Invitations, OnboardingHeader, SwitchOrDeleteAccountDropdown, CreateWorkspace } from "@/components/onboarding";
import { Invitations, OnboardingHeader, SwitchAccountDropdown, CreateWorkspace } from "@/components/onboarding";
// hooks
import { useUser } from "@/hooks/store";
// assets
@ -55,7 +55,7 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-between">
<OnboardingHeader currentStep={totalSteps - 1} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6">
@ -79,7 +79,7 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? CreateJoinWorkspaceDark : CreateJoinWorkspace}

View File

@ -5,6 +5,6 @@ export * from "./profile-setup";
export * from "./create-workspace";
export * from "./invitations";
export * from "./step-indicator";
export * from "./switch-or-delete-account-dropdown";
export * from "./switch-delete-account-modal";
export * from "./switch-account-dropdown";
export * from "./switch-account-modal";
export * from "./header";

View File

@ -34,7 +34,7 @@ import InviteMembersDark from "public/onboarding/invite-members-dark.svg";
import InviteMembersLight from "public/onboarding/invite-members-light.svg";
// components
import { OnboardingHeader } from "./header";
import { SwitchOrDeleteAccountDropdown } from "./switch-or-delete-account-dropdown";
import { SwitchAccountDropdown } from "./switch-account-dropdown";
type Props = {
finishOnboarding: () => Promise<void>;
@ -366,7 +366,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
{/* Since this will always be the last step */}
<OnboardingHeader currentStep={totalSteps} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6 md:w-4/5 mx-auto">
@ -433,7 +433,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchOrDeleteAccountDropdown />
<SwitchAccountDropdown />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? InviteMembersDark : InviteMembersLight}

View File

@ -11,7 +11,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { PasswordStrengthMeter } from "@/components/account";
import { UserImageUploadModal } from "@/components/core";
import { OnboardingHeader, SwitchOrDeleteAccountDropdown } from "@/components/onboarding";
import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding";
// constants
import { USER_DETAILS } from "@/constants/event-tracker";
// helpers
@ -276,7 +276,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
<div className="flex items-center justify-between">
<OnboardingHeader currentStep={isCurrentStepUserPersonalization ? 2 : 1} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchOrDeleteAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<SwitchAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6">
@ -567,7 +567,7 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchOrDeleteAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<SwitchAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<div className="absolute inset-0 z-0">
{profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION ? (
<Image

View File

@ -9,16 +9,16 @@ import { cn } from "@/helpers/common.helper";
// hooks
import { useUser } from "@/hooks/store";
// components
import { SwitchOrDeleteAccountModal } from "./switch-delete-account-modal";
import { SwitchAccountModal } from "./switch-account-modal";
type TSwithOrDeleteAccountDropdownProps = {
type TSwitchAccountDropdownProps = {
fullName?: string;
};
export const SwitchOrDeleteAccountDropdown: FC<TSwithOrDeleteAccountDropdownProps> = observer((props) => {
export const SwitchAccountDropdown: FC<TSwitchAccountDropdownProps> = observer((props) => {
const { fullName } = props;
// states
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false);
const [showSwitchAccountModal, setShowSwitchAccountModal] = useState(false);
// store hooks
const { data: user } = useUser();
@ -30,7 +30,7 @@ export const SwitchOrDeleteAccountDropdown: FC<TSwithOrDeleteAccountDropdownProp
return (
<div className="flex w-full shrink-0 justify-end">
<SwitchOrDeleteAccountModal isOpen={showDeleteAccountModal} onClose={() => setShowDeleteAccountModal(false)} />
<SwitchAccountModal isOpen={showSwitchAccountModal} onClose={() => setShowSwitchAccountModal(false)} />
<div className="flex items-center gap-x-2 pr-4 z-10">
{user?.avatar && (
<Avatar
@ -64,7 +64,7 @@ export const SwitchOrDeleteAccountDropdown: FC<TSwithOrDeleteAccountDropdownProp
"bg-custom-background-80": active,
})
}
onClick={() => setShowDeleteAccountModal(true)}
onClick={() => setShowSwitchAccountModal(true)}
>
Wrong e-mail address?
</Menu.Item>

View File

@ -1,34 +1,31 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { mutate } from "swr";
import { Trash2 } from "lucide-react";
import { ArrowRightLeft } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { useUser } from "@/hooks/store";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useUser } from "@/hooks/store";
type Props = {
isOpen: boolean;
onClose: () => void;
};
export const SwitchOrDeleteAccountModal: React.FC<Props> = (props) => {
export const SwitchAccountModal: React.FC<Props> = (props) => {
const { isOpen, onClose } = props;
// states
const [switchingAccount, setSwitchingAccount] = useState(false);
const [isDeactivating, setIsDeactivating] = useState(false);
// router
const router = useRouter();
// store hooks
const { signOut, deactivateAccount } = useUser();
const { data: userData, signOut } = useUser();
const { setTheme } = useTheme();
const handleClose = () => {
setSwitchingAccount(false);
setIsDeactivating(false);
onClose();
};
@ -51,32 +48,6 @@ export const SwitchOrDeleteAccountModal: React.FC<Props> = (props) => {
.finally(() => setSwitchingAccount(false));
};
const handleDeactivateAccount = async () => {
setIsDeactivating(true);
await deactivateAccount()
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Account deleted successfully.",
});
mutate("CURRENT_USER_DETAILS", null);
signOut();
setTheme("system");
router.push("/");
handleClose();
})
.catch((err: any) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error,
})
)
.finally(() => setIsDeactivating(false));
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -104,32 +75,30 @@ export const SwitchOrDeleteAccountModal: React.FC<Props> = (props) => {
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div>
<div className="flex items-center gap-x-4">
<div className="grid place-items-center rounded-full bg-red-500/20 p-4">
<Trash2 className="h-6 w-6 text-red-600" aria-hidden="true" />
<div className="p-6 pb-1">
<div className="flex gap-x-4">
<div className="flex items-start">
<div className="grid place-items-center rounded-full bg-custom-primary-100/20 p-4">
<ArrowRightLeft className="h-5 w-5 text-custom-primary-100" aria-hidden="true" />
</div>
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Not the right workspace?
</Dialog.Title>
</div>
<div className="mt-6 px-4">
<ul className="list-disc text-base font-normal text-onboarding-text-300">
<li>Delete this account if you have another and won{"'"}t use this account.</li>
<li>Switch to another account if you{"'"}d like to come back to this account another time.</li>
</ul>
<div className="flex flex-col py-3 gap-y-6">
<Dialog.Title as="h3" className="text-2xl font-medium leading-6 text-onboarding-text-100">
Switch account
</Dialog.Title>
{userData?.email && (
<div className="text-base font-normal text-onboarding-text-200">
If you have signed up via <span className="text-custom-primary-100">{userData.email}</span>{" "}
un-intentionally, you can switch your account to a different one from here.
</div>
)}
</div>
</div>
</div>
<div className="mb-2 flex items-center justify-end gap-3 p-4 sm:px-6">
<Button variant="neutral-primary" onClick={handleSwitchAccount} disabled={switchingAccount}>
<Button variant="accent-primary" onClick={handleSwitchAccount} disabled={switchingAccount}>
{switchingAccount ? "Switching..." : "Switch account"}
</Button>
<Button variant="outline-danger" onClick={handleDeactivateAccount} loading={isDeactivating}>
{isDeactivating ? "Deleting..." : "Delete account"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>

View File

@ -70,53 +70,47 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
return (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
{invitationDetail ? (
<>
{error ? (
<div className="flex w-full flex-col space-y-4 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-8 text-center shadow-2xl md:w-1/3">
<h2 className="text-xl uppercase">INVITATION NOT FOUND</h2>
</div>
) : (
<>
{invitationDetail.accepted ? (
<>
<EmptySpace
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
</EmptySpace>
</>
) : (
<EmptySpace
title={`You have been invited to ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Check} title="Accept" action={handleAccept} />
<EmptySpaceItem Icon={X} title="Ignore" action={handleReject} />
</EmptySpace>
)}
</>
)}
</>
) : error ? (
<EmptySpace
title="This invitation link is not active anymore."
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
link={{ text: "Or start from an empty project", href: "/" }}
>
{!currentUser ? (
<EmptySpaceItem Icon={User2} title="Sign in to continue" href="/" />
) : (
{invitationDetail && !invitationDetail.responded_at ? (
error ? (
<div className="flex w-full flex-col space-y-4 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-8 text-center shadow-2xl md:w-1/3">
<h2 className="text-xl uppercase">INVITATION NOT FOUND</h2>
</div>
) : (
<EmptySpace
title={`You have been invited to ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Check} title="Accept" action={handleAccept} />
<EmptySpaceItem Icon={X} title="Ignore" action={handleReject} />
</EmptySpace>
)
) : error || invitationDetail?.responded_at ? (
invitationDetail?.accepted ? (
<EmptySpace
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
)}
<EmptySpaceItem Icon={Star} title="Star us on GitHub" href="https://github.com/makeplane" />
<EmptySpaceItem
Icon={Share2}
title="Join our community of active creators"
href="https://discord.com/invite/8SR2N9PAcJ"
/>
</EmptySpace>
</EmptySpace>
) : (
<EmptySpace
title="This invitation link is not active anymore."
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
link={{ text: "Or start from an empty project", href: "/" }}
>
{!currentUser ? (
<EmptySpaceItem Icon={User2} title="Sign in to continue" href="/" />
) : (
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
)}
<EmptySpaceItem Icon={Star} title="Star us on GitHub" href="https://github.com/makeplane" />
<EmptySpaceItem
Icon={Share2}
title="Join our community of active creators"
href="https://discord.com/invite/A92xrEGCge"
/>
</EmptySpace>
)
) : (
<div className="flex h-full w-full items-center justify-center">
<LogoSpinner />