Compare commits

..

1 Commits

Author SHA1 Message Date
Muhamade Sabith .T.U 6174833dc2 feat/webhooks 2023-10-27 19:54:21 +05:30
20 changed files with 257 additions and 164 deletions
+3 -2
View File
@@ -579,6 +579,7 @@ class CycleIssueViewSet(BaseViewSet):
)
)
total_issues = issues.count()
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
@@ -590,12 +591,12 @@ class CycleIssueViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(
issues_data, status=status.HTTP_200_OK
{"data": issues_data, "total_issues": total_issues}, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id, cycle_id):
+9 -6
View File
@@ -217,6 +217,7 @@ 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
@@ -231,12 +232,12 @@ class IssueViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
)
@@ -425,6 +426,7 @@ 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
@@ -439,12 +441,12 @@ class UserWorkSpaceIssues(BaseAPIView):
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
)
@@ -2149,6 +2151,7 @@ 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
@@ -2156,12 +2159,12 @@ class IssueDraftViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues, group_by)
return Response(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id):
+3 -2
View File
@@ -364,6 +364,7 @@ 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:
@@ -375,12 +376,12 @@ class ModuleIssueViewSet(BaseViewSet):
if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(
issues_data, status=status.HTTP_200_OK
{"data": issues_data, "total_issues": total_issues}, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id, module_id):
+14 -5
View File
@@ -93,6 +93,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
filters = issue_filters(request.query_params, "GET")
@@ -116,7 +117,9 @@ 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")
@@ -126,7 +129,9 @@ 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(
@@ -178,6 +183,8 @@ 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
@@ -188,15 +195,17 @@ 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(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(issues, status=status.HTTP_200_OK)
return Response(
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
)
class IssueViewViewSet(BaseViewSet):
+3 -2
View File
@@ -1223,6 +1223,7 @@ 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
@@ -1230,12 +1231,12 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
if group_by:
grouped_results = group_results(issues, group_by)
return Response(
grouped_results,
{"data": grouped_results, "total_issues": total_issues},
status=status.HTTP_200_OK,
)
return Response(
issues, status=status.HTTP_200_OK
{"data": issues, "total_issues": total_issues}, status=status.HTTP_200_OK
)
+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: "",
@@ -11,7 +11,7 @@ import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
import { ArchiveRestore, Clock, MessageSquare, User2 } from "lucide-react";
// helper
import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
import { stripHTML, replaceUnderscoreIfSnakeCase, truncateText } 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"
) : notification.data.issue_activity.field === "description" ? (
stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
) : (
notification.data.issue_activity.new_value
stripHTML(notification.data.issue_activity.new_value)
)
) : (
<span>
+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 -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">
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,4 @@
export * from "./empty-webhooks";
export * from "./webhooks-list";
export * from "./webhooks-list-item";
export * from "./webhook-details";
@@ -0,0 +1,99 @@
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>
</>
);
};
@@ -0,0 +1,19 @@
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>
);
};
@@ -0,0 +1,25 @@
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>
);
};
+3 -12
View File
@@ -111,20 +111,11 @@ export const getFirstCharacters = (str: string) => {
*/
export const stripHTML = (html: string) => {
const strippedText = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ""); // Remove script tags
return strippedText.replace(/<[^>]*>/g, ""); // Remove all other HTML tags
const tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
};
/**
*
* @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,6 +26,10 @@ export const WorkspaceSettingsSidebar = () => {
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
{
label: "Webhooks",
href: `/${workspaceSlug}/settings/webhooks`,
},
{
label: "Imports",
href: `/${workspaceSlug}/settings/imports`,
@@ -0,0 +1,22 @@
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;
-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;
});
}
}