Skip to content

Commit

Permalink
Merge pull request #49 from aws-educate-tw/SCRUM-189-fix-error-of-not…
Browse files Browse the repository at this point in the history
…-showing-trigger_webhook-api

Scrum 189 fix error of not showing trigger webhook api
  • Loading branch information
chungchihhan authored Dec 17, 2024
2 parents aeebec0 + 12f5d7a commit 5be0f9c
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 41 deletions.
119 changes: 85 additions & 34 deletions src/app/ui/create-webhook-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { submitWebhookForm } from "@/lib/actions";
import HelpTip from "@/app/ui/help-tip";
import SelectDropdown from "@/app/ui/select-dropdown";
import AttachDropdown from "./attach-dropdown";
import WebhookTypeDropdown from "@/app/ui/webhook-type-dropdown";
import FileUpload from "@/app/ui/file-upload";
import IframePreview from "@/app/ui/iframe-preview";
import EmailInput from "@/app/ui/email-input";
Expand All @@ -12,12 +13,12 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { Toast } from "flowbite-react";
import { HiCheck, HiClipboard } from "react-icons/hi";
import { GoAlert } from "react-icons/go";

interface SubmitResponse {
status: string;
message: string;
webhook_id?: string;
webhook_url?: string;
data?: { webhook_id: string; webhook_url: string };
errors?: { path: string; message: string }[];
}

Expand All @@ -38,8 +39,10 @@ export default function CreateWebhookForm() {
const [attachment_file_ids, setAttachment_file_ids] = useState<string[]>([]);
const [isGenerateCertificate, setIsGenerateCertificate] = useState<boolean>(false);
const [showSuccessToast, setShowSuccessToast] = useState(false);
const [showCopyToast, setShowCopyToast] = useState(false);
const [showFailedToast, setShowFailedToast] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [webhookUrl, setWebhookUrl] = useState("");
const [webhookType, setWebhookType] = useState<string>("surveycake");

const router = useRouter();

Expand All @@ -58,8 +61,12 @@ export default function CreateWebhookForm() {

const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setErrors({});
setIsSubmitting(true);

if (!ref.current) return;

// Extract form data
const formData = {
subject: (ref.current.querySelector("[id='subject']") as HTMLInputElement).value,
display_name: (ref.current.querySelector("[id='display_name']") as HTMLInputElement).value,
Expand All @@ -75,34 +82,58 @@ export default function CreateWebhookForm() {
bcc: bccEmails,
cc: ccEmails,
is_generate_certificate: isGenerateCertificate,
webhook_type: webhookType,
};

setIsSubmitting(true);
try {
const response: SubmitResponse = await submitWebhookForm(
const response = await submitWebhookForm(
JSON.stringify(formData),
localStorage.getItem("access_token") ?? ""
localStorage.getItem("access_token") || ""
);

if (response.status === "error" && response.errors) {
handleResponse(response);
} catch (error: any) {
console.error("Unexpected error:", error);
showToast("error", "Unexpected error occurred. Please try again.");
} finally {
setIsSubmitting(false);
}
};

// Helper function to handle the response
const handleResponse = (response: SubmitResponse) => {
if (response.status === "error") {
if (response.errors) {
// Handle validation errors
const newErrors: { [key: string]: string } = {};
response.errors.forEach(err => {
newErrors[err.path] = err.message;
});
setErrors(newErrors);
setIsSubmitting(false);
return;
showToast("error", "Validation failed. Please fix the errors.");
} else {
// Handle general API errors
showToast("error", response.message || "Failed to create webhook.");
}
return;
}

// Success case
setWebhookUrl(response.data?.webhook_url || "");
setIsCopied(false);
showToast("success", "Webhook created successfully!");
};

setWebhookUrl(response.webhook_url || "");
// Helper function for showing toast notifications
const showToast = (status: "success" | "error", message: string) => {
if (status === "success") {
setShowSuccessToast(true);
setTimeout(() => setShowSuccessToast(false), 10000);
setErrors({});
ref.current.reset();
} catch (error: any) {
alert("Failed to create webhook: " + error.message);
} else {
setShowFailedToast(true);
setTimeout(() => setShowFailedToast(false), 3000);
}
setIsSubmitting(false);
console.log(`[${status.toUpperCase()}] ${message}`);
};

const handleHtmlSelect = (file_id: string | null, file_url: string | null) => {
Expand Down Expand Up @@ -135,12 +166,14 @@ export default function CreateWebhookForm() {
setShowAttachUpload(false);
};

// 複製功能
const handleWebhookTypeChange = (webhook_type: string) => {
setWebhookType(webhook_type);
};

const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setShowCopyToast(true); // 顯示複製成功提示
setTimeout(() => setShowCopyToast(false), 3000); // 3秒後關閉
setIsCopied(true);
} catch (err) {
console.error("Failed to copy:", err);
}
Expand Down Expand Up @@ -283,6 +316,7 @@ export default function CreateWebhookForm() {
value="no"
onChange={() => setIsGenerateCertificate(false)}
name="inline-radio-group"
defaultChecked
className="w-4 h-4 text-blue-600 bg-white border-gray-300 focus:ring-blue-500"
/>
<label htmlFor="inline-2-radio" className="ms-2 text-sm font-medium text-gray-900">
Expand Down Expand Up @@ -448,6 +482,15 @@ export default function CreateWebhookForm() {
/>
{errors.iv_key && <p className="text-red-500 text-sm">{errors.iv_key}</p>}
</div>
<div className="m-3">
<label className="mb-2 flex text-sm font-medium gap-2">
Webhook Type:
<HelpTip message="Select the webhook type.">
<Info size={16} color="gray" />
</HelpTip>
</label>
<WebhookTypeDropdown onSelect={handleWebhookTypeChange} />
</div>
</div>

<div className="w-full flex justify-end my-3 gap-3">
Expand Down Expand Up @@ -525,13 +568,22 @@ export default function CreateWebhookForm() {
{/* 建立成功的 Toast */}
{showSuccessToast && (
<div className="fixed bottom-4 right-4 z-50">
<Toast>
<div className="flex items-start gap-4">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-500">
<HiCheck className="h-5 w-5" />
<Toast
className={`${isCopied ? "bg-blue-400" : "bg-green-400"} drop-shadow-lg transition-all`}
>
<div className="flex flex-col items-start gap-4">
<div className="flex items-center gap-2">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white">
<HiCheck className="h-5 w-5" />
</div>
{isCopied ? (
<p className="font-medium text-white">URL copied to clipboard !</p>
) : (
<p className="font-medium text-white">Webhook created successfully !</p>
)}
{/* <p className="font-medium text-white">Webhook created successfully!</p> */}
</div>
<div className="flex flex-col gap-2">
<p className="font-semibold">Webhook created successfully!</p>
<div className="flex items-center gap-2 bg-gray-100 p-2 rounded">
<p className="text-sm break-all">{webhookUrl}</p>
<button
Expand All @@ -546,17 +598,16 @@ export default function CreateWebhookForm() {
</Toast>
</div>
)}

{/* 複製成功的 Toast */}
{showCopyToast && (
<div className="fixed bottom-20 right-4 z-50">
<Toast>
<div className="flex items-start gap-4">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500">
<HiCheck className="h-5 w-5" />
</div>
<div>
<p>URL copied to clipboard!</p>
{showFailedToast && (
<div className="fixed bottom-4 right-4 z-50">
<Toast className="bg-red-500 drop-shadow-lg transition-all">
<div className="flex flex-col items-start gap-4">
<div className="flex items-center gap-2">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-white">
<GoAlert className="h-5 w-5" />
</div>
<p className="font-medium text-white">Failed to create the Webhook Url !</p>
{/* <p className="font-medium text-white">Webhook created successfully!</p> */}
</div>
</div>
</Toast>
Expand Down
86 changes: 86 additions & 0 deletions src/app/ui/webhook-type-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client";
import React, { useState, useRef, useEffect } from "react";

interface WebhookTypeDropdownProps {
onSelect: (type: string) => void;
}

export default function WebhookTypeDropdown({ onSelect }: WebhookTypeDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedType, setSelectedType] = useState<string>("surveycake");
const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

const toggleDropdown = () => {
setIsOpen(!isOpen);
};

const handleSelect = (type: string) => {
setSelectedType(type);
onSelect(type); // Pass the selected type to the parent component
setIsOpen(false);
};

return (
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
<div>
<button
type="button"
className="inline-flex justify-between w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggleDropdown}
aria-expanded={isOpen}
aria-haspopup="true"
>
{selectedType === "surveycake" ? "SurveyCake" : "Slack"}
<svg
className="-mr-1 ml-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 011.414 1.414l-4 4a1 1 01-1.414 0l-4-4a1 1 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>

{isOpen && (
<div
className="absolute z-50 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
>
<div>
<div
className="hover:bg-gray-200 cursor-pointer active:bg-gray-300 py-2 px-4 text-sm"
onClick={() => handleSelect("surveycake")}
>
SurveyCake
</div>
<div
className="hover:bg-gray-200 cursor-pointer active:bg-gray-300 py-2 px-4 text-sm"
onClick={() => handleSelect("slack")}
>
Slack
</div>
</div>
</div>
)}
</div>
);
}
29 changes: 22 additions & 7 deletions src/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const webhookFormSchema = z.object({
hash_key: z.string().min(1, "Hash key is required"),
iv_key: z.string().min(1, "IV key is required"),
webhook_name: z.string().min(1, "Webhook name is required"),
webhook_type: z.string().min(1, "Webhook type is required"),
});

export async function submitForm(data: string, access_token: string) {
Expand Down Expand Up @@ -200,6 +201,7 @@ export async function submitWebhookForm(data: string, access_token: string) {

try {
console.log("data", validation.data);

const base_url = process.env.NEXT_PUBLIC_API_ENDPOINT;
const url = new URL(`${base_url}/webhook`);
const response = await fetch(url.toString(), {
Expand All @@ -211,19 +213,32 @@ export async function submitWebhookForm(data: string, access_token: string) {
body: JSON.stringify(validation.data),
});

// Deal with the error response
if (!response.ok) {
const errorResponse = await response.json();
console.error("API responded with an error:", errorResponse);
return {
status: "error",
message: errorResponse.message || "Failed to create webhook.",
};
}

// Deal with the success response
const result = await response.json();
return {
status: result.status,
message: result.message,
webhook_id: result.webhook_id,
webhook_url: result.webhook_url,
errors: result.errors,
status: "success",
message: result.message || "Webhook created successfully.",
data: {
webhook_id: result.webhook_id,
webhook_url: result.webhook_url,
},
};
} catch (error: any) {
console.error("Error during API call:", error);
return {
status: "error",
message: "Error: Failed to create webhook. Please try again.",
error: error.message,
message: "An unexpected error occurred while creating the webhook.",
debugInfo: error.message,
};
}
}
Expand Down

0 comments on commit 5be0f9c

Please sign in to comment.