Compare commits

..

5 Commits

Author SHA1 Message Date
sriram veeraghanta 1a0f912ab4 fix: cloud fixes 2023-10-30 16:50:15 +05:30
sriram veeraghanta 912851659c Merge branch 'develop' of github.com:makeplane/plane into dev/upgrade-workspace 2023-10-30 16:15:32 +05:30
sriram veeraghanta 85e53af66c fix: merge conflicts resolved 2023-10-27 14:06:41 +05:30
sriram veeraghanta 6f751fd0a4 fix: integration fixes 2023-10-25 21:01:23 +05:30
sriramveeraghanta 67c0b73260 feat: Workspace upgrade functionality 2023-10-25 11:58:40 +05:30
11 changed files with 216 additions and 136 deletions
+20 -34
View File
@@ -12,20 +12,10 @@ export interface EmailPasswordFormValues {
export interface IEmailPasswordForm {
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
buttonText?: string;
submittingButtonText?: string;
withForgetPassword?: boolean;
withSignUpLink?: boolean;
}
export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
const {
onSubmit,
buttonText = "Sign in",
submittingButtonText = "Signing in...",
withForgetPassword = false,
withSignUpLink = false,
} = props;
const { onSubmit } = props;
// router
const router = useRouter();
// form info
@@ -92,17 +82,15 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
)}
/>
</div>
{withForgetPassword && (
<div className="text-right text-xs">
<button
type="button"
onClick={() => router.push("/accounts/forgot-password")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
Forgot your password?
</button>
</div>
)}
<div className="text-right text-xs">
<button
type="button"
onClick={() => router.push("/accounts/forgot-password")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
Forgot your password?
</button>
</div>
<div>
<Button
variant="primary"
@@ -112,20 +100,18 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? submittingButtonText : buttonText}
{isSubmitting ? "Signing in..." : "Sign in"}
</Button>
</div>
{withSignUpLink && (
<div className="text-xs">
<button
type="button"
onClick={() => router.push("/accounts/sign-up")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
{"Don't have an account? Sign Up"}
</button>
</div>
)}
<div className="text-xs">
<button
type="button"
onClick={() => router.push("/accounts/sign-up")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
{"Don't have an account? Sign Up"}
</button>
</div>
</form>
</>
);
+3 -3
View File
@@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
// ui
import { Button, Input } from "@plane/ui";
// types
export type EmailPasswordSignUpFormValues = {
type EmailPasswordFormValues = {
email: string;
password?: string;
confirm_password: string;
@@ -12,7 +12,7 @@ export type EmailPasswordSignUpFormValues = {
};
type Props = {
onSubmit: (formData: EmailPasswordSignUpFormValues) => Promise<void>;
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
};
export const EmailSignUpForm: React.FC<Props> = (props) => {
@@ -23,7 +23,7 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
control,
watch,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailPasswordSignUpFormValues>({
} = useForm<EmailPasswordFormValues>({
defaultValues: {
email: "",
password: "",
+1 -3
View File
@@ -210,9 +210,7 @@ export const SignInView = observer(() => {
) : (
<>
<>
{enableEmailPassword && (
<EmailPasswordForm onSubmit={handlePasswordSignIn} withForgetPassword withSignUpLink />
)}
{enableEmailPassword && <EmailPasswordForm onSubmit={handlePasswordSignIn} />}
{data?.magic_login && (
<div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-7">
-1
View File
@@ -30,7 +30,6 @@ export const ProjectSidebarList: FC = observer(() => {
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
// refs
const containerRef = useRef<HTMLDivElement | null>(null);
// toast
const { setToastAlert } = useToast();
+1 -3
View File
@@ -149,9 +149,7 @@ export const SignInView = observer(() => {
Sign in to Plane
</h1>
<>
{enableEmailPassword && (
<EmailPasswordForm onSubmit={handlePasswordSignIn} withForgetPassword withSignUpLink />
)}
{enableEmailPassword && <EmailPasswordForm onSubmit={handlePasswordSignIn} />}
{data?.magic_login && (
<div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-7">
+16 -4
View File
@@ -12,6 +12,7 @@ import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-reac
import { DiscordIcon, GithubIcon } from "@plane/ui";
// assets
import packageJson from "package.json";
import { UpgradeWorkspaceModal } from "./upgrade-modal";
const helpOptions = [
{
@@ -37,15 +38,14 @@ const helpOptions = [
},
];
export interface WorkspaceHelpSectionProps {
setSidebarActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
export interface WorkspaceHelpSectionProps {}
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
// store
const { theme: themeStore } = useMobxStore();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const [isUpgradeModalOpen, setUpgradeModalOpen] = useState(false);
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
@@ -53,15 +53,27 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
const isCollapsed = themeStore.sidebarCollapsed || false;
const openUpgradeModal = () => {
setUpgradeModalOpen(true);
};
const handleUpgradeModalClose = () => {
setUpgradeModalOpen(false);
};
return (
<>
<UpgradeWorkspaceModal isOpen={isUpgradeModalOpen} handleClose={handleUpgradeModalClose} />
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 px-4 ${
isCollapsed ? "flex-col" : ""
}`}
>
{!isCollapsed && (
<div className="w-1/2 text-center cursor-default rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
<div
className="w-1/2 cursor-pointer text-center rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500"
onClick={openUpgradeModal}
>
Free Plan
</div>
)}
+1
View File
@@ -11,3 +11,4 @@ export * from "./sidebar-dropdown";
export * from "./sidebar-menu";
export * from "./sidebar-quick-action";
export * from "./member-select";
export * from "./upgrade-modal";
+135
View File
@@ -0,0 +1,135 @@
import { FC, Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
// icons
import { Check } from "lucide-react";
import useSWR from "swr";
// services
import { LicenseService } from "services/license.service";
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
const licenseService = new LicenseService();
export interface IUpgradeWorkspaceModal {
isOpen: boolean;
handleClose: () => void;
}
export const UpgradeWorkspaceModal: FC<IUpgradeWorkspaceModal> = observer((props) => {
const { isOpen, handleClose } = props;
// store
const { user: userStore, workspace: workspaceStore } = useMobxStore();
const { currentUser } = userStore;
const { workspaceMembers, currentWorkspace } = workspaceStore;
// fetching products
const { data: products } = useSWR("UPGRADE_PRODUCTS", () => licenseService.getProducts());
const handleUpgrade = (product: any) => {
if (currentUser && workspaceMembers && workspaceMembers?.length >= 1 && currentWorkspace) {
licenseService
.createCheckoutSession(
product?.default_price?.id,
workspaceMembers?.length,
currentWorkspace,
currentUser,
product?.metadata
)
.then((response) => {
console.log("subscription", response);
window.open(response.url, "_blank");
});
}
};
return (
<>
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all sm:w-3/5 lg:w-1/2 xl:w-2/5">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
Upgrade your plan
</Dialog.Title>
<div className="mx-auto max-w-4xl">
<div className="mx-auto mt-10 grid max-w-md grid-cols-1 gap-8 lg:mx-0 lg:max-w-none lg:grid-cols-2">
{products?.map((product: any) => (
<div
key={product.id}
className={`${
product?.mostPopular ? "ring-2 ring-[#3e76fe]" : "ring-1 ring-gray-200"
} rounded-3xl p-8 xl:p-10`}
>
<div className="flex items-center justify-between gap-x-4">
<h3
id={product.id}
className={`${
product?.mostPopular ? "text-[#3e76fe]" : "text-gray-900"
} text-lg font-semibold leading-8`}
>
{product.name}
</h3>
{product?.mostPopular ? (
<p className="rounded-full bg-[#3e76fe]/10 px-2.5 py-1 text-xs font-semibold leading-5 text-[#3e76fe]">
Most popular
</p>
) : null}
</div>
<p className="mt-4 text-sm leading-6 text-gray-600">{product.description}</p>
<p className="mt-6 flex items-baseline gap-x-1">
<span className="text-4xl font-bold tracking-tight text-gray-900">
$ {product.default_price.unit_amount / 100}
</span>
<span className="text-sm font-semibold leading-6 text-gray-600">/ per month</span>
</p>
<button
onClick={() => handleUpgrade(product)}
className={`${
product?.mostPopular
? "bg-[#3e76fe] text-white shadow-sm hover:bg-indigo-500"
: "text-[#3e76fe] ring-1 ring-inset ring-indigo-200 hover:ring-indigo-300"
} mt-6 block rounded-md px-3 py-2 text-center text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#3e76fe] cursor-pointer`}
>
Upgrade
</button>
<ul role="list" className="mt-8 space-y-3 text-sm leading-6 text-gray-600 xl:mt-10">
{product.features?.map((feature: any) => (
<li key={feature.name} className="flex gap-x-3">
<Check className="h-6 w-5 flex-none text-[#3e76fe]" aria-hidden="true" />
{feature.name}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
);
});
-58
View File
@@ -1,58 +0,0 @@
import Image from "next/image";
// layouts
import DefaultLayout from "layouts/default-layout";
// components
import { EmailPasswordForm, EmailPasswordFormValues } from "components/account";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// services
import { InstanceService } from "services/instance.service";
import { useRouter } from "next/router";
import { useEffect } from "react";
const instanceService = new InstanceService();
const ActivateInstancePage = () => {
const router = useRouter();
useEffect(() => {
instanceService.checkForInstanceStatus().then((response) => {
console.log(response);
});
}, []);
const handleSignUp = (values: EmailPasswordFormValues) =>
instanceService
.createInstance(values)
.then((response) => {
router.push("/");
})
.catch((error) => {});
return (
<DefaultLayout>
<>
<div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" />
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="grid place-items-center bg-custom-background-100">
<div className="h-[30px] w-[30px]">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div>
</div>
</>
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div>
<h1 className="text-3xl text-center font-bold">Activate Your Instance</h1>
<EmailPasswordForm
onSubmit={handleSignUp}
buttonText="Activate Instance"
submittingButtonText="Activating Instance"
/>
</div>
</div>
</DefaultLayout>
);
};
export default ActivateInstancePage;
-30
View File
@@ -1,30 +0,0 @@
import { APIService } from "services/api.service";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
import Cookies from "js-cookie";
export class InstanceService extends APIService {
constructor() {
super(API_BASE_URL);
}
async checkForInstanceStatus() {
return this.get("/api/licenses/instances/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async createInstance(values: any) {
return this.post("/api/licenses/instances/", values)
.then((response) => {
console.log(response);
Cookies.set("instance_id", response?.data?.instance_id);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
}
+39
View File
@@ -0,0 +1,39 @@
// services
import { APIService } from "services/api.service";
import { IUser, IWorkspace } from "types";
export class LicenseService extends APIService {
constructor() {
super("http://localhost:8080");
}
async getProducts(): Promise<any[]> {
return this.get(`/api/products/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async createSubscription(user: IUser, priceId: string): Promise<any> {
return this.post(`/api/subscriptions/`, { user, priceId })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async createCheckoutSession(
priceId: string,
seats: number,
workspace: IWorkspace,
user: IUser,
metadata: any
): Promise<any> {
return this.post(`/api/checkout/create-session/`, { priceId, seats, workspace, user, metadata })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}