Redesign the frontend with a dark-tech theme: add Tailwind + PostCSS, design tokens, and shadcn-style primitives (Button/Card/Input/Dialog/ DropdownMenu/Tabs/ScrollArea/etc.); restyle the app shell, navigation, sidebar tree, content view, markdown rendering, editors, modals and settings panels. Behavior/props unchanged; Font Awesome replaced with lucide-react. Add the patch cards feature UI: patch-queries hooks and a PatchCards component rendered below the markdown body, with an Add Patch button and create/edit dialog. Fix tree expandability: folders with an index page now expand on name click (and navigate), and the chevron+folder icon is one larger toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
412 lines
16 KiB
JavaScript
412 lines
16 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import {
|
|
useWebhookSetting,
|
|
useCreateWebhookSetting,
|
|
useUpdateWebhookSetting,
|
|
useWebhooks,
|
|
} from "../../../utils/queries/webhook-queries";
|
|
|
|
import {
|
|
useCreateWebhook,
|
|
useUpdateWebhook,
|
|
useDeleteWebhook,
|
|
} from "../../../utils/queries/webhook-queries";
|
|
import {useUpdatePathSetting} from "../../../utils/queries/path-setting-queries";
|
|
import { Plus, Save, Pencil, Trash2, Check } from "lucide-react";
|
|
import { Button } from "../../ui/button";
|
|
import { Input, Label } from "../../ui/input";
|
|
import { Spinner } from "../../ui/misc";
|
|
|
|
const SELECT_CLASS =
|
|
"flex h-9 w-full rounded-md border border-input bg-background/60 px-3 py-1 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring/40";
|
|
|
|
const CheckboxRow = ({ checked, onChange, label }) => (
|
|
<label className="flex cursor-pointer items-center gap-2 text-sm text-foreground">
|
|
<input
|
|
type="checkbox"
|
|
className="h-4 w-4 accent-primary"
|
|
checked={checked}
|
|
onChange={onChange}
|
|
/>
|
|
{label}
|
|
</label>
|
|
);
|
|
|
|
const WebhookSettingPanel = ({pathSetting, onClose}) => {
|
|
|
|
const {data: setting} = useWebhookSetting(pathSetting?.webhook_setting_id || 0);
|
|
const {data: webhooks, isLoading: isWebhooksLoading} = useWebhooks();
|
|
|
|
const createWebhookSetting = useCreateWebhookSetting();
|
|
const updateWebhookSetting = useUpdateWebhookSetting();
|
|
|
|
const createWebhook = useCreateWebhook();
|
|
const updateWebhook = useUpdateWebhook();
|
|
const deleteWebhook = useDeleteWebhook();
|
|
|
|
const updatePathSetting = useUpdatePathSetting();
|
|
|
|
|
|
const [enabled, setEnabled] = useState(false);
|
|
const [isRecursive, setIsRecursive] = useState(false);
|
|
const [onEvents, setOnEvents] = useState(0);
|
|
const [selectedUrl, setSelectedUrl] = useState("");
|
|
const [headerList, setHeaderList] = useState([]);
|
|
const [additionalHeaders, setAdditionalHeaders] = useState({});
|
|
|
|
const [isOnMarkdownCreated, setIsOnMarkdownCreated] = useState(false);
|
|
const [isOnMarkdownUpdated, setIsOnMarkdownUpdated] = useState(false);
|
|
const [isOnMarkdownDeleted, setIsOnMarkdownDeleted] = useState(false);
|
|
const [isOnPathCreated, setIsOnPathCreated] = useState(false);
|
|
const [isOnPathUpdated, setIsOnPathUpdated] = useState(false);
|
|
const [isOnPathDeleted, setIsOnPathDeleted] = useState(false);
|
|
|
|
const assignFromOnEvents = (bits) => {
|
|
setIsOnMarkdownCreated(!!(bits & 1));
|
|
setIsOnMarkdownUpdated(!!(bits & 2));
|
|
setIsOnMarkdownDeleted(!!(bits & 4));
|
|
setIsOnPathCreated(!!(bits & 8));
|
|
setIsOnPathUpdated(!!(bits & 16));
|
|
setIsOnPathDeleted(!!(bits & 32));
|
|
};
|
|
|
|
const handleTriggerEventsUpdate = (eventType, isChecked) => {
|
|
setOnEvents((prev) => {
|
|
let nextVal = prev;
|
|
switch (eventType) {
|
|
case "MARKDOWN_CREATED":
|
|
nextVal = isChecked ? nextVal | 1 : nextVal & ~1;
|
|
setIsOnMarkdownCreated(isChecked);
|
|
break;
|
|
case "MARKDOWN_UPDATED":
|
|
nextVal = isChecked ? nextVal | 2 : nextVal & ~2;
|
|
setIsOnMarkdownUpdated(isChecked);
|
|
break;
|
|
case "MARKDOWN_DELETED":
|
|
nextVal = isChecked ? nextVal | 4 : nextVal & ~4;
|
|
setIsOnMarkdownDeleted(isChecked);
|
|
break;
|
|
case "PATH_CREATED":
|
|
nextVal = isChecked ? nextVal | 8 : nextVal & ~8;
|
|
setIsOnPathCreated(isChecked);
|
|
break;
|
|
case "PATH_UPDATED":
|
|
nextVal = isChecked ? nextVal | 16 : nextVal & ~16;
|
|
setIsOnPathUpdated(isChecked);
|
|
break;
|
|
case "PATH_DELETED":
|
|
nextVal = isChecked ? nextVal | 32 : nextVal & ~32;
|
|
setIsOnPathUpdated(isChecked);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return nextVal;
|
|
});
|
|
};
|
|
|
|
const handleCreateWebhookSetting = () => {
|
|
createWebhookSetting.mutate({}, {
|
|
onSuccess: (data) => {
|
|
updatePathSetting.mutate({
|
|
id: pathSetting.id,
|
|
data: {
|
|
webhook_setting_id: data.id
|
|
}
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (setting && webhooks) {
|
|
setEnabled(setting.enabled);
|
|
setIsRecursive(setting.recursive);
|
|
setOnEvents(setting.on_events);
|
|
assignFromOnEvents(setting.on_events);
|
|
|
|
try {
|
|
const headers = setting.additional_header
|
|
? JSON.parse(setting.additional_header)
|
|
: {};
|
|
setAdditionalHeaders(headers);
|
|
setHeaderList(
|
|
Object.entries(headers).map(([k, v]) => ({ key: k, value: v }))
|
|
);
|
|
} catch (err) {
|
|
setAdditionalHeaders({});
|
|
setHeaderList([]);
|
|
}
|
|
|
|
const found = webhooks.find((wh) => wh.id === setting.webhook_id);
|
|
setSelectedUrl(found ? found.hook_url : "");
|
|
} else {
|
|
setEnabled(false);
|
|
setIsRecursive(false);
|
|
setOnEvents(0);
|
|
assignFromOnEvents(0);
|
|
setAdditionalHeaders({});
|
|
setHeaderList([]);
|
|
setSelectedUrl("");
|
|
}
|
|
}, [setting, webhooks]);
|
|
|
|
const handleAddHeader = () => {
|
|
setHeaderList([...headerList, { key: "", value: "" }]);
|
|
};
|
|
const handleHeaderChange = (index, field, val) => {
|
|
const updated = [...headerList];
|
|
updated[index][field] = val;
|
|
setHeaderList(updated);
|
|
};
|
|
const handleApplyHeaders = () => {
|
|
const out = {};
|
|
headerList.forEach(({ key, value }) => {
|
|
if (key.trim()) out[key] = value;
|
|
});
|
|
setAdditionalHeaders(out);
|
|
};
|
|
|
|
const handleCreateWebhook = () => {
|
|
const newUrl = prompt("Enter the new webhook URL");
|
|
if (!newUrl) return;
|
|
createWebhook.mutate(newUrl, {
|
|
onSuccess: () => alert("Created new Webhook successfully"),
|
|
onError: () => alert("Failed to create new Webhook"),
|
|
});
|
|
};
|
|
const handleUpdateWebhook = () => {
|
|
if (!setting || !setting.webhook_id) {
|
|
alert("No webhook selected. Must pick from dropdown first.");
|
|
return;
|
|
}
|
|
const newUrl = prompt("Enter updated Webhook URL", selectedUrl);
|
|
if (!newUrl) return;
|
|
updateWebhook.mutate(
|
|
{ id: setting.webhook_id, data: { hook_url: newUrl } },
|
|
{
|
|
onSuccess: () => alert("Updated Webhook successfully"),
|
|
onError: () => alert("Failed to update Webhook"),
|
|
}
|
|
);
|
|
};
|
|
const handleDeleteWebhook = () => {
|
|
if (!setting || !setting.webhook_id) {
|
|
alert("No webhook selected to delete");
|
|
return;
|
|
}
|
|
if (!window.confirm("Are you sure?")) return;
|
|
deleteWebhook.mutate(setting.webhook_id, {
|
|
onSuccess: () => alert("Deleted Webhook successfully"),
|
|
onError: () => alert("Failed to delete Webhook"),
|
|
});
|
|
};
|
|
|
|
const handleSaveWebhookSetting = async () => {
|
|
const hook = webhooks.find((wh) => wh.hook_url === selectedUrl);
|
|
const payload = {
|
|
webhook_id: hook? hook.id : null,
|
|
recursive: isRecursive,
|
|
additional_header: JSON.stringify(additionalHeaders),
|
|
enabled,
|
|
on_events: onEvents,
|
|
|
|
};
|
|
if(!setting || !setting.id){
|
|
createWebhookSetting.mutate(payload, {
|
|
onSuccess: (res) => {
|
|
updatePathSetting.mutate({id: pathSetting.id, data: {webhook_setting_id: res.id}},{
|
|
onSuccess: () => alert("Webhook setting successfully created"),
|
|
onError: () => alert("Failed to save Webhook"),
|
|
})
|
|
},
|
|
onError: () => alert("Failed to save Webhook"),
|
|
});
|
|
} else {
|
|
updateWebhookSetting.mutate({id: setting.id, data: payload}, {
|
|
onSuccess: () => alert("Updated Webhook successfully"),
|
|
onError: () => alert("Failed to update Webhook"),
|
|
});
|
|
}
|
|
onClose();
|
|
};
|
|
|
|
return setting ? (
|
|
<div className="mt-4 space-y-5 rounded-lg border border-border bg-surface/40 p-5">
|
|
<h4 className="font-mono text-sm font-semibold uppercase tracking-wide text-foreground">
|
|
Webhook Setting
|
|
</h4>
|
|
<div className="space-y-2">
|
|
<Label>Select or Create a Webhook</Label>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
{isWebhooksLoading ? (
|
|
<Spinner label="Loading webhooks" />
|
|
) : (
|
|
<select
|
|
className={SELECT_CLASS}
|
|
value={selectedUrl}
|
|
onChange={(e) => setSelectedUrl(e.target.value)}
|
|
>
|
|
<option value="">(none)</option>
|
|
{webhooks.map((hook) => (
|
|
<option key={hook.id} value={hook.hook_url}>
|
|
{hook.hook_url}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
onClick={handleCreateWebhook}
|
|
>
|
|
<Plus className="h-4 w-4" /> Add
|
|
</Button>
|
|
</div>
|
|
{setting?.webhook_id && (
|
|
<div className="flex flex-wrap gap-2 pt-1">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleUpdateWebhook}
|
|
>
|
|
<Pencil className="h-4 w-4" /> Update Webhook URL
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={handleDeleteWebhook}
|
|
>
|
|
<Trash2 className="h-4 w-4" /> Delete Webhook
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<CheckboxRow
|
|
checked={enabled}
|
|
onChange={(e) => setEnabled(e.target.checked)}
|
|
label="Enabled"
|
|
/>
|
|
|
|
<div className="space-y-2">
|
|
<Label>On Events</Label>
|
|
<div className="grid grid-cols-1 gap-3 rounded-md border border-border bg-background/40 p-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<CheckboxRow
|
|
checked={isOnMarkdownCreated}
|
|
onChange={(e) =>
|
|
handleTriggerEventsUpdate("MARKDOWN_CREATED", e.target.checked)
|
|
}
|
|
label="Markdown Created"
|
|
/>
|
|
<CheckboxRow
|
|
checked={isOnMarkdownUpdated}
|
|
onChange={(e) =>
|
|
handleTriggerEventsUpdate("MARKDOWN_UPDATED", e.target.checked)
|
|
}
|
|
label="Markdown Updated"
|
|
/>
|
|
<CheckboxRow
|
|
checked={isOnMarkdownDeleted}
|
|
onChange={(e) =>
|
|
handleTriggerEventsUpdate("MARKDOWN_DELETED", e.target.checked)
|
|
}
|
|
label="Markdown Deleted"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<CheckboxRow
|
|
checked={isOnPathCreated}
|
|
onChange={(e) =>
|
|
handleTriggerEventsUpdate("PATH_CREATED", e.target.checked)
|
|
}
|
|
label="Path Created"
|
|
/>
|
|
<CheckboxRow
|
|
checked={isOnPathUpdated}
|
|
onChange={(e) =>
|
|
handleTriggerEventsUpdate("PATH_UPDATED", e.target.checked)
|
|
}
|
|
label="Path Updated"
|
|
/>
|
|
<CheckboxRow
|
|
checked={isOnPathDeleted}
|
|
onChange={(e) =>
|
|
handleTriggerEventsUpdate("PATH_DELETED", e.target.checked)
|
|
}
|
|
label="Path Deleted"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<CheckboxRow
|
|
checked={isRecursive}
|
|
onChange={(e) => setIsRecursive(e.target.checked)}
|
|
label="Recursive"
|
|
/>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Additional Headers</Label>
|
|
<div className="space-y-3 rounded-md border border-border bg-background/40 p-4">
|
|
{headerList.map((h, idx) => (
|
|
<div className="flex gap-2" key={idx}>
|
|
<Input
|
|
type="text"
|
|
placeholder="key"
|
|
value={h.key}
|
|
onChange={(e) => handleHeaderChange(idx, "key", e.target.value)}
|
|
/>
|
|
<Input
|
|
type="text"
|
|
placeholder="value"
|
|
value={h.value}
|
|
onChange={(e) =>
|
|
handleHeaderChange(idx, "value", e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
))}
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleAddHeader}
|
|
>
|
|
<Plus className="h-4 w-4" /> Header
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
onClick={handleApplyHeaders}
|
|
>
|
|
<Check className="h-4 w-4" /> Apply
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
onClick={handleSaveWebhookSetting}
|
|
>
|
|
<Save className="h-4 w-4" /> Save Webhook Setting
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<Button
|
|
type="button"
|
|
onClick={handleCreateWebhookSetting}
|
|
>
|
|
<Plus className="h-4 w-4" /> Create Webhook Setting
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
|
|
export default WebhookSettingPanel |