Merge pull request #1812 from ManishMadan2882/main

Tools redesign
This commit is contained in:
Alex
2025-05-23 23:49:45 +01:00
committed by GitHub
12 changed files with 486 additions and 184 deletions

View File

@@ -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:

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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": {

View File

@@ -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 ${

View File

@@ -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"

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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 } };

View 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]);
});
}