mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-30 00:53:14 +00:00
@@ -2870,6 +2870,9 @@ class CreateTool(Resource):
|
||||
"config": fields.Raw(
|
||||
required=True, description="Configuration of the tool"
|
||||
),
|
||||
"customName": fields.String(
|
||||
required=False, description="Custom name for the tool"
|
||||
),
|
||||
"actions": fields.List(
|
||||
fields.Raw,
|
||||
required=True,
|
||||
@@ -2916,6 +2919,7 @@ class CreateTool(Resource):
|
||||
"name": data["name"],
|
||||
"displayName": data["displayName"],
|
||||
"description": data["description"],
|
||||
"customName": data.get("customName", ""),
|
||||
"actions": transformed_actions,
|
||||
"config": data["config"],
|
||||
"status": data["status"],
|
||||
@@ -2937,6 +2941,7 @@ class UpdateTool(Resource):
|
||||
"id": fields.String(required=True, description="Tool ID"),
|
||||
"name": fields.String(description="Name of the tool"),
|
||||
"displayName": fields.String(description="Display name for the tool"),
|
||||
"customName": fields.String(description="Custom name for the tool"),
|
||||
"description": fields.String(description="Tool description"),
|
||||
"config": fields.Raw(description="Configuration of the tool"),
|
||||
"actions": fields.List(
|
||||
@@ -2963,6 +2968,8 @@ class UpdateTool(Resource):
|
||||
update_data["name"] = data["name"]
|
||||
if "displayName" in data:
|
||||
update_data["displayName"] = data["displayName"]
|
||||
if "customName" in data:
|
||||
update_data["customName"] = data["customName"]
|
||||
if "description" in data:
|
||||
update_data["description"] = data["description"]
|
||||
if "actions" in data:
|
||||
|
||||
@@ -36,6 +36,8 @@ const Input = ({
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const hasValue = value !== undefined && value !== null && value !== '';
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<input
|
||||
@@ -58,7 +60,7 @@ const Input = ({
|
||||
{placeholder && (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`absolute -top-2.5 left-3 px-2 ${textSizeStyles[textSize]} transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400 ${labelBgClassName}`}
|
||||
className={`absolute select-none ${hasValue ? '-top-2.5 left-3 text-xs' : ''} px-2 transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:text-gray-400 ${labelBgClassName}`}
|
||||
>
|
||||
{placeholder}
|
||||
{required && (
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function ToolsPopup({
|
||||
/>
|
||||
<div className="overflow-hidden">
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-xs font-medium text-gray-900 dark:text-white">
|
||||
{tool.displayName}
|
||||
{tool.customName || tool.displayName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,31 @@ body.dark {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Thin scrollbar utility */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
||||
@@ -117,9 +117,15 @@
|
||||
"noToolsFound": "No tools found",
|
||||
"selectToolSetup": "Select a tool to set up",
|
||||
"settingsIconAlt": "Settings icon",
|
||||
"configureToolAria": "Configure {toolName}",
|
||||
"toggleToolAria": "Toggle {toolName}",
|
||||
"manageTools": "Go to Tools"
|
||||
"configureToolAria": "Configure {{toolName}}",
|
||||
"toggleToolAria": "Toggle {{toolName}}",
|
||||
"manageTools": "Go to Tools",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"deleteWarning": "Are you sure you want to delete the tool \"{{toolName}}\" ?",
|
||||
"unsavedChanges": "You have unsaved changes that will be lost if you leave without saving.",
|
||||
"leaveWithoutSaving": "Leave without Saving",
|
||||
"saveAndLeave": "Save and Leave"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -47,9 +47,6 @@ export default function AddActionModal({
|
||||
New Action
|
||||
</h2>
|
||||
<div className="relative mt-6 px-3">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
|
||||
Action Name
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={actionName}
|
||||
@@ -60,7 +57,8 @@ export default function AddActionModal({
|
||||
}}
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
placeholder={'Enter name'}
|
||||
placeholder="Action Name"
|
||||
required={true}
|
||||
/>
|
||||
<p
|
||||
className={`ml-1 mt-2 text-xs italic ${
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function ConfigToolModal({
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const [authKey, setAuthKey] = React.useState<string>('');
|
||||
const [customName, setCustomName] = React.useState<string>('');
|
||||
|
||||
const handleAddTool = (tool: AvailableToolType) => {
|
||||
userService
|
||||
@@ -34,6 +35,7 @@ export default function ConfigToolModal({
|
||||
displayName: tool.displayName,
|
||||
description: tool.description,
|
||||
config: { token: authKey },
|
||||
customName: customName,
|
||||
actions: tool.actions,
|
||||
status: true,
|
||||
},
|
||||
@@ -58,6 +60,16 @@ export default function ConfigToolModal({
|
||||
{t('modals.configTool.type')}:{' '}
|
||||
<span className="font-semibold">{tool?.name}</span>
|
||||
</p>
|
||||
<div className="mt-6 px-3">
|
||||
<Input
|
||||
type="text"
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter custom name (optional)"
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 px-3">
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
@@ -32,12 +32,7 @@ export default function ConfirmationModal({
|
||||
return (
|
||||
<>
|
||||
{modalState === 'ACTIVE' && (
|
||||
<WrapperModal
|
||||
close={() => {
|
||||
setModalState('INACTIVE');
|
||||
handleCancel && handleCancel();
|
||||
}}
|
||||
>
|
||||
<WrapperModal close={() => setModalState('INACTIVE')}>
|
||||
<div className="relative">
|
||||
<div>
|
||||
<p className="font-base mb-1 w-[90%] break-words text-lg text-jet dark:text-bright-gray">
|
||||
|
||||
@@ -10,10 +10,12 @@ import Dropdown from '../components/Dropdown';
|
||||
import Input from '../components/Input';
|
||||
import ToggleSwitch from '../components/ToggleSwitch';
|
||||
import AddActionModal from '../modals/AddActionModal';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { APIActionType, APIToolType, UserToolType } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { areObjectsEqual } from '../utils/objectUtils';
|
||||
|
||||
export default function ToolConfig({
|
||||
tool,
|
||||
@@ -28,9 +30,40 @@ export default function ToolConfig({
|
||||
const [authKey, setAuthKey] = React.useState<string>(
|
||||
'token' in tool.config ? tool.config.token : '',
|
||||
);
|
||||
const [customName, setCustomName] = React.useState<string>(
|
||||
tool.customName || '',
|
||||
);
|
||||
const [actionModalState, setActionModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
const [initialState, setInitialState] = React.useState({
|
||||
customName: tool.customName || '',
|
||||
authKey: 'token' in tool.config ? tool.config.token : '',
|
||||
config: tool.config,
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
});
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false);
|
||||
const [showUnsavedModal, setShowUnsavedModal] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (hasUnsavedChanges) {
|
||||
setShowUnsavedModal(true);
|
||||
} else {
|
||||
handleGoBack();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const currentState = {
|
||||
customName,
|
||||
authKey,
|
||||
config: tool.config,
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
};
|
||||
|
||||
setHasUnsavedChanges(!areObjectsEqual(initialState, currentState));
|
||||
}, [customName, authKey, tool]);
|
||||
|
||||
const handleCheckboxChange = (actionIndex: number, property: string) => {
|
||||
setTool({
|
||||
...tool,
|
||||
@@ -66,6 +99,7 @@ export default function ToolConfig({
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
displayName: tool.displayName,
|
||||
customName: customName,
|
||||
description: tool.description,
|
||||
config: tool.name === 'api_tool' ? tool.config : { token: authKey },
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
@@ -74,6 +108,14 @@ export default function ToolConfig({
|
||||
token,
|
||||
)
|
||||
.then(() => {
|
||||
// Update initialState to match current state
|
||||
setInitialState({
|
||||
customName,
|
||||
authKey,
|
||||
config: tool.config,
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
});
|
||||
setHasUnsavedChanges(false);
|
||||
handleGoBack();
|
||||
});
|
||||
};
|
||||
@@ -114,23 +156,38 @@ export default function ToolConfig({
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="mt-8 flex flex-col gap-4">
|
||||
<div className="mb-4 flex items-center gap-3 text-sm text-eerie-black dark:text-bright-gray">
|
||||
<div className="scrollbar-thin mt-8 flex flex-col gap-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-sm text-eerie-black dark:text-bright-gray">
|
||||
<button
|
||||
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
onClick={handleBackClick}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</button>
|
||||
<p className="mt-px">Back to all tools</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
onClick={handleGoBack}
|
||||
className="text-nowrap rounded-full bg-purple-30 px-3 py-2 text-xs text-white hover:bg-violets-are-blue sm:px-4 sm:py-2"
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
Save
|
||||
</button>
|
||||
<p className="mt-px">Back to all tools</p>
|
||||
</div>
|
||||
<div>
|
||||
{/* Custom name section */}
|
||||
<div className="mt-1">
|
||||
<p className="text-sm font-semibold text-eerie-black dark:text-bright-gray">
|
||||
Type
|
||||
</p>
|
||||
<p className="mt-1 font-sans text-base font-normal text-eerie-black dark:text-bright-gray">
|
||||
{tool.name}
|
||||
Custom Name
|
||||
</p>
|
||||
<div className="relative mt-4 w-full max-w-96">
|
||||
<Input
|
||||
type="text"
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter a custom name (optional)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{Object.keys(tool?.config).length !== 0 && tool.name !== 'api_tool' && (
|
||||
@@ -141,7 +198,7 @@ export default function ToolConfig({
|
||||
<div className="mt-4 flex flex-col items-start gap-2 sm:flex-row sm:items-center">
|
||||
{Object.keys(tool?.config).length !== 0 &&
|
||||
tool.name !== 'api_tool' && (
|
||||
<div className="relative w-96">
|
||||
<div className="relative w-full max-w-96">
|
||||
<Input
|
||||
type="text"
|
||||
value={authKey}
|
||||
@@ -151,20 +208,6 @@ export default function ToolConfig({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="text-nowrap rounded-full bg-purple-30 px-5 py-[10px] text-sm text-white hover:bg-violets-are-blue"
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
<button
|
||||
className="text-nowrap rounded-full border border-solid border-red-500 px-5 py-[10px] text-sm text-red-500 hover:bg-red-500 hover:text-white"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -173,17 +216,19 @@ export default function ToolConfig({
|
||||
<p className="text-base font-semibold text-eerie-black dark:text-bright-gray">
|
||||
Actions
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActionModalState('ACTIVE');
|
||||
}}
|
||||
className="rounded-full border border-solid border-violets-are-blue px-5 py-1 text-sm text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
|
||||
>
|
||||
Add action
|
||||
</button>
|
||||
</div>
|
||||
{tool.name === 'api_tool' ? (
|
||||
<APIToolConfig tool={tool as APIToolType} setTool={setTool} />
|
||||
<>
|
||||
<APIToolConfig tool={tool as APIToolType} setTool={setTool} />
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={() => setActionModalState('ACTIVE')}
|
||||
className="rounded-full border border-solid border-violets-are-blue px-5 py-1 text-sm text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
|
||||
>
|
||||
Add action
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-12">
|
||||
{'actions' in tool &&
|
||||
@@ -214,10 +259,10 @@ export default function ToolConfig({
|
||||
id={`actionToggle-${actionIndex}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative mt-5">
|
||||
<div className="relative mt-5 w-full px-5">
|
||||
<Input
|
||||
type="text"
|
||||
className="ml-5 w-[97%]"
|
||||
className="w-full"
|
||||
placeholder="Enter description"
|
||||
value={action.description}
|
||||
onChange={(e) => {
|
||||
@@ -368,6 +413,49 @@ export default function ToolConfig({
|
||||
setModalState={setActionModalState}
|
||||
handleSubmit={handleAddNewAction}
|
||||
/>
|
||||
{showUnsavedModal && (
|
||||
<ConfirmationModal
|
||||
message={t('settings.tools.unsavedChanges', {
|
||||
defaultValue:
|
||||
'You have unsaved changes that will be lost if you leave without saving.',
|
||||
})}
|
||||
modalState="ACTIVE"
|
||||
setModalState={(state) => setShowUnsavedModal(state === 'ACTIVE')}
|
||||
submitLabel={t('settings.tools.saveAndLeave', {
|
||||
defaultValue: 'Save and Leave',
|
||||
})}
|
||||
handleSubmit={() => {
|
||||
userService
|
||||
.updateTool(
|
||||
{
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
displayName: tool.displayName,
|
||||
customName: customName,
|
||||
description: tool.description,
|
||||
config:
|
||||
tool.name === 'api_tool'
|
||||
? tool.config
|
||||
: { token: authKey },
|
||||
actions: 'actions' in tool ? tool.actions : [],
|
||||
status: tool.status,
|
||||
},
|
||||
token,
|
||||
)
|
||||
.then(() => {
|
||||
setShowUnsavedModal(false);
|
||||
handleGoBack();
|
||||
});
|
||||
}}
|
||||
cancelLabel={t('settings.tools.leaveWithoutSaving', {
|
||||
defaultValue: 'Leave without Saving',
|
||||
})}
|
||||
handleCancel={() => {
|
||||
setShowUnsavedModal(false);
|
||||
handleGoBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -381,6 +469,34 @@ function APIToolConfig({
|
||||
setTool: (tool: APIToolType) => void;
|
||||
}) {
|
||||
const [apiTool, setApiTool] = React.useState<APIToolType>(tool);
|
||||
const { t } = useTranslation();
|
||||
const [actionToDelete, setActionToDelete] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
|
||||
const handleDeleteActionClick = (actionName: string) => {
|
||||
setActionToDelete(actionName);
|
||||
setDeleteModalState('ACTIVE');
|
||||
};
|
||||
const handleConfirmedDelete = () => {
|
||||
if (actionToDelete) {
|
||||
setApiTool((prevApiTool) => {
|
||||
const { [actionToDelete]: deletedAction, ...remainingActions } =
|
||||
prevApiTool.config.actions;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: remainingActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
setActionToDelete(null);
|
||||
setDeleteModalState('INACTIVE');
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionChange = (
|
||||
actionName: string,
|
||||
@@ -417,19 +533,31 @@ function APIToolConfig({
|
||||
setTool(apiTool);
|
||||
}, [apiTool]);
|
||||
return (
|
||||
<div className="flex flex-col gap-16">
|
||||
<div className="scrollbar-thin flex flex-col gap-16">
|
||||
{/* Actions list */}
|
||||
{apiTool.config.actions &&
|
||||
Object.entries(apiTool.config.actions).map(
|
||||
([actionName, action], actionIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={actionIndex}
|
||||
className="w-full rounded-xl border border-silver dark:border-silver/40"
|
||||
>
|
||||
<div className="flex h-10 flex-wrap items-center justify-between rounded-t-xl border-b border-silver bg-[#F9F9F9] px-5 dark:border-silver/40 dark:bg-[#28292D]">
|
||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||
{action.name}
|
||||
</p>
|
||||
([actionName, action], actionIndex) => (
|
||||
<div
|
||||
key={actionIndex}
|
||||
className="w-full rounded-xl border border-silver dark:border-silver/40"
|
||||
>
|
||||
<div className="flex h-10 flex-wrap items-center justify-between rounded-t-xl border-b border-silver bg-[#F9F9F9] px-5 dark:border-silver/40 dark:bg-[#28292D]">
|
||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||
{action.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleDeleteActionClick(actionName)}
|
||||
className="mr-2 flex h-6 w-6 items-center justify-center rounded-full"
|
||||
title={t('convTile.delete')}
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="delete"
|
||||
className="h-4 w-4 opacity-40 transition-opacity hover:opacity-100"
|
||||
/>
|
||||
</button>
|
||||
<ToggleSwitch
|
||||
checked={action.active}
|
||||
onChange={() => handleActionToggle(actionName)}
|
||||
@@ -437,117 +565,136 @@ function APIToolConfig({
|
||||
id={`actionToggle-${actionIndex}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 px-5">
|
||||
<div className="relative w-full">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
|
||||
URL
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={action.url}
|
||||
onChange={(e) => {
|
||||
setApiTool((prevApiTool) => {
|
||||
const updatedActions = {
|
||||
...prevApiTool.config.actions,
|
||||
};
|
||||
const updatedAction = {
|
||||
...updatedActions[actionName],
|
||||
};
|
||||
updatedAction.url = e.target.value;
|
||||
updatedActions[actionName] = updatedAction;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: updatedActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter url"
|
||||
></Input>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 px-5">
|
||||
<div className="relative w-full">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
|
||||
URL
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={action.url}
|
||||
onChange={(e) => {
|
||||
setApiTool((prevApiTool) => {
|
||||
const updatedActions = {
|
||||
...prevApiTool.config.actions,
|
||||
};
|
||||
const updatedAction = {
|
||||
...updatedActions[actionName],
|
||||
};
|
||||
updatedAction.url = e.target.value;
|
||||
updatedActions[actionName] = updatedAction;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: updatedActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter url"
|
||||
></Input>
|
||||
</div>
|
||||
<div className="mt-4 px-5 py-2">
|
||||
<div className="relative w-full">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
|
||||
Method
|
||||
</span>
|
||||
<Dropdown
|
||||
options={['GET', 'POST', 'PUT', 'DELETE']}
|
||||
selectedValue={action.method}
|
||||
onSelect={(value: string) => {
|
||||
setApiTool((prevApiTool) => {
|
||||
const updatedActions = {
|
||||
...prevApiTool.config.actions,
|
||||
};
|
||||
const updatedAction = {
|
||||
...updatedActions[actionName],
|
||||
};
|
||||
updatedAction.method = value as
|
||||
| 'GET'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE';
|
||||
updatedActions[actionName] = updatedAction;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: updatedActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
size="w-56"
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 px-5 py-2">
|
||||
<div className="relative w-full">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
|
||||
Description
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={action.description}
|
||||
onChange={(e) => {
|
||||
setApiTool((prevApiTool) => {
|
||||
const updatedActions = {
|
||||
...prevApiTool.config.actions,
|
||||
};
|
||||
const updatedAction = {
|
||||
...updatedActions[actionName],
|
||||
};
|
||||
updatedAction.description = e.target.value;
|
||||
updatedActions[actionName] = updatedAction;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: updatedActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter description"
|
||||
></Input>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 px-5 py-2">
|
||||
<APIActionTable
|
||||
apiAction={action}
|
||||
handleActionChange={handleActionChange}
|
||||
</div>
|
||||
<div className="mt-4 px-5 py-2">
|
||||
<div className="relative w-full">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
|
||||
Method
|
||||
</span>
|
||||
<Dropdown
|
||||
options={['GET', 'POST', 'PUT', 'DELETE']}
|
||||
selectedValue={action.method}
|
||||
onSelect={(value: string) => {
|
||||
setApiTool((prevApiTool) => {
|
||||
const updatedActions = {
|
||||
...prevApiTool.config.actions,
|
||||
};
|
||||
const updatedAction = {
|
||||
...updatedActions[actionName],
|
||||
};
|
||||
updatedAction.method = value as
|
||||
| 'GET'
|
||||
| 'POST'
|
||||
| 'PUT'
|
||||
| 'DELETE';
|
||||
updatedActions[actionName] = updatedAction;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: updatedActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
size="w-56"
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
<div className="mt-4 px-5 py-2">
|
||||
<div className="relative w-full">
|
||||
<span className="absolute -top-2 left-5 z-10 bg-white px-2 text-xs text-gray-4000 dark:bg-raisin-black dark:text-silver">
|
||||
Description
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={action.description}
|
||||
onChange={(e) => {
|
||||
setApiTool((prevApiTool) => {
|
||||
const updatedActions = {
|
||||
...prevApiTool.config.actions,
|
||||
};
|
||||
const updatedAction = {
|
||||
...updatedActions[actionName],
|
||||
};
|
||||
updatedAction.description = e.target.value;
|
||||
updatedActions[actionName] = updatedAction;
|
||||
return {
|
||||
...prevApiTool,
|
||||
config: {
|
||||
...prevApiTool.config,
|
||||
actions: updatedActions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter description"
|
||||
></Input>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 px-5 py-2">
|
||||
<APIActionTable
|
||||
apiAction={action}
|
||||
handleActionChange={handleActionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{deleteModalState === 'ACTIVE' && actionToDelete && (
|
||||
<ConfirmationModal
|
||||
message={t('settings.tools.deleteActionWarning', {
|
||||
name: actionToDelete,
|
||||
defaultValue: `Are you sure you want to delete the action "${actionToDelete}"?`,
|
||||
})}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={handleConfirmedDelete}
|
||||
handleCancel={() => {
|
||||
setDeleteModalState('INACTIVE');
|
||||
setActionToDelete(null);
|
||||
}}
|
||||
submitLabel={t('convTile.delete')}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -876,7 +1023,7 @@ function APIActionTable({
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="scrollbar-thin flex flex-col gap-6">
|
||||
<div>
|
||||
<h3 className="mb-1 text-base font-normal text-eerie-black dark:text-bright-gray">
|
||||
Headers
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import CogwheelIcon from '../assets/cogwheel.svg';
|
||||
import ThreeDotsIcon from '../assets/three-dots.svg';
|
||||
import NoFilesIcon from '../assets/no-files.svg';
|
||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||
import Input from '../components/Input';
|
||||
@@ -15,6 +15,10 @@ import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import ToolConfig from './ToolConfig';
|
||||
import { APIToolType, UserToolType } from './types';
|
||||
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
|
||||
export default function Tools() {
|
||||
const { t } = useTranslation();
|
||||
@@ -29,6 +33,57 @@ export default function Tools() {
|
||||
UserToolType | APIToolType | null
|
||||
>(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [activeMenuId, setActiveMenuId] = React.useState<string | null>(null);
|
||||
const menuRefs = React.useRef<{
|
||||
[key: string]: React.RefObject<HTMLDivElement>;
|
||||
}>({});
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
const [toolToDelete, setToolToDelete] = React.useState<UserToolType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
userTools.forEach((tool) => {
|
||||
if (!menuRefs.current[tool.id]) {
|
||||
menuRefs.current[tool.id] = React.createRef();
|
||||
}
|
||||
});
|
||||
}, [userTools]);
|
||||
|
||||
const handleDeleteTool = (tool: UserToolType) => {
|
||||
setToolToDelete(tool);
|
||||
setDeleteModalState('ACTIVE');
|
||||
};
|
||||
|
||||
const confirmDeleteTool = () => {
|
||||
if (toolToDelete) {
|
||||
userService.deleteTool({ id: toolToDelete.id }, token).then(() => {
|
||||
getUserTools();
|
||||
setDeleteModalState('INACTIVE');
|
||||
setToolToDelete(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getMenuOptions = (tool: UserToolType): MenuOption[] => [
|
||||
{
|
||||
icon: Edit,
|
||||
label: t('settings.tools.edit'),
|
||||
onClick: () => handleSettingsClick(tool),
|
||||
variant: 'primary',
|
||||
iconWidth: 14,
|
||||
iconHeight: 14,
|
||||
},
|
||||
{
|
||||
icon: Trash,
|
||||
label: t('settings.tools.delete'),
|
||||
onClick: () => handleDeleteTool(tool),
|
||||
variant: 'danger',
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
},
|
||||
];
|
||||
|
||||
const getUserTools = () => {
|
||||
setLoading(true);
|
||||
@@ -150,28 +205,41 @@ export default function Tools() {
|
||||
) : (
|
||||
userTools
|
||||
.filter((tool) =>
|
||||
tool.displayName
|
||||
(tool.customName || tool.displayName)
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
.map((tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative flex h-52 w-[300px] flex-col justify-between rounded-2xl border border-light-gainsboro bg-white-3000 p-6 dark:border-arsenic dark:bg-transparent"
|
||||
className="relative flex h-52 w-[300px] flex-col justify-between rounded-2xl bg-[#F5F5F5] p-6 hover:bg-[#ECECEC] dark:bg-[#383838] dark:hover:bg-[#303030]"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSettingsClick(tool)}
|
||||
aria-label={t('settings.tools.configureToolAria', {
|
||||
toolName: tool.displayName,
|
||||
})}
|
||||
className="absolute right-4 top-4"
|
||||
<div
|
||||
ref={menuRefs.current[tool.id]}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveMenuId(
|
||||
activeMenuId === tool.id ? null : tool.id,
|
||||
);
|
||||
}}
|
||||
className="absolute right-4 top-4 z-10 cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={CogwheelIcon}
|
||||
src={ThreeDotsIcon}
|
||||
alt={t('settings.tools.settingsIconAlt')}
|
||||
className="h-[19px] w-[19px]"
|
||||
/>
|
||||
</button>
|
||||
<ContextMenu
|
||||
isOpen={activeMenuId === tool.id}
|
||||
setIsOpen={(isOpen) => {
|
||||
setActiveMenuId(isOpen ? tool.id : null);
|
||||
}}
|
||||
options={getMenuOptions(tool)}
|
||||
anchorRef={menuRefs.current[tool.id]}
|
||||
position="top-right"
|
||||
offset={{ x: 0, y: 0 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center px-1">
|
||||
<img
|
||||
@@ -182,10 +250,10 @@ export default function Tools() {
|
||||
</div>
|
||||
<div className="mt-[9px]">
|
||||
<p
|
||||
title={tool.displayName}
|
||||
title={tool.customName || tool.displayName}
|
||||
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-raisin-black-light dark:text-bright-gray"
|
||||
>
|
||||
{tool.displayName}
|
||||
{tool.customName || tool.displayName}
|
||||
</p>
|
||||
<p className="mt-1 h-24 overflow-auto px-1 text-[12px] leading-relaxed text-old-silver dark:text-sonic-silver-light">
|
||||
{tool.description}
|
||||
@@ -201,7 +269,7 @@ export default function Tools() {
|
||||
size="small"
|
||||
id={`toolToggle-${index}`}
|
||||
ariaLabel={t('settings.tools.toggleToolAria', {
|
||||
toolName: tool.displayName,
|
||||
toolName: tool.customName || tool.displayName,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -218,6 +286,17 @@ export default function Tools() {
|
||||
getUserTools={getUserTools}
|
||||
onToolAdded={handleToolAdded}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
message={t('settings.tools.deleteWarning', {
|
||||
toolName:
|
||||
toolToDelete?.customName || toolToDelete?.displayName || '',
|
||||
})}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={confirmDeleteTool}
|
||||
submitLabel={t('settings.tools.delete')}
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ export type UserToolType = {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
customName?: string;
|
||||
description: string;
|
||||
status: boolean;
|
||||
config: {
|
||||
@@ -81,6 +82,7 @@ export type APIToolType = {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
customName?: string;
|
||||
description: string;
|
||||
status: boolean;
|
||||
config: { actions: { [key: string]: APIActionType } };
|
||||
|
||||
29
frontend/src/utils/objectUtils.ts
Normal file
29
frontend/src/utils/objectUtils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Deeply compares two objects for equality
|
||||
* @param obj1 First object to compare
|
||||
* @param obj2 Second object to compare
|
||||
* @returns boolean indicating if objects are equal
|
||||
*/
|
||||
export function areObjectsEqual(obj1: any, obj2: any): boolean {
|
||||
if (obj1 === obj2) return true;
|
||||
if (obj1 == null || obj2 == null) return false;
|
||||
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
|
||||
|
||||
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
if (obj1.length !== obj2.length) return false;
|
||||
return obj1.every((val, idx) => areObjectsEqual(val, obj2[idx]));
|
||||
}
|
||||
|
||||
if (obj1 instanceof Date && obj2 instanceof Date) {
|
||||
return obj1.getTime() === obj2.getTime();
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
return keys1.every((key) => {
|
||||
return keys2.includes(key) && areObjectsEqual(obj1[key], obj2[key]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user