From 6405097c6b35db41fee960a8156553acef953445 Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Sun, 26 Oct 2025 13:47:37 +0530
Subject: [PATCH] Add Role tab to agent detail page and fix restricted fields
permission issue (#15276)
---
.../ai/components/RoutingStatusDisplay.tsx | 18 +-
.../pages/settings/ai/SettingsAgentForm.tsx | 103 +++++++----
.../ai/components/SettingsAgentRoleTab.tsx | 118 ++++++++++++
.../components/SettingsAgentSettingsTab.tsx | 174 ++++++++++++++++++
.../ai/constants/SettingsAgentDetailTabs.ts | 7 +
.../forms/components/SettingsAIAgentForm.tsx | 26 ---
.../core-modules/ai/services/tool.service.ts | 30 ++-
.../services/create-record.service.ts | 29 ++-
.../services/find-records.service.ts | 52 +++++-
.../services/update-record.service.ts | 35 +++-
.../services/upsert-record.service.ts | 31 +++-
...nerate-create-record-input-schema.util.ts} | 5 +-
...enerate-update-record-input-schema.util.ts | 33 ++++
...ted-columns-from-restricted-fields.util.ts | 23 +++
.../zod-schemas/find-tool.zod-schema.ts | 10 +-
.../record-properties.zod-schema.ts | 12 +-
16 files changed, 597 insertions(+), 109 deletions(-)
create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx
create mode 100644 packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx
create mode 100644 packages/twenty-front/src/pages/settings/ai/constants/SettingsAgentDetailTabs.ts
rename packages/twenty-server/src/engine/core-modules/record-crud/{zod-schemas/record-input.zod-schema.ts => utils/generate-create-record-input-schema.util.ts} (79%)
create mode 100644 packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts
diff --git a/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx b/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx
index 468e55276a..4991c73eed 100644
--- a/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx
+++ b/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx
@@ -1,14 +1,8 @@
import { ShimmeringText } from '@/ai/components/ShimmeringText';
-import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
import { type DataMessagePart } from 'twenty-shared/ai';
import { IconCpu, IconSparkles } from 'twenty-ui/display';
-const pulseAnimation = keyframes`
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
-`;
-
const StyledRoutingContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.lighter};
@@ -25,9 +19,19 @@ const StyledRoutingContainer = styled.div`
const StyledIconContainer = styled.div<{ isLoading: boolean }>`
align-items: center;
animation: ${({ isLoading }) =>
- isLoading ? `${pulseAnimation} 2s ease-in-out infinite` : 'none'};
+ isLoading ? 'pulseAnimation 2s ease-in-out infinite' : 'none'};
color: ${({ theme }) => theme.color.blue};
display: flex;
+
+ @keyframes pulseAnimation {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ }
`;
const StyledText = styled.div`
diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx
index 8484697751..57f9de8882 100644
--- a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx
@@ -5,13 +5,14 @@ import { useParams } from 'react-router-dom';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
+import { TabList } from '@/ui/layout/tab-list/components/TabList';
+import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
-import { H2Title, IconTrash } from 'twenty-ui/display';
-import { Button } from 'twenty-ui/input';
+import { H2Title, IconLock, IconSettings } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import {
type CreateAgentInput,
@@ -23,9 +24,10 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { useState } from 'react';
-import { SettingsAgentDeleteConfirmationModal } from './components/SettingsAgentDeleteConfirmationModal';
import { SettingsAgentDetailSkeletonLoader } from './components/SettingsAgentDetailSkeletonLoader';
-import { SettingsAIAgentForm } from './forms/components/SettingsAIAgentForm';
+import { SettingsAgentRoleTab } from './components/SettingsAgentRoleTab';
+import { SettingsAgentSettingsTab } from './components/SettingsAgentSettingsTab';
+import { SETTINGS_AGENT_DETAIL_TABS } from './constants/SettingsAgentDetailTabs';
import { useSettingsAgentFormState } from './hooks/useSettingsAgentFormState';
const StyledContentContainer = styled.div`
@@ -36,20 +38,26 @@ const StyledContentContainer = styled.div`
width: 100%;
`;
-const DELETE_AGENT_MODAL_ID = 'delete-agent-modal';
+const StyledTabList = styled(TabList)`
+ margin-bottom: ${({ theme }) => theme.spacing(8)};
+`;
export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const { agentId = '' } = useParams<{ agentId: string }>();
const navigate = useNavigateSettings();
const navigateApp = useNavigateApp();
const { enqueueErrorSnackBar } = useSnackBar();
- const { openModal } = useModal();
const [isReadonlyMode, setIsReadonlyMode] = useState(false);
const isEditMode = mode === 'edit';
-
const isCreateMode = mode === 'create';
+ const tabListComponentId = `${SETTINGS_AGENT_DETAIL_TABS.COMPONENT_INSTANCE_ID}-${agentId}`;
+ const activeTabId = useRecoilComponentValue(
+ activeTabIdComponentState,
+ tabListComponentId,
+ );
+
const {
formValues,
isSubmitting,
@@ -105,6 +113,19 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const canSave = !isReadonlyMode && validateForm() && !isSubmitting;
+ const tabs = [
+ {
+ id: SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.SETTINGS,
+ title: t`Settings`,
+ Icon: IconSettings,
+ },
+ {
+ id: SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.ROLE,
+ title: t`Role`,
+ Icon: IconLock,
+ },
+ ];
+
const handleSave = async () => {
if (isReadonlyMode) {
return;
@@ -181,6 +202,31 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
: agent?.label
: t`New Agent`;
+ const renderActiveTabContent = () => {
+ switch (activeTabId) {
+ case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.ROLE:
+ return (
+
+ );
+
+ case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.SETTINGS:
+ return (
+
+ );
+ default:
+ return <>>;
+ }
+ };
+
return (
<>
{
{isEditMode && loading ? (
) : (
-
-
+
- {!isReadonlyMode &&
- isEditMode &&
- agent &&
- formValues.isCustom && (
-
-
-
- )}
-
+
+ {renderActiveTabContent()}
+
+ >
)}
- {isEditMode && agent && (
-
- )}
>
);
};
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx
new file mode 100644
index 0000000000..3f2130dca5
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx
@@ -0,0 +1,118 @@
+import styled from '@emotion/styled';
+import { useLingui } from '@lingui/react/macro';
+
+import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
+import { Select } from '@/ui/input/components/Select';
+import { SettingsPath } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+import {
+ H1Title,
+ H1TitleFontColor,
+ H2Title,
+ IconArrowUpRight,
+ IconUser,
+ useIcons,
+} from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+import { Section } from 'twenty-ui/layout';
+import { useGetRolesQuery } from '~/generated-metadata/graphql';
+import { useNavigateSettings } from '~/hooks/useNavigateSettings';
+import { type SettingsAIAgentFormValues } from '../hooks/useSettingsAgentFormState';
+
+const StyledRoleContainer = styled.div`
+ align-items: flex-end;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
+ margin-bottom: ${({ theme }) => theme.spacing(8)};
+`;
+
+const StyledRoleSelector = styled.div`
+ flex: 1;
+`;
+
+type SettingsAgentRoleTabProps = {
+ formValues: SettingsAIAgentFormValues;
+ onFieldChange: (
+ field: keyof SettingsAIAgentFormValues,
+ value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues],
+ ) => void;
+ disabled: boolean;
+};
+
+export const SettingsAgentRoleTab = ({
+ formValues,
+ onFieldChange,
+ disabled,
+}: SettingsAgentRoleTabProps) => {
+ const { t } = useLingui();
+ const { getIcon } = useIcons();
+ const navigateSettings = useNavigateSettings();
+
+ const { data: rolesData } = useGetRolesQuery();
+
+ const rolesOptions = [
+ {
+ label: t`None`,
+ value: null,
+ Icon: IconUser,
+ },
+ ...(rolesData?.getRoles
+ ?.filter((role) => role.canBeAssignedToAgents)
+ .map((role) => ({
+ label: role.label,
+ value: role.id,
+ Icon: getIcon(role.icon) ?? IconUser,
+ })) || []),
+ ];
+
+ const selectedRole = rolesData?.getRoles?.find(
+ (role) => role.id === formValues.role,
+ );
+
+ const handleOpenRole = () => {
+ if (isDefined(selectedRole)) {
+ navigateSettings(SettingsPath.RoleDetail, { roleId: selectedRole.id });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ {selectedRole?.id && (
+ <>
+
+
+ >
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx
new file mode 100644
index 0000000000..0f4169cd0c
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx
@@ -0,0 +1,174 @@
+import styled from '@emotion/styled';
+import { useLingui } from '@lingui/react/macro';
+
+import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
+import { IconPicker } from '@/ui/input/components/IconPicker';
+import { Select } from '@/ui/input/components/Select';
+import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
+import { TextArea } from '@/ui/input/components/TextArea';
+import { useModal } from '@/ui/layout/modal/hooks/useModal';
+import { isDefined } from 'twenty-shared/utils';
+import { H2Title, IconTrash } from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+import { Section } from 'twenty-ui/layout';
+import { type Agent } from '~/generated/graphql';
+import { SettingsAgentDeleteConfirmationModal } from '~/pages/settings/ai/components/SettingsAgentDeleteConfirmationModal';
+import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
+import { SettingsAgentModelCapabilities } from '../components/SettingsAgentModelCapabilities';
+import { type SettingsAIAgentFormValues } from '../hooks/useSettingsAgentFormState';
+
+const StyledFormContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledIconNameRow = styled.div`
+ align-items: flex-start;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledNameContainer = styled.div`
+ flex: 1;
+`;
+
+const StyledErrorMessage = styled.div`
+ color: ${({ theme }) => theme.color.red};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ margin-top: ${({ theme }) => theme.spacing(1)};
+`;
+
+const DELETE_AGENT_MODAL_ID = 'delete-agent-modal';
+
+type SettingsAgentSettingsTabProps = {
+ formValues: SettingsAIAgentFormValues;
+ onFieldChange: (
+ field: keyof SettingsAIAgentFormValues,
+ value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues],
+ ) => void;
+ disabled: boolean;
+ agent?: Agent;
+};
+
+export const SettingsAgentSettingsTab = ({
+ formValues,
+ onFieldChange,
+ disabled,
+ agent,
+}: SettingsAgentSettingsTabProps) => {
+ const { t } = useLingui();
+ const { openModal } = useModal();
+
+ const modelOptions = useAiModelOptions();
+
+ const noModelsAvailable = modelOptions.length === 0;
+
+ const fillNameFromLabel = (label: string) => {
+ if (isDefined(label)) {
+ onFieldChange('name', computeMetadataNameFromLabel(label));
+ }
+ };
+
+ return (
+
+
+
+ {
+ onFieldChange('icon', iconKey);
+ }}
+ disabled={disabled}
+ />
+
+
+ {
+ onFieldChange('label', value);
+ fillNameFromLabel(value);
+ }}
+ fullWidth
+ disabled={disabled}
+ />
+
+
+
+
+
+
+
+
+
+
+ {formValues.modelId && (
+
+
+ onFieldChange('modelConfiguration', configuration)
+ }
+ disabled={disabled}
+ />
+
+ )}
+
+
+
+
+ {!disabled && agent && formValues.isCustom && (
+
+
+
+ )}
+ {!disabled && agent && (
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/constants/SettingsAgentDetailTabs.ts b/packages/twenty-front/src/pages/settings/ai/constants/SettingsAgentDetailTabs.ts
new file mode 100644
index 0000000000..71fa03521f
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/ai/constants/SettingsAgentDetailTabs.ts
@@ -0,0 +1,7 @@
+export const SETTINGS_AGENT_DETAIL_TABS = {
+ COMPONENT_INSTANCE_ID: 'settings-agent-detail-tabs',
+ TABS_IDS: {
+ ROLE: 'role',
+ SETTINGS: 'settings',
+ },
+};
diff --git a/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx
index 5d05204b14..0d8847716b 100644
--- a/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/forms/components/SettingsAIAgentForm.tsx
@@ -7,7 +7,6 @@ import { Select } from '@/ui/input/components/Select';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { isDefined } from 'twenty-shared/utils';
-import { useGetRolesQuery } from '~/generated-metadata/graphql';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel';
import { SettingsAgentModelCapabilities } from '../../components/SettingsAgentModelCapabilities';
import { type SettingsAIAgentFormValues } from '../../hooks/useSettingsAgentFormState';
@@ -51,20 +50,6 @@ export const SettingsAIAgentForm = ({
const { t } = useLingui();
const modelOptions = useAiModelOptions();
- const { data: rolesData } = useGetRolesQuery();
-
- const rolesOptions = [
- {
- label: t`None`,
- value: null,
- },
- ...(rolesData?.getRoles
- ?.filter((role) => role.canBeAssignedToAgents)
- .map((role) => ({
- label: role.label,
- value: role.id,
- })) || []),
- ];
const noModelsAvailable = modelOptions.length === 0;
@@ -141,17 +126,6 @@ export const SettingsAIAgentForm = ({
)}
-
-
-