Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0f912ab4 | ||
|
|
912851659c | ||
|
|
7edaa49c21 | ||
|
|
8cc61bc427 | ||
|
|
85e53af66c | ||
|
|
6f751fd0a4 | ||
|
|
67c0b73260 |
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user