Compare commits

..
19 changed files with 224 additions and 237 deletions
+2 -3
View File
@@ -579,7 +579,6 @@ class CycleIssueViewSet(BaseViewSet):
)
)
total_issues = issues.count()
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
@@ -591,12 +590,12 @@ class CycleIssueViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues_data, "total_issues": total_issues}, status=status.HTTP_200_OK
issues_data, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id, cycle_id):
+6 -9
View File
@@ -217,7 +217,6 @@ class IssueViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
total_issues = issue_queryset.count()
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
@@ -232,12 +231,12 @@ class IssueViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
issues, status=status.HTTP_200_OK
)
@@ -426,7 +425,6 @@ class UserWorkSpaceIssues(BaseAPIView):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
total_issues = issue_queryset.count()
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
@@ -441,12 +439,12 @@ class UserWorkSpaceIssues(BaseAPIView):
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
issues, status=status.HTTP_200_OK
)
@@ -2151,7 +2149,6 @@ class IssueDraftViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
total_issues = issue_queryset.count()
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
@@ -2159,12 +2156,12 @@ class IssueDraftViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues, group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
issues, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id):
+2 -3
View File
@@ -364,7 +364,6 @@ class ModuleIssueViewSet(BaseViewSet):
.values("count")
)
)
total_issues = issues.count()
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
@@ -376,12 +375,12 @@ class ModuleIssueViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues_data, "total_issues": total_issues}, status=status.HTTP_200_OK
issues_data, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id, module_id):
+5 -14
View File
@@ -93,7 +93,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
filters = issue_filters(request.query_params, "GET")
@@ -117,9 +116,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -129,9 +126,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
priority_order if order_by_param == "priority" else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
@@ -183,8 +178,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
total_issues = issue_queryset.count()
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
@@ -195,17 +188,15 @@ class GlobalViewIssuesViewSet(BaseViewSet):
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet):
+2 -3
View File
@@ -1223,7 +1223,6 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
total_issues = issue_queryset.count()
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
@@ -1231,12 +1230,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
if group_by:
grouped_results = group_results(issues, group_by)
return Response(
{"data": grouped_results, "total_issues": total_issues},
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
issues, status=status.HTTP_200_OK
)
@@ -11,7 +11,7 @@ import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
// helper
import { stripHTML, replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper";
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
import {
formatDateDistance,
render12HourFormatTime,
@@ -115,10 +115,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
) : notification.data.issue_activity.field === "attachment" ? (
"the issue"
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
) : notification.data.issue_activity.field === "description" ? (
stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
) : (
stripHTML(notification.data.issue_activity.new_value)
notification.data.issue_activity.new_value
)
) : (
<span>
-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,20 +0,0 @@
import React from "react";
import { Button } from "@plane/ui";
import Image from "next/image";
import EmptyWebhookLogo from "public/empty-state/issue.svg";
export const EmptyWebhooks = () => {
return (
<div className="flex items-start justify-center">
<div className="flex p-10 flex-col items-center justify-center rounded-[4px] border border-neutral-200 bg-neutral-50">
<Image width="178" height="116" src={EmptyWebhookLogo} alt="empty-webhook image" />
<h3 className="mt-4 text-base font-semibold">No Webhooks yet</h3>
<p className="text-sm text-neutral-600">Create labels to help organize and filter issues in your project</p>
<Button variant="primary" className="mt-2">
Add Webhook
</Button>
</div>
</div>
);
};
-4
View File
@@ -1,4 +0,0 @@
export * from "./empty-webhooks";
export * from "./webhooks-list";
export * from "./webhooks-list-item";
export * from "./webhook-details";
@@ -1,99 +0,0 @@
import React, { useState } from "react";
import { Button, ToggleSwitch } from "@plane/ui";
import { RefreshCw } from "lucide-react";
export const WebhookDetails = () => {
const [isIndividualEventsSelected, setIndividualEventsSelected] = useState<boolean>(false);
const [isWebhookEnabled, setWebhookEnabled] = useState<boolean>(false);
const [isRegenarateKeyLoading, setRegenarateKeyLoading] = useState<boolean>(false);
const [showGenaratedKey, setShowGenaratedKey] = useState<boolean>(true);
return (
<>
<div className="px-32 pt-10">
<h3 className="font-medium text-2xl">Webhook Name</h3>
<p className="font-normal text-base my-1 text-neutral-700">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex.
</p>
<div className="border-b my-6 border-custom-border-200 " />
<h4 className="font-medium text-sm">URL</h4>
<input
type="url"
placeholder="Enter URL"
className="border h-10 border-neutral-200 py-1 px-2 w-full rounded-s text-sm mt-2"
/>
{/* WebHook toggle */}
<div className="flex gap-6 mt-6">
<h4 className="text-sm"> Enable webhook</h4>
<ToggleSwitch value={isWebhookEnabled} onChange={(value) => setWebhookEnabled(value)} />
</div>
<h4 className="mt-8 text-sm font-medium">Which event do you like to trigger this webhook</h4>
{/* Radio */}
<input
type="radio"
name="whatToSend"
value="everything"
defaultChecked={true}
className="w-3.5 mt-4"
onChange={(value) => setIndividualEventsSelected(value.target.value == "individual")}
/>
<label className="text-xs"> Send everything </label>
<br />
<input
type="radio"
name="whatToSend"
value="individual"
defaultChecked={false}
onChange={(value) => setIndividualEventsSelected(value.target.value == "individual")}
className="w-3.5 mt-5 mb-2"
/>
<label className="text-xs"> Select Individual events </label>
{/* Grid */}
{isIndividualEventsSelected && (
<div className="bg-custom-background-80 rounded-md grid grid-cols-2 p-7 gap-2 max-w-md mt-6 ">
<div>
<input type="checkbox" name="selectIndividualEvents" /> <label htmlFor="1"> Project </label>{" "}
</div>
<div>
<input type="checkbox" name="selectIndividualEvents" /> <label htmlFor="2"> Cycle </label>{" "}
</div>
<div>
<input type="checkbox" name="selectIndividualEvents" /> <label htmlFor="3"> Issues </label>
</div>
<div>
<input type="checkbox" name="selectIndividualEvents" /> <label htmlFor="4"> Modules </label>
</div>
<div>
<input type="checkbox" name="selectIndividualEvents" /> <label htmlFor="5"> IssueComment </label>
</div>
</div>
)}
<h4 className="text-sm font-medium mt-3.5"> Secret Key </h4>
<p className="text-sm text-neutral-400 mt-2.5"> Genarate a token to sign-in the webhook payload </p>
</div>
<div className="pl-32 ">
{showGenaratedKey && (
<div className="flex gap-5">
<div className="h-10 w-full border rounded-md shadow-sm mt-3.5">
<div className="flex justify-between items-center h-full">
<div className="border-dotted w-[60%] h-0 border-t-8 ml-4 border-neutral-300 "></div>
<div className="flex justify-end space-x-2 mr-2">
<img className="h-5 w-5" src="" />
<img className="h-5 w-5" src="" />
</div>
</div>
</div>
<Button className="mt-2.5 ">
<RefreshCw className={`h-3 w-3 ${isRegenarateKeyLoading ? "animate-spin" : ""}`} />
Re-genarate Key
</Button>
</div>
)}
<Button className="mt-2.5"> Genarate Key </Button>
</div>
</>
);
};
@@ -1,19 +0,0 @@
import React from "react";
import { ToggleSwitch } from "@plane/ui";
export const WebhooksListItem = () => {
return (
<div>
{/* <div class="flex items-center py-3.5 border-b border-custom-border-200"></div> */}
<div className="flex justify-between px-3.5 py-[18px]">
<div>
<h4 className="text-base font-medium">Webhook Name</h4>
<h5 className="text-base text-neutral-700">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
</h5>
</div>
<ToggleSwitch value={true} onChange={(value) => console.log("change")} />
</div>
</div>
);
};
@@ -1,25 +0,0 @@
import React from "react";
import { Button } from "@plane/ui";
import { WebhooksListItem } from "./webhooks-list-item";
export const WebhookLists = () => {
return (
<div className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium">Webhooks</h4>
<Button variant="primary" size="sm">
Add webhook
</Button>
</div>
{/* List */}
<div className="divide-y divide-custom-border-200">
<WebhooksListItem />
<WebhooksListItem />
<WebhooksListItem />
<WebhooksListItem />
<WebhooksListItem />
<WebhooksListItem />
</div>
</div>
);
};
+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>
</>
);
});
+12 -3
View File
@@ -111,11 +111,20 @@ export const getFirstCharacters = (str: string) => {
*/
export const stripHTML = (html: string) => {
const tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
const strippedText = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ""); // Remove script tags
return strippedText.replace(/<[^>]*>/g, ""); // Remove all other HTML tags
};
/**
*
* @example:
* const html = "<p>Some text</p>";
* const text = stripAndTruncateHTML(html);
* console.log(text); // Some text
*/
export const stripAndTruncateHTML = (html: string, length: number = 55) => truncateText(stripHTML(html), length);
/**
* @description: This function return number count in string if number is more than 100 then it will return 99+
* @param {number} number
@@ -26,10 +26,6 @@ export const WorkspaceSettingsSidebar = () => {
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
{
label: "Webhooks",
href: `/${workspaceSlug}/settings/webhooks`,
},
{
label: "Imports",
href: `/${workspaceSlug}/settings/imports`,
@@ -1,22 +0,0 @@
import React from "react";
import type { NextPage } from "next";
import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingHeader } from "components/headers";
import { WorkspaceSettingLayout } from "layouts/setting-layout";
import { EmptyWebhooks, WebhookLists, WebhookDetails } from "components/web-hooks";
const Webhooks: NextPage = () => {
return (
<AppLayout header={<WorkspaceSettingHeader title="Webhook Settings" />}>
<WorkspaceSettingLayout>
<section className="pr-9 py-8 w-full overflow-y-auto">
{/* <EmptyWebhooks /> */}
{/* <WebhookLists /> */}
<WebhookDetails />
</section>
</WorkspaceSettingLayout>
</AppLayout>
);
};
export default Webhooks;
+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;
});
}
}