Compare commits

...

2 Commits

Author SHA1 Message Date
2c330904e4 add: webhook 2025-03-17 13:54:53 +00:00
8abf54eade fix: table boarder not shown in md 2025-03-05 18:29:13 +00:00
8 changed files with 584 additions and 22 deletions

View File

@@ -32,7 +32,6 @@ const Footer = () => {
<a href="https://git.hangman-lab.top/hzhang/HangmanLab">git</a>
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;
v0.0.10
</span>
)}</p>
{
@@ -79,7 +78,6 @@ const Footer = () => {
)
}
</div>
</footer>
);
};

View File

@@ -42,9 +42,8 @@ const MarkdownEditor = () => {
const hasPermission = roles.includes("admin") || roles.includes("creator");
if (!hasPermission) {
if (!hasPermission)
return <div className="notification is-danger">Permission Denied</div>;
}
if(isLoading)
return <p>Loading...</p>;

View File

@@ -93,3 +93,20 @@ pre {
color: #888;
}
.markdown-preview table {
width: 100%;
border-collapse: collapse;
border: 1px solid #ddd;
}
.markdown-preview th,
.markdown-preview td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.markdown-preview th {
background-color: #f4f4f4;
font-weight: bold;
}

View File

@@ -0,0 +1,363 @@
import React, { useState, useEffect } from "react";
import {
useCreateWebhookSetting,
useUpdateWebhookSetting,
useWebhookSettingByPathId,
useWebhooks,
useCreateWebhook,
useUpdateWebhook,
useDeleteWebhook
} from "../../utils/webhook-queries";
const PathSettingModal = ({ pathId, isOpen, onClose }) => {
const { data: setting } = useWebhookSettingByPathId(pathId);
const { data: webhooks } = useWebhooks();
const createWebhookSetting = useCreateWebhookSetting();
const updateWebhookSetting = useUpdateWebhookSetting();
const createWebhook = useCreateWebhook();
const updateWebhook = useUpdateWebhook();
const deleteWebhook = useDeleteWebhook();
const [url, setUrl] = useState("");
const [enabled, setEnabled] = useState(false);
const [webhookId, setWebhookId] = useState(-1);
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 [triggerEvents, setTriggerEvents] = useState(0);
const [isRecursive, setIsRecursive] = useState(false);
const [additionalHeaders, setAdditionalHeaders] = useState({});
const [headerList, setHeaderList] = useState([]);
const handleTriggerEventsUpdate = (eventType, isChecked) => {
setTriggerEvents((prevEvents) => {
let newEvents = prevEvents;
switch (eventType) {
case "isOnMarkdownCreated":
newEvents = isChecked ? (newEvents | 1) : (newEvents & ~1);
break;
case "isOnMarkdownUpdated":
newEvents = isChecked ? (newEvents | 2) : (newEvents & ~2);
break;
case "isOnMarkdownDeleted":
newEvents = isChecked ? (newEvents | 4) : (newEvents & ~4);
break;
case "isOnPathCreated":
newEvents = isChecked ? (newEvents | 8) : (newEvents & ~8);
break;
case "isOnPathUpdated":
newEvents = isChecked ? (newEvents | 16) : (newEvents & ~16);
break;
case "isOnPathDeleted":
newEvents = isChecked ? (newEvents | 32) : (newEvents & ~32);
break;
default:
break;
}
return newEvents;
});
};
const assignFromTriggerEvents = (events) => {
setIsOnMarkdownCreated((events & 1) > 0);
setIsOnMarkdownUpdated((events & 2) > 0);
setIsOnMarkdownDeleted((events & 4) > 0);
setIsOnPathCreated((events & 8) > 0);
setIsOnPathUpdated((events & 16) > 0);
setIsOnPathDeleted((events & 32) > 0);
};
useEffect(() => {
if (setting && webhooks) {
setEnabled(setting.enabled);
setWebhookId(setting.webhook_id || -1);
const selectedWebhook = webhooks?.find(hook => hook.id === setting.webhook_id);
if (selectedWebhook) {
setUrl(selectedWebhook.hook_url);
}
setTriggerEvents(setting.on_events);
assignFromTriggerEvents(setting.on_events);
setIsRecursive(setting.recursive);
try{
const headers = setting.additional_header ?
JSON.parse(setting.additional_header) : {};
setAdditionalHeaders(headers);
setHeaderList(Object.entries(headers).map(([key, value]) => ({key, value })));
} catch(err) {
setAdditionalHeaders({});
setHeaderList([]);
}
} else {
setUrl("");
setEnabled(false);
setWebhookId(-1);
setTriggerEvents(0);
assignFromTriggerEvents(0);
setIsRecursive(false);
setAdditionalHeaders({});
setHeaderList([]);
}
}, [setting, webhooks]);
const handleSave = () => {
const payload = {
path_id: pathId,
webhook_id: webhookId,
enabled,
on_events: triggerEvents,
recursive: isRecursive,
additional_header: JSON.stringify(additionalHeaders),
};
console.log(payload);
if (!setting) {
createWebhookSetting.mutate(payload, {
onSuccess: () => alert("Webhook setting created successfully"),
onError: () => alert("Webhook setting creation failed"),
});
} else {
updateWebhookSetting.mutate({ id: setting.id, data: payload }, {
onSuccess: () => alert("Webhook setting updated successfully"),
onError: () => alert("Webhook setting update failed"),
});
}
onClose();
};
const handleCreateWebhook = () => {
const newUrl = prompt("Enter new webhook URL");
if (newUrl) {
createWebhook.mutate(newUrl, {
onSuccess: () => alert("Webhook created successfully"),
onError: () => alert("Webhook creation failed"),
});
}
};
const handleUpdateWebhook = () => {
const newUrl = prompt("Enter new webhook URL", url);
if (newUrl && webhookId !== -1) {
updateWebhook.mutate({ id: webhookId, data: { hook_url: newUrl } }, {
onSuccess: () => alert("Webhook updated successfully"),
onError: () => alert("Webhook update failed"),
});
}
};
const handleDeleteWebhook = () => {
if (webhookId !== -1 && window.confirm("Are you sure you want to delete this webhook?")) {
deleteWebhook.mutate(webhookId, {
onSuccess: () => alert("Webhook deleted successfully"),
onError: () => alert("Webhook deletion failed"),
});
}
};
const handleApplyHeaders = () => {
const newHeaders = {};
headerList.forEach(({ key, value }) => {
if (key.trim()) newHeaders[key] = value;
});
setAdditionalHeaders(newHeaders);
};
const handleHeaderChange = (index, field, value) => {
const updatedHeaders = [...headerList];
updatedHeaders[index][field] = value;
setHeaderList(updatedHeaders);
};
const handleAddHeader = () => {
setHeaderList([...headerList, { key: "", value: "" }])
};
return (
<div className={`modal ${isOpen ? "is-active" : ""}`}>
<div className="modal-background"></div>
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Webhook Settings</p>
<button className="delete" aria-label="close" onClick={onClose}></button>
</header>
<section className="modal-card-body">
<form>
<div className="field">
<label className="label">Webhook URL</label>
<div className="field has-addons">
<div className="control is-expanded">
<div className="select is-fullwidth">
<select
value={url}
onChange={(e) => {
setUrl(e.target.value);
const selectedWebhook = webhooks?.find(h => h.hook_url === e.target.value);
setWebhookId(selectedWebhook?.id || -1);
}}
>
<option value="">Select a webhook</option>
{webhooks?.map(hook => (
<option key={hook.id} value={hook.hook_url}>{hook.hook_url}</option>
))}
</select>
</div>
</div>
<div className="control">
<button type="button" className="button is-primary" onClick={handleCreateWebhook}>Add</button>
</div>
</div>
{webhookId !== -1 && (
<div className="buttons">
<button type="button" className="button is-info" onClick={handleUpdateWebhook}>Update Webhook URL</button>
<button type="button" className="button is-danger" onClick={handleDeleteWebhook}>Delete Webhook</button>
</div>
)}
<div className="field">
<label className="checkbox">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>
Enabled
</label>
</div>
<div className="field">
<label className="label">On Events</label>
<div className="box">
<div className="columns">
<div className="column">
<label className="checkbox">
<input
type="checkbox"
checked={isOnMarkdownCreated}
onChange={(e) => {
setIsOnMarkdownCreated(e.target.checked);
handleTriggerEventsUpdate("isOnMarkdownCreated", e.target.checked);
}}
/>
Markdown Created
</label>
<br />
<label className="checkbox">
<input
type="checkbox"
checked={isOnMarkdownUpdated}
onChange={(e) => {
setIsOnMarkdownUpdated(e.target.checked);
handleTriggerEventsUpdate("isOnMarkdownUpdated", e.target.checked);
}}
/>
Markdown Updated
</label>
<br />
<label className="checkbox">
<input
type="checkbox"
checked={isOnMarkdownDeleted}
onChange={(e) => {
setIsOnMarkdownDeleted(e.target.checked);
handleTriggerEventsUpdate("isOnMarkdownDeleted", e.target.checked);
}}
/>
Markdown Deleted
</label>
</div>
<div className="column">
<label className="checkbox">
<input
type="checkbox"
checked={isOnPathCreated}
onChange={(e) => {
setIsOnPathCreated(e.target.checked);
handleTriggerEventsUpdate("isOnPathCreated", e.target.checked);
}}
/>
Path Created
</label>
<br />
<label className="checkbox">
<input
type="checkbox"
checked={isOnPathUpdated}
onChange={(e) => {
setIsOnPathUpdated(e.target.checked);
handleTriggerEventsUpdate("isOnPathUpdated", e.target.checked);
}}
/>
Path Updated
</label>
<br />
<label className="checkbox">
<input
type="checkbox"
checked={isOnPathDeleted}
onChange={(e) => {
setIsOnPathDeleted(e.target.checked);
handleTriggerEventsUpdate("isOnPathDeleted", e.target.checked);
}}
/>
Path Deleted
</label>
</div>
</div>
</div>
</div>
<div className="field">
<label className="checkbox">
<input
type="checkbox"
checked={isRecursive}
onChange={(e) => setIsRecursive(e.target.checked)}
/>
Recursive
</label>
</div>
<div className="field">
<label className="checkbox">Additional Headers</label>
<div className="box">
{headerList.map((header, index) => (
<div className="columns" key={index}>
<div className="column">
<input
type="text"
className="input"
placeholder="key"
value={header.key}
onChange={(e) => {handleHeaderChange(index, "key", e.target.value)}}
/>
</div>
<div className="column">
<input
type="text"
className="input"
placeholder="value"
value={header.value}
onChange={(e) => handleHeaderChange(index, "value", e.target.value)}
/>
</div>
</div>
))}
<button type="button" className="button is-small is-info" onClick={handleAddHeader}>Add Header</button>
<button type="button" className="button is-small is-success" onClick={handleApplyHeaders}>Apply</button>
</div>
</div>
</div>
</form>
</section>
<footer className="modal-card-foot">
<button className="button is-success" type="button" onClick={handleSave}>Save</button>
<button className="button" onClick={onClose}>Cancel</button>
</footer>
</div>
</div>
);
};
export default PathSettingModal;

View File

@@ -2,8 +2,8 @@ import React, {useState} from "react";
import { Link } from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import "./PathNode.css";
import {useDeletePath, useMovePath, usePath, usePaths, useUpdatePath} from "../../utils/path-queries";
import {useIndexMarkdown, useMarkdownsByPath, useMoveMarkdown} from "../../utils/markdown-queries";
import {useDeletePath, useMovePath, useUpdatePath} from "../../utils/path-queries";
import {useIndexMarkdown, useMoveMarkdown} from "../../utils/markdown-queries";
import MarkdownNode from "./MarkdownNode";
const PathNode = ({ path, isRoot = false }) => {
@@ -11,12 +11,10 @@ const PathNode = ({ path, isRoot = false }) => {
const [isEditing, setIsEditing] = useState(false);
const [newName, setNewName] = useState(path.name);
// const { data: childPaths, isLoading: isChildLoading, error: childError } = usePaths(path.id);
// const { data: markdowns, isLoading: isMarkdownLoading, error: markdownError } = useMarkdownsByPath(path.id);
const deletePath = useDeletePath();
const updatePath = useUpdatePath();
const {data: indexMarkdown, isLoading: isIndexLoading, error: indexMarkdownError} = useIndexMarkdown(path.id);
const {data: indexMarkdown} = useIndexMarkdown(path.id);
const movePath = useMovePath();
const moveMarkdown = useMoveMarkdown();
@@ -33,7 +31,7 @@ const PathNode = ({ path, isRoot = false }) => {
const handleSave = () => {
console.log(`handleSave ${path.id}`);
updatePath.mutate({id: path.id, data: {name: newName}}, {
onsuccess: () => setIsEditing(false),
onSuccess: () => setIsEditing(false),
onError: err => alert("failed to update this path"),
})
};

View File

@@ -5,6 +5,8 @@ import { useQueryClient } from "react-query";
import "./PathManager.css";
import {fetch_} from "../utils/request-utils";
import {ConfigContext} from "../ConfigProvider";
import PathSettingModal from "./Modals/PathSettingModal";
const PathManager = ({ currentPathId = 1, onPathChange }) => {
const [currentPath, setCurrentPath] = useState([{ name: "Root", id: 1 }]);
@@ -17,6 +19,11 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
const createPath = useCreatePath();
const config = useContext(ConfigContext).config;
const [isPathSettingModalOpen, setIsPathSettingModalModalOpen] = useState(false);
const handleSettingClick = () => {
setIsPathSettingModalModalOpen(true);
}
const buildPath = async (pathId) => {
const path = [];
@@ -104,8 +111,8 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
return (
<div className="path-manager">
<div className="path-manager-header">
<div className="current-path">
<div className="path-manager-header field has-addons">
<div className="current-path control">
{currentPath.map((path, index) => (
<span
key={path.id}
@@ -117,6 +124,23 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
</span>
))}
</div>
<div className="control">
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
</div>
<div className="control">
<button
className="button is-small is-primary"
type="button"
onClick={handleSettingClick}
>
Settings
</button>
<PathSettingModal
isOpen={isPathSettingModalOpen}
pathId={currentPathId}
onClose={() => setIsPathSettingModalModalOpen(false)}
/>
</div>
</div>
<div className="path-manager-body">
@@ -144,6 +168,7 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
Create "{searchTerm}"
</button>
</div>
</div>
{dropdownActive && (
<div className="dropdown is-active">

View File

@@ -21,23 +21,16 @@ export const usePaths = (parent_id) => {
}
}
);
}
};
export const usePath = (id) => {
const config = useConfig();
const queryClient = useQueryClient();
const cachedData = queryClient.getQueryData(["path", id]);
return useQuery(
["path", id],
() => fetch_(`${config.BACKEND_HOST}/api/path/${id}`),
{
enabled: !!id,
onSuccess: (data) => {
console.log(`path ${id} - ${cachedData}` );
}
enabled: !!id
}
);
};
@@ -53,7 +46,6 @@ export const useCreatePath = () => {
}),
{
onSuccess: (res, variables) => {
console.log(JSON.stringify(variables));
queryClient.invalidateQueries(["paths", variables.parent_id]);
queryClient.invalidateQueries("tree");
},

View File

@@ -0,0 +1,170 @@
import {fetch_ } from "./request-utils"
import {useConfig} from "../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
export const useWebhooks = () =>{
const queryClient = useQueryClient();
const config = useConfig();
return useQuery(
"webhooks",
() => fetch_(`${config.BACKEND_HOST}/api/webhook/`),
{
onSuccess: (data) => {
if(data)
queryClient.setQueryData("webhooks", data);
}
}
);
};
export const useCreateWebhook = () =>{
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/webhook/`, {
method: "POST",
body: JSON.stringify({
"hook_url": data
}),
}),
{
onSuccess: () => {
queryClient.invalidateQueries("webhooks");
}
}
);
};
export const useUpdateWebhook = () =>{
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
method: "PATCH",
body: JSON.stringify(data)
}),
{
onSuccess: () => {
queryClient.invalidateQueries("webhooks");
}
}
);
};
export const useDeleteWebhook = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
method: "DELETE",
}),
{
onSuccess: () => {
queryClient.invalidateQueries("webhooks");
}
}
)
}
export const useWebhookSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"webhook_setting",
() => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/`),
{
onSuccess: (data) => {
if(data){
for(const setting of data){
queryClient.setQueryData(["webhook_setting", setting.id], setting);
queryClient.setQueryData(["webhook_setting_path_id", setting.path_id], setting);
}
}
}
}
);
};
export const useWebhookSetting = (setting_id) => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
["webhook_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/${setting_id}`),
{
enabled: !!setting_id,
onSuccess: (res) => {
if(res)
queryClient.setQueryData(["webhook_setting_path_id", res.path_id], res);
}
});
};
export const useWebhookSettingByPathId = (pathId) => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
["webhook_setting_path_id", pathId],
() => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/path/${pathId}`),
{
enabled: !!pathId,
onSuccess: (res) => {
if(res)
queryClient.setQueryData(["webhook_setting", res.id], res);
}
});
};
export const useCreateWebhookSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/`, {
method: "POST",
body: JSON.stringify(data)
}),{
onSuccess: (res) => {
queryClient.invalidateQueries(["webhook_setting", res.id]);
queryClient.invalidateQueries(["webhook_setting_path_id", res.path_id]);
queryClient.invalidateQueries("webhook_setting");
}
}
);
};
export const useUpdateWebhookSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/${id}`, {
method: "PATCH",
body: JSON.stringify(data)
}),{
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["webhook_setting", variables.id]);
queryClient.invalidateQueries(["webhook_setting_path_id", variables.path_id]);
queryClient.invalidateQueries("webhook_setting");
}
}
);
};
export const useDeleteWebhookSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/${id}`, {
method: "DELETE",
}),
{
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["webhook_setting", variables.id]);
queryClient.invalidateQueries(["webhook_setting_path_id", variables.path_id]);
queryClient.invalidateQueries("webhook_setting");
}
}
);
};