feat: workflow agent node permissions tab (#16092)

This commit is contained in:
Abdul Rahman
2025-11-28 02:57:33 +05:30
committed by GitHub
parent 41a07006ef
commit a343bc1aee
32 changed files with 2191 additions and 420 deletions
@@ -1,34 +1,13 @@
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader';
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow';
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useSettingsRolePermissionFlagConfig } from '@/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { useRecoilState } from 'recoil';
import {
H2Title,
IconApps,
IconCode,
IconCreditCard,
IconHierarchy,
IconKey,
IconLayoutSidebarRightCollapse,
IconLockOpen,
IconSettings,
IconSettingsAutomation,
IconShield,
IconSparkles,
IconSpy,
IconUsers,
} from 'twenty-ui/display';
import { H2Title, IconSettings } from 'twenty-ui/display';
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
const StyledTable = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
@@ -56,174 +35,13 @@ export const SettingsRolePermissionsSettingsSection = ({
settingsDraftRoleFamilyState(roleId),
);
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const isApplicationEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_APPLICATION_ENABLED,
);
const settingsPermissionsConfig = useMemo(() => {
const allPermissions: SettingsRolePermissionsSettingPermission[] = [
{
key: PermissionFlagType.API_KEYS_AND_WEBHOOKS,
name: t`API Keys & Webhooks`,
description: t`Manage API keys and webhooks`,
Icon: IconCode,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.WORKSPACE,
name: t`Workspace`,
description: t`Set global workspace preferences`,
Icon: IconSettings,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.WORKSPACE_MEMBERS,
name: t`Users`,
description: t`Add or remove users`,
Icon: IconUsers,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.ROLES,
name: t`Roles`,
description: t`Define user roles and access levels`,
Icon: IconLockOpen,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.DATA_MODEL,
name: t`Data Model`,
description: t`Edit data structure and fields`,
Icon: IconHierarchy,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.SECURITY,
name: t`Security`,
description: t`Manage security policies`,
Icon: IconKey,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.WORKFLOWS,
name: t`Workflows`,
description: t`Manage workflows`,
Icon: IconSettingsAutomation,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.SSO_BYPASS,
name: t`SSO Bypass`,
description: t`Enable bypass options`,
Icon: IconShield,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.IMPERSONATE,
name: t`Impersonate`,
description: t`Impersonate workspace users`,
Icon: IconSpy,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.APPLICATIONS,
name: t`Applications`,
description: t`Install and manage applications`,
Icon: IconApps,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.LAYOUTS,
name: t`Layouts`,
description: t`Customize page layouts and UI structure`,
Icon: IconLayoutSidebarRightCollapse,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.BILLING,
name: t`Billing`,
description: t`Manage billing and subscriptions`,
Icon: IconCreditCard,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.AI_SETTINGS,
name: t`AI`,
description: t`Create and configure AI agents`,
Icon: IconSparkles,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
];
return allPermissions.filter((permission) => {
if (permission.key === PermissionFlagType.AI_SETTINGS && !isAIEnabled) {
return false;
}
if (
permission.key === PermissionFlagType.APPLICATIONS &&
!isApplicationEnabled
) {
return false;
}
// Filter based on role assignment capabilities
const canBeAssignedOnlyToAgents =
settingsDraftRole.canBeAssignedToAgents &&
!settingsDraftRole.canBeAssignedToUsers &&
!settingsDraftRole.canBeAssignedToApiKeys;
const canBeAssignedOnlyToApiKeys =
settingsDraftRole.canBeAssignedToApiKeys &&
!settingsDraftRole.canBeAssignedToUsers &&
!settingsDraftRole.canBeAssignedToAgents;
const canBeAssignedOnlyToUsers =
settingsDraftRole.canBeAssignedToUsers &&
!settingsDraftRole.canBeAssignedToAgents &&
!settingsDraftRole.canBeAssignedToApiKeys;
if (canBeAssignedOnlyToAgents && !permission.isRelevantForAgents) {
return false;
}
if (canBeAssignedOnlyToApiKeys && !permission.isRelevantForApiKeys) {
return false;
}
if (canBeAssignedOnlyToUsers && !permission.isRelevantForUsers) {
return false;
}
return true;
});
}, [isAIEnabled, isApplicationEnabled, settingsDraftRole]);
const settingsPermissionsConfig = useSettingsRolePermissionFlagConfig({
assignmentCapabilities: {
canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
},
});
const shouldShowAllAccessToggle =
!settingsDraftRole.canBeAssignedToAgents ||
@@ -1,32 +1,14 @@
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader';
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow';
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useActionRolePermissionFlagConfig } from '@/settings/roles/role-permissions/permission-flags/hooks/useActionRolePermissionFlagConfig';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import {
H2Title,
IconApi,
IconAt,
IconDownload,
IconFileExport,
IconFileImport,
IconFileUpload,
IconMail,
IconSparkles,
IconTable,
IconTool,
IconUser,
} from 'twenty-ui/display';
import { H2Title, IconTool } from 'twenty-ui/display';
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
const StyledTable = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
@@ -54,144 +36,12 @@ export const SettingsRolePermissionsToolSection = ({
settingsDraftRoleFamilyState(roleId),
);
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const allPermissions: SettingsRolePermissionsSettingPermission[] = [
{
key: PermissionFlagType.AI,
name: t`Ask AI`,
description: t`Chat with AI agents and use AI features`,
Icon: IconSparkles,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
const toolPermissionsConfig = useActionRolePermissionFlagConfig({
assignmentCapabilities: {
canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
},
{
key: PermissionFlagType.UPLOAD_FILE,
name: t`Upload Files`,
description: t`Allow uploading files and attachments`,
Icon: IconFileUpload,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.DOWNLOAD_FILE,
name: t`Download Files`,
description: t`Allow downloading files and attachments`,
Icon: IconDownload,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.SEND_EMAIL_TOOL,
name: t`Send Email`,
description: t`Send emails via connected accounts`,
Icon: IconMail,
isToolPermission: true,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.HTTP_REQUEST_TOOL,
name: t`HTTP Request`,
description: t`Make HTTP requests to external APIs`,
Icon: IconApi,
isToolPermission: true,
isRelevantForAgents: true,
isRelevantForApiKeys: false,
isRelevantForUsers: false,
},
{
key: PermissionFlagType.IMPORT_CSV,
name: t`Import CSV`,
description: t`Allow importing data from CSV files`,
Icon: IconFileImport,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.EXPORT_CSV,
name: t`Export CSV`,
description: t`Allow exporting data to CSV files`,
Icon: IconFileExport,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.CONNECTED_ACCOUNTS,
name: t`Sync Account`,
description: t`Sync email and calendar accounts`,
Icon: IconAt,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.PROFILE_INFORMATION,
name: t`Edit Profile`,
description: t`Edit own profile information`,
Icon: IconUser,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.VIEWS,
name: t`Manage Views`,
description: t`Create, edit, and delete workspace views`,
Icon: IconTable,
isToolPermission: true,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
];
const toolPermissionsConfig = allPermissions.filter((permission) => {
if (permission.key === PermissionFlagType.AI && !isAIEnabled) {
return false;
}
const canBeAssignedOnlyToAgents =
settingsDraftRole.canBeAssignedToAgents &&
!settingsDraftRole.canBeAssignedToUsers &&
!settingsDraftRole.canBeAssignedToApiKeys;
const canBeAssignedOnlyToApiKeys =
settingsDraftRole.canBeAssignedToApiKeys &&
!settingsDraftRole.canBeAssignedToUsers &&
!settingsDraftRole.canBeAssignedToAgents;
const canBeAssignedOnlyToUsers =
settingsDraftRole.canBeAssignedToUsers &&
!settingsDraftRole.canBeAssignedToAgents &&
!settingsDraftRole.canBeAssignedToApiKeys;
if (canBeAssignedOnlyToAgents && !permission.isRelevantForAgents) {
return false;
}
if (canBeAssignedOnlyToApiKeys && !permission.isRelevantForApiKeys) {
return false;
}
if (canBeAssignedOnlyToUsers && !permission.isRelevantForUsers) {
return false;
}
return true;
});
const shouldShowAllAccessToggle =
@@ -0,0 +1,184 @@
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import {
IconApi,
IconAt,
IconDownload,
IconFileExport,
IconFileImport,
IconFileUpload,
IconMail,
IconSparkles,
IconTable,
IconUser,
} from 'twenty-ui/display';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
type UseActionRolePermissionFlagConfigParams = {
assignmentCapabilities?: {
canBeAssignedToAgents?: boolean;
canBeAssignedToUsers?: boolean;
canBeAssignedToApiKeys?: boolean;
};
};
export const useActionRolePermissionFlagConfig = ({
assignmentCapabilities,
}: UseActionRolePermissionFlagConfigParams = {}): SettingsRolePermissionsSettingPermission[] => {
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const {
canBeAssignedToAgents = false,
canBeAssignedToUsers = false,
canBeAssignedToApiKeys = false,
} = assignmentCapabilities ?? {};
const hasAssignmentCapabilities = assignmentCapabilities !== undefined;
return useMemo(() => {
const allPermissions: SettingsRolePermissionsSettingPermission[] = [
{
key: PermissionFlagType.AI,
name: t`Ask AI`,
description: t`Chat with AI agents and use AI features`,
Icon: IconSparkles,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.UPLOAD_FILE,
name: t`Upload Files`,
description: t`Allow uploading files and attachments`,
Icon: IconFileUpload,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.DOWNLOAD_FILE,
name: t`Download Files`,
description: t`Allow downloading files and attachments`,
Icon: IconDownload,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.SEND_EMAIL_TOOL,
name: t`Send Email`,
description: t`Send emails via connected accounts`,
Icon: IconMail,
isToolPermission: true,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.HTTP_REQUEST_TOOL,
name: t`HTTP Request`,
description: t`Make HTTP requests to external APIs`,
Icon: IconApi,
isToolPermission: true,
isRelevantForAgents: true,
isRelevantForApiKeys: false,
isRelevantForUsers: false,
},
{
key: PermissionFlagType.IMPORT_CSV,
name: t`Import CSV`,
description: t`Allow importing data from CSV files`,
Icon: IconFileImport,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.EXPORT_CSV,
name: t`Export CSV`,
description: t`Allow exporting data to CSV files`,
Icon: IconFileExport,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.CONNECTED_ACCOUNTS,
name: t`Sync Account`,
description: t`Sync email and calendar accounts`,
Icon: IconAt,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.PROFILE_INFORMATION,
name: t`Edit Profile`,
description: t`Edit own profile information`,
Icon: IconUser,
isToolPermission: true,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.VIEWS,
name: t`Manage Views`,
description: t`Create, edit, and delete workspace views`,
Icon: IconTable,
isToolPermission: true,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
];
const canBeAssignedOnlyToAgents =
canBeAssignedToAgents && !canBeAssignedToUsers && !canBeAssignedToApiKeys;
const canBeAssignedOnlyToApiKeys =
canBeAssignedToApiKeys && !canBeAssignedToUsers && !canBeAssignedToAgents;
const canBeAssignedOnlyToUsers =
canBeAssignedToUsers && !canBeAssignedToAgents && !canBeAssignedToApiKeys;
return allPermissions.filter((permission) => {
if (permission.key === PermissionFlagType.AI && !isAIEnabled) {
return false;
}
if (hasAssignmentCapabilities) {
if (canBeAssignedOnlyToAgents && !permission.isRelevantForAgents) {
return false;
}
if (canBeAssignedOnlyToApiKeys && !permission.isRelevantForApiKeys) {
return false;
}
if (canBeAssignedOnlyToUsers && !permission.isRelevantForUsers) {
return false;
}
}
return true;
});
}, [
hasAssignmentCapabilities,
canBeAssignedToAgents,
canBeAssignedToUsers,
canBeAssignedToApiKeys,
isAIEnabled,
]);
};
@@ -0,0 +1,214 @@
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import {
IconApps,
IconCode,
IconCreditCard,
IconHierarchy,
IconKey,
IconLayoutSidebarRightCollapse,
IconLockOpen,
IconSettings,
IconSettingsAutomation,
IconShield,
IconSparkles,
IconSpy,
IconUsers,
} from 'twenty-ui/display';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
type UseSettingsRolePermissionFlagConfigParams = {
assignmentCapabilities?: {
canBeAssignedToAgents?: boolean;
canBeAssignedToUsers?: boolean;
canBeAssignedToApiKeys?: boolean;
};
};
export const useSettingsRolePermissionFlagConfig = ({
assignmentCapabilities,
}: UseSettingsRolePermissionFlagConfigParams = {}): SettingsRolePermissionsSettingPermission[] => {
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const isApplicationEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_APPLICATION_ENABLED,
);
const {
canBeAssignedToAgents = false,
canBeAssignedToUsers = false,
canBeAssignedToApiKeys = false,
} = assignmentCapabilities ?? {};
const hasAssignmentCapabilities = assignmentCapabilities !== undefined;
return useMemo(() => {
const allPermissions: SettingsRolePermissionsSettingPermission[] = [
{
key: PermissionFlagType.API_KEYS_AND_WEBHOOKS,
name: t`API Keys & Webhooks`,
description: t`Manage API keys and webhooks`,
Icon: IconCode,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.WORKSPACE,
name: t`Workspace`,
description: t`Set global workspace preferences`,
Icon: IconSettings,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.WORKSPACE_MEMBERS,
name: t`Users`,
description: t`Add or remove users`,
Icon: IconUsers,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.ROLES,
name: t`Roles`,
description: t`Define user roles and access levels`,
Icon: IconLockOpen,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.DATA_MODEL,
name: t`Data Model`,
description: t`Edit data structure and fields`,
Icon: IconHierarchy,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.SECURITY,
name: t`Security`,
description: t`Manage security policies`,
Icon: IconKey,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.WORKFLOWS,
name: t`Workflows`,
description: t`Manage workflows`,
Icon: IconSettingsAutomation,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.SSO_BYPASS,
name: t`SSO Bypass`,
description: t`Enable bypass options`,
Icon: IconShield,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.IMPERSONATE,
name: t`Impersonate`,
description: t`Impersonate workspace users`,
Icon: IconSpy,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.APPLICATIONS,
name: t`Applications`,
description: t`Install and manage applications`,
Icon: IconApps,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.LAYOUTS,
name: t`Layouts`,
description: t`Customize page layouts and UI structure`,
Icon: IconLayoutSidebarRightCollapse,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.BILLING,
name: t`Billing`,
description: t`Manage billing and subscriptions`,
Icon: IconCreditCard,
isRelevantForAgents: false,
isRelevantForApiKeys: false,
isRelevantForUsers: true,
},
{
key: PermissionFlagType.AI_SETTINGS,
name: t`AI`,
description: t`Create and configure AI agents`,
Icon: IconSparkles,
isRelevantForAgents: true,
isRelevantForApiKeys: true,
isRelevantForUsers: true,
},
];
const canBeAssignedOnlyToAgents =
canBeAssignedToAgents && !canBeAssignedToUsers && !canBeAssignedToApiKeys;
const canBeAssignedOnlyToApiKeys =
canBeAssignedToApiKeys && !canBeAssignedToUsers && !canBeAssignedToAgents;
const canBeAssignedOnlyToUsers =
canBeAssignedToUsers && !canBeAssignedToAgents && !canBeAssignedToApiKeys;
return allPermissions.filter((permission) => {
if (permission.key === PermissionFlagType.AI_SETTINGS && !isAIEnabled) {
return false;
}
if (
permission.key === PermissionFlagType.APPLICATIONS &&
!isApplicationEnabled
) {
return false;
}
if (hasAssignmentCapabilities) {
if (canBeAssignedOnlyToAgents && !permission.isRelevantForAgents) {
return false;
}
if (canBeAssignedOnlyToApiKeys && !permission.isRelevantForApiKeys) {
return false;
}
if (canBeAssignedOnlyToUsers && !permission.isRelevantForUsers) {
return false;
}
}
return true;
});
}, [
hasAssignmentCapabilities,
canBeAssignedToAgents,
canBeAssignedToUsers,
canBeAssignedToApiKeys,
isAIEnabled,
isApplicationEnabled,
]);
};
@@ -17,11 +17,13 @@ const StyledWorkflowStepBody = styled.div`
export const WorkflowStepBody = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<StyledWorkflowStepBody>
<StyledWorkflowStepBody className={className}>
<AppErrorBoundary
resetOnLocationChange={true}
FallbackComponent={({
@@ -8,13 +8,23 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { useDuplicateStep } from '@/workflow/workflow-steps/hooks/useDuplicateStep';
import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState';
import { useLingui } from '@lingui/react/macro';
import { useId } from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { IconCopyPlus, IconPencil, IconTrash } from 'twenty-ui/display';
import {
IconCopyPlus,
IconPencil,
IconRobot,
IconTrash,
IconUsers,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const WorkflowStepFooter = ({
stepId,
@@ -33,12 +43,22 @@ export const WorkflowStepFooter = ({
openWorkflowTriggerTypeInCommandMenu,
} = useWorkflowCommandMenu();
const { deleteStep } = useDeleteStep();
const navigateSettings = useNavigateSettings();
const workflowAiAgentActionAgent = useRecoilValue(
workflowAiAgentActionAgentState,
);
const shouldPinDeleteButton =
!isDefined(additionalActions) || additionalActions.length === 0;
const agentId = workflowAiAgentActionAgent?.id;
const hasViewAgentOption = isDefined(agentId);
const hasViewRoleOption = isDefined(workflowAiAgentActionAgent?.roleId);
const selectableItemIdArray = [
'change-node-type',
...(stepId !== TRIGGER_STEP_ID ? ['duplicate'] : []),
...(hasViewAgentOption ? ['view-agent'] : []),
...(hasViewRoleOption ? ['view-role'] : []),
...(!shouldPinDeleteButton ? ['delete'] : []),
];
@@ -62,6 +82,22 @@ export const WorkflowStepFooter = ({
deleteStep(stepId);
};
const handleViewAgent = () => {
closeDropdown(dropdownId);
if (isDefined(agentId)) {
navigateSettings(SettingsPath.AIAgentDetail, { agentId });
}
};
const handleViewRole = () => {
closeDropdown(dropdownId);
if (isDefined(workflowAiAgentActionAgent?.roleId)) {
navigateSettings(SettingsPath.RoleDetail, {
roleId: workflowAiAgentActionAgent.roleId,
});
}
};
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
@@ -94,6 +130,26 @@ export const WorkflowStepFooter = ({
/>
</SelectableListItem>
)}
{hasViewAgentOption && (
<SelectableListItem itemId="view-agent" onEnter={handleViewAgent}>
<MenuItem
focused={selectedItemId === 'view-agent'}
onClick={handleViewAgent}
text={t`View Agent`}
LeftIcon={IconRobot}
/>
</SelectableListItem>
)}
{hasViewRoleOption && (
<SelectableListItem itemId="view-role" onEnter={handleViewRole}>
<MenuItem
focused={selectedItemId === 'view-role'}
onClick={handleViewRole}
text={t`View Role`}
LeftIcon={IconUsers}
/>
</SelectableListItem>
)}
{!shouldPinDeleteButton && (
<SelectableListItem itemId="delete" onEnter={handleDeleteNode}>
<MenuItem
@@ -1,5 +1,9 @@
import React from 'react';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
const mockDeleteWorkflowVersionStep = jest.fn();
const mockGetUpdatableWorkflowVersion = jest.fn();
@@ -33,6 +37,36 @@ jest.mock('@/command-menu/hooks/useCommandMenu', () => ({
}),
}));
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
useWorkflowWithCurrentVersion: () => undefined,
}));
jest.mock(
'@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose',
() => ({
useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose: () => ({
resetPermissionState: jest.fn(),
}),
}),
);
const wrapper = ({ children }: { children: React.ReactNode }) => {
const workflowVisualizerComponentInstanceId =
'workflow-visualizer-instance-id';
return (
<RecoilRoot>
<WorkflowVisualizerComponentInstanceContext.Provider
value={{
instanceId: workflowVisualizerComponentInstanceId,
}}
>
{children}
</WorkflowVisualizerComponentInstanceContext.Provider>
</RecoilRoot>
);
};
describe('useDeleteStep', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -55,7 +89,9 @@ describe('useDeleteStep', () => {
},
});
const { result } = renderHook(() => useDeleteStep());
const { result } = renderHook(() => useDeleteStep(), {
wrapper,
});
await result.current.deleteStep(mockStepId);
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalled();
@@ -1,19 +1,36 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
import { useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose';
import { isDefined } from 'twenty-shared/utils';
export const useDeleteStep = () => {
const { resetPermissionState } =
useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose();
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
const { deleteStepsOutputSchema } = useStepsOutputSchema();
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
const { closeCommandMenu } = useCommandMenu();
const workflowVisualizerWorkflowId = useRecoilComponentValue(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const deleteStep = async (stepId: string) => {
const workflowVersionId = await getUpdatableWorkflowVersion();
const isAiAgentStep =
isDefined(workflow?.currentVersion?.steps) &&
workflow.currentVersion.steps.some(
(step) => step.id === stepId && step.type === 'AI_AGENT',
);
await deleteWorkflowVersionStep({
workflowVersionId,
stepId,
@@ -25,6 +42,10 @@ export const useDeleteStep = () => {
stepIds: [stepId],
workflowVersionId,
});
if (isAiAgentStep) {
resetPermissionState();
}
};
return {
@@ -0,0 +1,120 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/SettingsRoleObjectPermissionIconConfig';
import { t } from '@lingui/core/macro';
import { type ObjectPermission } from '~/generated/graphql';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
import { CRUD_PERMISSIONS } from '../constants/WorkflowAiAgentCrudPermissions';
import { WorkflowAiAgentPermissionsPermissionRow } from './WorkflowAiAgentPermissionsPermissionRow';
import { StyledLabel, StyledList } from './WorkflowAiAgentPermissionsStyles';
const CRUD_PERMISSION_ORDER = CRUD_PERMISSIONS.reduce<
Record<SettingsRoleObjectPermissionKey, number>
>(
(orderMap, permission, index) => {
orderMap[permission.key] = index;
return orderMap;
},
{} as Record<SettingsRoleObjectPermissionKey, number>,
);
type WorkflowAiAgentPermissionListProps = {
readonly: boolean;
objectPermissions: ObjectPermission[];
onDeletePermission: (
objectMetadataId: string,
permissionKey: SettingsRoleObjectPermissionKey,
) => void;
searchQuery: string;
};
export const WorkflowAiAgentPermissionList = ({
readonly,
objectPermissions,
onDeletePermission,
searchQuery,
}: WorkflowAiAgentPermissionListProps) => {
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const sortedExistingPermissions = objectPermissions
.reduce<
Array<{
objectMetadataId: string;
objectLabel: string;
permissionKey: SettingsRoleObjectPermissionKey;
permissionLabel: string;
}>
>((permissions, objectPermission) => {
const objectMetadata = objectMetadataItems.find(
(item) => item.id === objectPermission.objectMetadataId,
);
if (!objectMetadata) {
return permissions;
}
CRUD_PERMISSIONS.forEach((crud) => {
if (objectPermission[crud.key] === true) {
permissions.push({
objectMetadataId: objectPermission.objectMetadataId,
objectLabel: objectMetadata.labelPlural,
permissionKey: crud.key,
permissionLabel: crud.label(objectMetadata.labelPlural),
});
}
});
return permissions;
}, [])
.sort((permissionA, permissionB) => {
if (
permissionA.objectMetadataId === permissionB.objectMetadataId &&
permissionA.permissionKey !== permissionB.permissionKey
) {
return (
CRUD_PERMISSION_ORDER[permissionA.permissionKey] -
CRUD_PERMISSION_ORDER[permissionB.permissionKey]
);
}
return permissionA.objectLabel.localeCompare(permissionB.objectLabel);
});
const filteredPermissions = filterBySearchQuery({
items: sortedExistingPermissions,
searchQuery,
getSearchableValues: (permission) => [
permission.permissionLabel,
permission.objectLabel,
],
});
if (filteredPermissions.length === 0) {
return null;
}
return (
<div>
<StyledLabel>{t`CRUD`}</StyledLabel>
<StyledList>
{filteredPermissions.map((permission) => (
<WorkflowAiAgentPermissionsPermissionRow
key={`${permission.objectMetadataId}-${permission.permissionKey}`}
permission={{
key: permission.permissionKey,
label: permission.permissionLabel,
}}
isEnabled={true}
readonly={readonly}
onDelete={() =>
onDeletePermission(
permission.objectMetadataId,
permission.permissionKey,
)
}
/>
))}
</StyledList>
</div>
);
};
@@ -0,0 +1,49 @@
import { type SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/SettingsRoleObjectPermissionIconConfig';
import { t } from '@lingui/core/macro';
import { type ObjectPermission } from '~/generated-metadata/graphql';
import { WorkflowAiAgentPermissionsPermissionRow } from './WorkflowAiAgentPermissionsPermissionRow';
import { StyledLabel, StyledList } from './WorkflowAiAgentPermissionsStyles';
type WorkflowAiAgentPermissionsCrudListProps = {
permissions: Array<{
key: SettingsRoleObjectPermissionKey;
label: string;
}>;
objectPermissions?: ObjectPermission;
readonly: boolean;
onAddPermission: (
objectMetadataId: string,
permissionKey: SettingsRoleObjectPermissionKey,
) => void;
objectMetadataId: string;
};
export const WorkflowAiAgentPermissionsCrudList = ({
permissions,
objectPermissions,
readonly,
onAddPermission,
objectMetadataId,
}: WorkflowAiAgentPermissionsCrudListProps) => {
return (
<div>
<StyledLabel>{t`CRUD`}</StyledLabel>
<StyledList>
{permissions.map((permission) => {
const isEnabled = Boolean(objectPermissions?.[permission.key]);
return (
<WorkflowAiAgentPermissionsPermissionRow
key={permission.key}
permission={permission}
isEnabled={isEnabled}
readonly={readonly}
showDeleteButton={false}
alwaysShowGranted={true}
onAdd={() => onAddPermission(objectMetadataId, permission.key)}
/>
);
})}
</StyledList>
</div>
);
};
@@ -0,0 +1,61 @@
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { type PermissionFlagType } from '~/generated-metadata/graphql';
import { WorkflowAiAgentPermissionsFlagRow } from './WorkflowAiAgentPermissionsFlagRow';
import { StyledLabel, StyledList } from './WorkflowAiAgentPermissionsStyles';
type WorkflowAiAgentPermissionsFlagListProps = {
title: string;
permissions: SettingsRolePermissionsSettingPermission[];
enabledPermissionFlagKeys: PermissionFlagType[];
readonly: boolean;
showDeleteButton?: boolean;
onAddPermissionFlag?: (permissionKey: PermissionFlagType) => void;
onDeletePermissionFlag?: (permissionKey: PermissionFlagType) => void;
};
export const WorkflowAiAgentPermissionsFlagList = ({
title,
permissions,
enabledPermissionFlagKeys,
readonly,
showDeleteButton = false,
onAddPermissionFlag,
onDeletePermissionFlag,
}: WorkflowAiAgentPermissionsFlagListProps) => {
const hasPermissions = permissions.length > 0;
if (!hasPermissions) {
return null;
}
return (
<div>
<StyledLabel>{title}</StyledLabel>
<StyledList>
{permissions.map((permission) => {
const isEnabled = enabledPermissionFlagKeys.includes(permission.key);
return (
<WorkflowAiAgentPermissionsFlagRow
key={permission.key}
permission={permission}
isEnabled={isEnabled}
readonly={readonly}
showDeleteButton={showDeleteButton}
onAdd={
isEnabled
? undefined
: () => onAddPermissionFlag?.(permission.key)
}
onDelete={
showDeleteButton
? () => onDeletePermissionFlag?.(permission.key)
: undefined
}
/>
);
})}
</StyledList>
</div>
);
};
@@ -0,0 +1,60 @@
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useTheme } from '@emotion/react';
import { IconTrash } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import {
StyledDeleteButton,
StyledIconContainer,
StyledRow,
StyledRowLeftContent,
StyledText,
} from './WorkflowAiAgentPermissionsStyles';
type WorkflowAiAgentPermissionsFlagRowProps = {
permission: SettingsRolePermissionsSettingPermission;
isEnabled: boolean;
readonly: boolean;
showDeleteButton?: boolean;
onAdd?: () => void;
onDelete?: () => void;
};
export const WorkflowAiAgentPermissionsFlagRow = ({
permission,
isEnabled,
readonly,
showDeleteButton = false,
onAdd,
onDelete,
}: WorkflowAiAgentPermissionsFlagRowProps) => {
const theme = useTheme();
const isClickable = !readonly && !isEnabled && Boolean(onAdd);
const isDisabled = isEnabled && !showDeleteButton;
return (
<StyledRow
onClick={isClickable ? onAdd : undefined}
isDisabled={isDisabled}
>
<StyledRowLeftContent>
<StyledIconContainer>
<permission.Icon size={theme.icon.size.sm} />
</StyledIconContainer>
<StyledText>{permission.name}</StyledText>
</StyledRowLeftContent>
{isEnabled && showDeleteButton && (
<StyledDeleteButton data-delete-button>
<IconButton
Icon={IconTrash}
variant="tertiary"
size="small"
onClick={(event) => {
event.stopPropagation();
onDelete?.();
}}
/>
</StyledDeleteButton>
)}
</StyledRow>
);
};
@@ -0,0 +1,44 @@
import { useTheme } from '@emotion/react';
import { useIcons } from 'twenty-ui/display';
import {
StyledIconChevronRight,
StyledIconContainer,
StyledRow,
StyledRowLeftContent,
StyledText,
} from './WorkflowAiAgentPermissionsStyles';
type WorkflowAiAgentPermissionsObjectRowProps = {
objectMetadata: {
id: string;
icon?: string | null;
labelPlural: string;
};
onClick?: () => void;
readonly: boolean;
};
export const WorkflowAiAgentPermissionsObjectRow = ({
objectMetadata,
onClick,
readonly,
}: WorkflowAiAgentPermissionsObjectRowProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const IconComponent = getIcon(objectMetadata.icon);
return (
<StyledRow
key={objectMetadata.id}
onClick={!readonly ? onClick : undefined}
>
<StyledRowLeftContent>
<StyledIconContainer>
<IconComponent size={theme.icon.size.sm} />
</StyledIconContainer>
<StyledText>{objectMetadata.labelPlural}</StyledText>
</StyledRowLeftContent>
<StyledIconChevronRight size={theme.icon.size.sm} />
</StyledRow>
);
};
@@ -0,0 +1,34 @@
import { WorkflowAiAgentPermissionsObjectRow } from './WorkflowAiAgentPermissionsObjectRow';
import { StyledLabel, StyledList } from './WorkflowAiAgentPermissionsStyles';
type WorkflowAiAgentPermissionsObjectsListProps = {
objects: Array<{
id: string;
icon?: string | null;
labelPlural: string;
}>;
onObjectClick: (objectId: string) => void;
readonly: boolean;
};
export const WorkflowAiAgentPermissionsObjectsList = ({
objects,
onObjectClick,
readonly,
}: WorkflowAiAgentPermissionsObjectsListProps) => {
return (
<div>
<StyledLabel>Objects</StyledLabel>
<StyledList>
{objects.map((objectMetadata) => (
<WorkflowAiAgentPermissionsObjectRow
key={objectMetadata.id}
objectMetadata={objectMetadata}
onClick={() => onObjectClick(objectMetadata.id)}
readonly={readonly}
/>
))}
</StyledList>
</div>
);
};
@@ -0,0 +1,67 @@
import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon';
import { type SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/SettingsRoleObjectPermissionIconConfig';
import { IconTrash } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import {
StyledDeleteButton,
StyledIconContainer,
StyledRow,
StyledRowLeftContent,
StyledText,
} from './WorkflowAiAgentPermissionsStyles';
type WorkflowAiAgentPermissionsPermissionRowProps = {
permission: {
key: SettingsRoleObjectPermissionKey;
label: string;
};
isEnabled: boolean;
readonly: boolean;
showDeleteButton?: boolean;
alwaysShowGranted?: boolean;
onAdd?: () => void;
onDelete?: () => void;
};
export const WorkflowAiAgentPermissionsPermissionRow = ({
permission,
isEnabled,
readonly,
showDeleteButton = true,
alwaysShowGranted = false,
onAdd,
onDelete,
}: WorkflowAiAgentPermissionsPermissionRowProps) => {
const isClickable = !readonly && !isEnabled;
const isDisabled = isEnabled && !showDeleteButton;
return (
<StyledRow
onClick={isClickable ? onAdd : undefined}
isDisabled={isDisabled}
>
<StyledRowLeftContent>
<StyledIconContainer>
<PermissionIcon
permission={permission.key}
state={alwaysShowGranted || isEnabled ? 'granted' : 'revoked'}
/>
</StyledIconContainer>
<StyledText>{permission.label}</StyledText>
</StyledRowLeftContent>
{isEnabled && showDeleteButton && (
<StyledDeleteButton data-delete-button>
<IconButton
Icon={IconTrash}
variant="tertiary"
size="small"
onClick={(e) => {
e.stopPropagation();
onDelete?.();
}}
/>
</StyledDeleteButton>
)}
</StyledRow>
);
};
@@ -0,0 +1,64 @@
import styled from '@emotion/styled';
import { IconChevronRight, Label } from 'twenty-ui/display';
export const StyledText = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
`;
export const StyledLabel = styled(Label)`
margin: ${({ theme }) => theme.spacing(3, 3, 0)};
`;
export const StyledRow = styled.div<{ isDisabled?: boolean }>`
align-items: center;
display: flex;
justify-content: space-between;
width: 100%;
padding: ${({ theme }) => theme.spacing(1)};
box-sizing: border-box;
border-radius: ${({ theme }) => theme.spacing(1)};
cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'pointer')};
opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
pointer-events: ${({ isDisabled }) => (isDisabled ? 'none' : 'auto')};
:hover {
background-color: ${({ theme, isDisabled }) =>
isDisabled ? 'inherit' : theme.background.transparent.light};
}
:hover [data-delete-button] {
opacity: 1;
}
`;
export const StyledDeleteButton = styled.div`
opacity: 0;
transition: opacity ${({ theme }) => theme.animation.duration.fast}ms;
`;
export const StyledRowLeftContent = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const StyledList = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(0.5)};
padding: ${({ theme }) => theme.spacing(2)};
`;
export const StyledIconContainer = styled.div`
background-color: ${({ theme }) => theme.background.tertiary};
padding: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.spacing(1)};
display: flex;
justify-content: center;
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
@@ -0,0 +1,264 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useActionRolePermissionFlagConfig } from '@/settings/roles/role-permissions/permission-flags/hooks/useActionRolePermissionFlagConfig';
import { TextInput } from '@/ui/input/components/TextInput';
import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { useWorkflowAiAgentPermissionActions } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useWorkflowAiAgentPermissionActions';
import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState';
import { workflowAiAgentPermissionsIsAddingPermissionState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsIsAddingPermissionState';
import { workflowAiAgentPermissionsSelectedObjectIdState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsSelectedObjectIdState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { type Agent, useGetRolesQuery } from '~/generated-metadata/graphql';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { WorkflowAiAgentPermissionList } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPermissionList';
import { CRUD_PERMISSIONS } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/WorkflowAiAgentCrudPermissions';
import { WorkflowAiAgentPermissionsCrudList } from './WorkflowAiAgentPermissionsCrudList';
import { WorkflowAiAgentPermissionsFlagList } from './WorkflowAiAgentPermissionsFlagList';
import { WorkflowAiAgentPermissionsObjectsList } from './WorkflowAiAgentPermissionsObjectsList';
import { getFilteredPermissions } from './workflowAiAgentPermissions.utils';
const StyledSearchInput = styled(TextInput)`
width: 100%;
height: 40px;
border-block: 1px solid ${({ theme }) => theme.border.color.medium};
input {
height: 40px;
line-height: 40px;
border: none;
border-radius: 0;
width: 100%;
}
`;
const StyledBackButtonText = styled.span`
color: ${({ theme }) => theme.font.color.secondary};
`;
const StyledBackButton = styled.button`
width: 100%;
align-items: center;
background: none;
border: none;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(3)};
text-align: left;
&:hover {
color: ${({ theme }) => theme.font.color.primary};
}
`;
const StyledContainer = styled.div`
width: 100%;
`;
type WorkflowAiAgentPermissionsTabProps = {
action: WorkflowAiAgentAction;
readonly: boolean;
isAgentLoading: boolean;
refetchAgent: () => Promise<{ data?: { findOneAgent?: Agent } }>;
};
export const WorkflowAiAgentPermissionsTab = ({
readonly,
isAgentLoading,
refetchAgent,
}: WorkflowAiAgentPermissionsTabProps) => {
const workflowAiAgentActionAgent = useRecoilValue(
workflowAiAgentActionAgentState,
);
const [
workflowAiAgentPermissionsSelectedObjectId,
setWorkflowAiAgentPermissionsSelectedObjectId,
] = useRecoilState(workflowAiAgentPermissionsSelectedObjectIdState);
const [
workflowAiAgentPermissionsIsAddingPermission,
setWorkflowAiAgentPermissionsIsAddingPermission,
] = useRecoilState(workflowAiAgentPermissionsIsAddingPermissionState);
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const {
data: rolesData,
loading: rolesLoading,
refetch: refetchRoles,
} = useGetRolesQuery();
const [searchQuery, setSearchQuery] = useState('');
const role = rolesData?.getRoles.find(
(item) => item.id === workflowAiAgentActionAgent?.roleId,
);
const objectPermissions = role?.objectPermissions || [];
const permissionFlagKeys =
role?.permissionFlags?.map((permissionFlag) => permissionFlag.flag) ?? [];
const hasRoleWithPermissions =
isDefined(workflowAiAgentActionAgent?.roleId) &&
(objectPermissions.length > 0 || permissionFlagKeys.length > 0);
const actionPermissionsConfig = useActionRolePermissionFlagConfig({
assignmentCapabilities: { canBeAssignedToAgents: true },
});
type ObjectMetadataListItem = (typeof objectMetadataItems)[number];
const filteredObjects = filterBySearchQuery<ObjectMetadataListItem>({
items: objectMetadataItems,
searchQuery,
getSearchableValues: (item) => [item.labelSingular, item.labelPlural],
});
const {
filteredPermissions: filteredActionPermissions,
filteredEnabledPermissions: filteredEnabledActionPermissions,
} = getFilteredPermissions({
permissions: actionPermissionsConfig,
permissionFlagKeys,
searchQuery,
});
const refetchAgentAndRoles = async () => {
await refetchRoles();
const result = await refetchAgent();
return {
refetchedAgent: result?.data?.findOneAgent,
};
};
const {
handleAddPermission,
handleDeletePermission,
handleAddPermissionFlag,
handleDeletePermissionFlag,
} = useWorkflowAiAgentPermissionActions({
readonly,
objectPermissions,
permissionFlagKeys,
refetchAgentAndRoles,
});
if (isAgentLoading || rolesLoading) {
return <RightDrawerSkeletonLoader />;
}
if (!isDefined(workflowAiAgentActionAgent)) {
return null;
}
const selectedObject = isDefined(workflowAiAgentPermissionsSelectedObjectId)
? objectMetadataItems.find(
(item) => item.id === workflowAiAgentPermissionsSelectedObjectId,
)
: undefined;
const objectPermissionForSelected = isDefined(selectedObject)
? objectPermissions.find(
(permission) => permission.objectMetadataId === selectedObject.id,
)
: undefined;
const handleBack = () => {
isDefined(workflowAiAgentPermissionsSelectedObjectId) &&
setWorkflowAiAgentPermissionsSelectedObjectId(undefined);
!isDefined(workflowAiAgentPermissionsSelectedObjectId) &&
setWorkflowAiAgentPermissionsIsAddingPermission(false);
};
const handleObjectClick = (objectId: string) => {
setWorkflowAiAgentPermissionsSelectedObjectId(objectId);
};
const shouldShowBackButton =
isDefined(workflowAiAgentPermissionsSelectedObjectId) ||
workflowAiAgentPermissionsIsAddingPermission;
const shouldShowCrudList = isDefined(selectedObject);
const shouldShowSelectionLists =
(!hasRoleWithPermissions || workflowAiAgentPermissionsIsAddingPermission) &&
!isDefined(selectedObject);
const shouldShowExistingPermissions =
hasRoleWithPermissions && !workflowAiAgentPermissionsIsAddingPermission;
return (
<StyledContainer>
{shouldShowBackButton && (
<StyledBackButton onClick={handleBack}>
<IconButton Icon={IconChevronLeft} variant="tertiary" size="small" />
<StyledBackButtonText>{t`Add permission`}</StyledBackButtonText>
</StyledBackButton>
)}
<StyledSearchInput
value={searchQuery}
onChange={(value: string) => setSearchQuery(value)}
placeholder={t`Type anything...`}
onKeyDown={(event) => {
if (isNonTextWritingKey(event.key)) {
event.stopPropagation();
}
}}
/>
{shouldShowCrudList && (
<WorkflowAiAgentPermissionsCrudList
permissions={filterBySearchQuery({
items: CRUD_PERMISSIONS.map((p) => ({
key: p.key,
label: p.label(selectedObject.labelPlural),
})),
searchQuery,
getSearchableValues: (permission) => [permission.label],
})}
objectPermissions={objectPermissionForSelected}
readonly={readonly}
onAddPermission={handleAddPermission}
objectMetadataId={selectedObject.id}
/>
)}
{shouldShowSelectionLists && (
<>
<WorkflowAiAgentPermissionsObjectsList
objects={filteredObjects}
onObjectClick={handleObjectClick}
readonly={readonly}
/>
<WorkflowAiAgentPermissionsFlagList
title={t`Actions`}
permissions={filteredActionPermissions}
enabledPermissionFlagKeys={permissionFlagKeys}
readonly={readonly}
onAddPermissionFlag={handleAddPermissionFlag}
/>
</>
)}
{shouldShowExistingPermissions && (
<>
<WorkflowAiAgentPermissionList
readonly={readonly}
objectPermissions={objectPermissions}
onDeletePermission={handleDeletePermission}
searchQuery={searchQuery}
/>
<WorkflowAiAgentPermissionsFlagList
title={t`Actions`}
permissions={filteredEnabledActionPermissions}
enabledPermissionFlagKeys={permissionFlagKeys}
readonly={readonly}
showDeleteButton={!readonly}
onDeletePermissionFlag={handleDeletePermissionFlag}
/>
</>
)}
</StyledContainer>
);
};
@@ -0,0 +1,84 @@
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { Select } from '@/ui/input/components/Select';
import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import {
type AgentResponseSchema,
type ModelConfiguration,
} from 'twenty-shared/ai';
import { type SelectOption } from 'twenty-ui/input';
import { SettingsAgentModelCapabilities } from '~/pages/settings/ai/components/SettingsAgentModelCapabilities';
import { SettingsAgentResponseFormat } from '~/pages/settings/ai/components/SettingsAgentResponseFormat';
type WorkflowAiAgentPromptTabProps = {
readonly: boolean;
aiModelOptions: SelectOption[];
onPromptChange: (value: string) => void;
onModelChange: (modelId: string) => void;
onModelConfigurationChange: (configuration: ModelConfiguration) => void;
onResponseFormatChange: (format: {
type: 'text' | 'json';
schema?: AgentResponseSchema;
}) => void;
};
export const WorkflowAiAgentPromptTab = ({
readonly,
aiModelOptions,
onPromptChange,
onModelChange,
onModelConfigurationChange,
onResponseFormatChange,
}: WorkflowAiAgentPromptTabProps) => {
const workflowAiAgentActionAgent = useRecoilValue(
workflowAiAgentActionAgentState,
);
return (
<>
<FormTextFieldInput
multiline
VariablePicker={WorkflowVariablePicker}
label={t`Instructions for AI`}
placeholder={t`Describe what you want the AI to do...`}
defaultValue={workflowAiAgentActionAgent?.prompt || ''}
onChange={onPromptChange}
readonly={readonly}
/>
{workflowAiAgentActionAgent ? (
<>
<Select
dropdownId="select-agent-model"
label={t`AI Model`}
options={aiModelOptions}
value={workflowAiAgentActionAgent.modelId}
onChange={onModelChange}
disabled={readonly}
/>
<SettingsAgentModelCapabilities
selectedModelId={workflowAiAgentActionAgent.modelId}
modelConfiguration={
workflowAiAgentActionAgent.modelConfiguration || {}
}
onConfigurationChange={onModelConfigurationChange}
disabled={readonly}
/>
<SettingsAgentResponseFormat
responseFormat={
workflowAiAgentActionAgent.responseFormat || {
type: 'text',
schema: {},
}
}
onResponseFormatChange={onResponseFormatChange}
disabled={readonly}
/>
</>
) : null}
</>
);
};
@@ -1,26 +1,42 @@
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { Select } from '@/ui/input/components/Select';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { t } from '@lingui/core/macro';
import { WorkflowAiAgentPermissionsTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowAiAgentPermissionsTab';
import { WORKFLOW_AI_AGENT_TABS } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/WorkflowAiAgentTabs';
import { useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose';
import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState';
import { workflowAiAgentPermissionsIsAddingPermissionState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsIsAddingPermissionState';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import {
type AgentResponseSchema,
type ModelConfiguration,
} from 'twenty-shared/ai';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconLock, IconSparkles } from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
import {
useFindOneAgentQuery,
useGetRolesQuery,
useUpdateOneAgentMutation,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
import { SettingsAgentModelCapabilities } from '~/pages/settings/ai/components/SettingsAgentModelCapabilities';
import { SettingsAgentResponseFormat } from '~/pages/settings/ai/components/SettingsAgentResponseFormat';
import { WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID } from '../constants/WorkflowAiAgentTabListComponentId';
import { WorkflowAiAgentPromptTab } from './WorkflowAiAgentPromptTab';
export type WorkflowAiAgentTabId =
(typeof WORKFLOW_AI_AGENT_TABS)[keyof typeof WORKFLOW_AI_AGENT_TABS];
type WorkflowEditActionAiAgentProps = {
action: WorkflowAiAgentAction;
@@ -32,72 +48,110 @@ type WorkflowEditActionAiAgentProps = {
};
};
const StyledTabList = styled(TabList)`
background-color: ${({ theme }) => theme.background.secondary};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledPermissionsStepBody = styled(WorkflowStepBody)`
padding-block: 0;
padding-inline: 0;
`;
export const WorkflowEditActionAiAgent = ({
action,
actionOptions,
}: WorkflowEditActionAiAgentProps) => {
const { t } = useLingui();
const componentInstanceId = `${WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID}-${action.id}`;
const agentId = action.settings.input.agentId;
const { data: agentData, loading: agentLoading } = useFindOneAgentQuery({
variables: { id: agentId || '' },
skip: !agentId,
});
const [workflowAiAgentActionAgent, setWorkflowAiAgentActionAgent] =
useRecoilState(workflowAiAgentActionAgentState);
const { loading: agentLoading, refetch: refetchAgent } = useFindOneAgentQuery(
{
variables: { id: agentId || '' },
skip: !agentId,
onCompleted: (data) => {
setWorkflowAiAgentActionAgent(data.findOneAgent);
},
},
);
useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose();
const [updateAgent] = useUpdateOneAgentMutation();
const aiModelOptions = useAiModelOptions();
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
const flow = useFlowOrThrow();
const agent = agentData?.findOneAgent;
const handleAgentPromptChange = useDebouncedCallback(
async (newPrompt: string) => {
if (actionOptions.readonly === true || !isDefined(agent)) {
if (
actionOptions.readonly === true ||
!isDefined(workflowAiAgentActionAgent)
) {
return;
}
await updateAgent({
const response = await updateAgent({
variables: {
input: {
id: agent.id,
id: workflowAiAgentActionAgent.id,
prompt: newPrompt,
},
},
refetchQueries: ['FindOneAgent'],
});
setWorkflowAiAgentActionAgent({
...workflowAiAgentActionAgent,
...response.data?.updateOneAgent,
});
},
500,
);
const handleAgentModelChange = async (modelId: string) => {
if (actionOptions.readonly === true || !isDefined(agent)) {
if (
actionOptions.readonly === true ||
!isDefined(workflowAiAgentActionAgent)
) {
return;
}
await updateAgent({
const response = await updateAgent({
variables: {
input: {
id: agent.id,
id: workflowAiAgentActionAgent.id,
modelId,
},
},
refetchQueries: ['FindOneAgent'],
});
setWorkflowAiAgentActionAgent({
...workflowAiAgentActionAgent,
...response.data?.updateOneAgent,
});
};
const handleModelConfigurationChange = async (
configuration: ModelConfiguration,
) => {
if (actionOptions.readonly === true || !isDefined(agent)) {
if (
actionOptions.readonly === true ||
!isDefined(workflowAiAgentActionAgent)
) {
return;
}
await updateAgent({
const response = await updateAgent({
variables: {
input: {
id: agent.id,
id: workflowAiAgentActionAgent.id,
modelConfiguration: configuration,
},
},
refetchQueries: ['FindOneAgent'],
});
setWorkflowAiAgentActionAgent({
...workflowAiAgentActionAgent,
...response.data?.updateOneAgent,
});
};
@@ -105,18 +159,25 @@ export const WorkflowEditActionAiAgent = ({
type: 'text' | 'json';
schema?: AgentResponseSchema;
}) => {
if (actionOptions.readonly === true || !isDefined(agent)) {
if (
actionOptions.readonly === true ||
!isDefined(workflowAiAgentActionAgent)
) {
return;
}
await updateAgent({
const response = await updateAgent({
variables: {
input: {
id: agent.id,
id: workflowAiAgentActionAgent.id,
responseFormat: format,
},
},
refetchQueries: ['FindOneAgent'],
});
setWorkflowAiAgentActionAgent({
...workflowAiAgentActionAgent,
...response.data?.updateOneAgent,
});
await updateWorkflowVersionStep({
@@ -130,11 +191,43 @@ export const WorkflowEditActionAiAgent = ({
300,
);
const tabs: SingleTabProps[] = [
{
id: WORKFLOW_AI_AGENT_TABS.PROMPT,
title: t`Prompt`,
Icon: IconSparkles,
},
{
id: WORKFLOW_AI_AGENT_TABS.PERMISSIONS,
title: t`Permissions`,
Icon: IconLock,
},
];
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
componentInstanceId,
);
const currentTabId =
(activeTabId as WorkflowAiAgentTabId) ?? WORKFLOW_AI_AGENT_TABS.PROMPT;
const navigateSettings = useNavigateSettings();
const { data: rolesData } = useGetRolesQuery();
const [
workflowAiAgentPermissionsIsAddingPermission,
setWorkflowAiAgentPermissionsIsAddingPermission,
] = useRecoilState(workflowAiAgentPermissionsIsAddingPermissionState);
const role = rolesData?.getRoles.find(
(item) => item.id === workflowAiAgentActionAgent?.roleId,
);
const handleAgentResponseFormatChange = async (format: {
type: 'text' | 'json';
schema?: AgentResponseSchema;
}) => {
if (format.type !== agent?.responseFormat?.type) {
if (format.type !== workflowAiAgentActionAgent?.responseFormat?.type) {
debouncedUpdateAgentResponseFormat.cancel();
void updateAgentResponseFormat(format);
} else {
@@ -142,50 +235,77 @@ export const WorkflowEditActionAiAgent = ({
}
};
const handleViewRole = () => {
if (isDefined(role?.id)) {
navigateSettings(SettingsPath.RoleDetail, { roleId: role.id });
}
};
const getFooterActions = () => {
if (currentTabId !== WORKFLOW_AI_AGENT_TABS.PERMISSIONS) {
return [];
}
if (workflowAiAgentPermissionsIsAddingPermission) {
return [
<CmdEnterActionButton
key="view-role"
title={t`View role`}
onClick={handleViewRole}
disabled={!isDefined(role?.id)}
/>,
];
}
if (isDefined(actionOptions.readonly)) {
return [];
}
return [
<CmdEnterActionButton
key="add-permission"
title={t`Add permission`}
onClick={() => setWorkflowAiAgentPermissionsIsAddingPermission(true)}
/>,
];
};
return agentLoading ? (
<RightDrawerSkeletonLoader />
) : (
<>
<WorkflowStepBody>
<FormTextFieldInput
multiline
VariablePicker={WorkflowVariablePicker}
label={t`Instructions for AI`}
placeholder={t`Describe what you want the AI to do...`}
defaultValue={agent?.prompt || ''}
onChange={handleAgentPromptChange}
readonly={actionOptions.readonly}
<StyledTabList
tabs={tabs}
componentInstanceId={componentInstanceId}
behaveAsLinks={false}
/>
{currentTabId === WORKFLOW_AI_AGENT_TABS.PERMISSIONS ? (
<StyledPermissionsStepBody>
<WorkflowAiAgentPermissionsTab
action={action}
readonly={actionOptions.readonly === true}
isAgentLoading={agentLoading}
refetchAgent={refetchAgent}
/>
</StyledPermissionsStepBody>
) : (
<WorkflowStepBody>
<WorkflowAiAgentPromptTab
readonly={actionOptions.readonly === true}
aiModelOptions={aiModelOptions}
onPromptChange={handleAgentPromptChange}
onModelChange={handleAgentModelChange}
onModelConfigurationChange={handleModelConfigurationChange}
onResponseFormatChange={handleAgentResponseFormatChange}
/>
</WorkflowStepBody>
)}
{!actionOptions.readonly && (
<WorkflowStepFooter
additionalActions={getFooterActions()}
stepId={action.id}
/>
{isDefined(agent) && (
<>
<Select
dropdownId="select-agent-model"
label={t`AI Model`}
options={aiModelOptions}
value={agent.modelId}
onChange={handleAgentModelChange}
disabled={actionOptions.readonly}
/>
<SettingsAgentModelCapabilities
selectedModelId={agent.modelId}
modelConfiguration={agent.modelConfiguration || {}}
onConfigurationChange={handleModelConfigurationChange}
disabled={actionOptions.readonly}
/>
<SettingsAgentResponseFormat
responseFormat={
agent.responseFormat || { type: 'text', schema: {} }
}
onResponseFormatChange={handleAgentResponseFormatChange}
disabled={actionOptions.readonly}
/>
</>
)}
</WorkflowStepBody>
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
)}
</>
);
};
@@ -0,0 +1,40 @@
import type { PermissionFlagType } from '~/generated-metadata/graphql';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
export type FilteredPermissionsResult<T> = {
filteredPermissions: T[];
filteredEnabledPermissions: T[];
};
export const getFilteredPermissions = <
T extends { key: PermissionFlagType; name: string },
>({
permissions,
permissionFlagKeys,
searchQuery,
}: {
permissions: T[];
permissionFlagKeys: PermissionFlagType[];
searchQuery: string;
}): FilteredPermissionsResult<T> => {
const filteredPermissions = filterBySearchQuery<T>({
items: permissions,
searchQuery,
getSearchableValues: (permission) => [permission.name],
});
const enabledPermissions = permissions.filter((permission) =>
permissionFlagKeys.includes(permission.key),
);
const filteredEnabledPermissions = filterBySearchQuery<T>({
items: enabledPermissions,
searchQuery,
getSearchableValues: (permission) => [permission.name],
});
return {
filteredPermissions,
filteredEnabledPermissions,
};
};
@@ -0,0 +1,23 @@
import { type SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/SettingsRoleObjectPermissionIconConfig';
export const CRUD_PERMISSIONS: Array<{
key: SettingsRoleObjectPermissionKey;
label: (objectLabel: string) => string;
}> = [
{
key: 'canReadObjectRecords',
label: (objectLabel: string) => `See ${objectLabel}`,
},
{
key: 'canUpdateObjectRecords',
label: (objectLabel: string) => `Edit ${objectLabel}`,
},
{
key: 'canSoftDeleteObjectRecords',
label: (objectLabel: string) => `Delete ${objectLabel}`,
},
{
key: 'canDestroyObjectRecords',
label: (objectLabel: string) => `Destroy ${objectLabel}`,
},
];
@@ -0,0 +1,2 @@
export const WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID =
'workflow-ai-agent-tab-list';
@@ -0,0 +1,4 @@
export const WORKFLOW_AI_AGENT_TABS = {
PROMPT: 'prompt',
PERMISSIONS: 'permissions',
} as const;
@@ -0,0 +1,27 @@
import { useListenToSidePanelClosing } from '@/ui/layout/right-drawer/hooks/useListenToSidePanelClosing';
import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState';
import { workflowAiAgentPermissionsIsAddingPermissionState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsIsAddingPermissionState';
import { workflowAiAgentPermissionsSelectedObjectIdState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsSelectedObjectIdState';
import { useResetRecoilState, useSetRecoilState } from 'recoil';
export const useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose = () => {
const resetSelectedObjectId = useResetRecoilState(
workflowAiAgentPermissionsSelectedObjectIdState,
);
const setIsAddingPermission = useSetRecoilState(
workflowAiAgentPermissionsIsAddingPermissionState,
);
const resetAgentState = useResetRecoilState(workflowAiAgentActionAgentState);
const resetPermissionState = () => {
resetSelectedObjectId();
setIsAddingPermission(false);
resetAgentState();
};
useListenToSidePanelClosing(resetPermissionState);
return {
resetPermissionState,
};
};
@@ -0,0 +1,365 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/SettingsRoleObjectPermissionIconConfig';
import { useActionRolePermissionFlagConfig } from '@/settings/roles/role-permissions/permission-flags/hooks/useActionRolePermissionFlagConfig';
import { useSettingsRolePermissionFlagConfig } from '@/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { CRUD_PERMISSIONS } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/WorkflowAiAgentCrudPermissions';
import { workflowAiAgentActionAgentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentActionAgentState';
import { workflowAiAgentPermissionsIsAddingPermissionState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsIsAddingPermissionState';
import { workflowAiAgentPermissionsSelectedObjectIdState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/workflowAiAgentPermissionsSelectedObjectIdState';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
useAssignRoleToAgentMutation,
useCreateOneRoleMutation,
useUpsertObjectPermissionsMutation,
useUpsertPermissionFlagsMutation,
type Agent,
type ObjectPermission,
type PermissionFlagType,
} from '~/generated-metadata/graphql';
type UseWorkflowAiAgentPermissionActionsParams = {
readonly: boolean;
objectPermissions: ObjectPermission[];
permissionFlagKeys: PermissionFlagType[];
refetchAgentAndRoles: () => Promise<{ refetchedAgent?: Agent }>;
};
export const useWorkflowAiAgentPermissionActions = ({
readonly,
objectPermissions,
permissionFlagKeys,
refetchAgentAndRoles,
}: UseWorkflowAiAgentPermissionActionsParams) => {
const { enqueueSuccessSnackBar } = useSnackBar();
const [workflowAiAgentActionAgent, setWorkflowAiAgentActionAgent] =
useRecoilState(workflowAiAgentActionAgentState);
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const settingsPermissionsConfig = useSettingsRolePermissionFlagConfig({
assignmentCapabilities: { canBeAssignedToAgents: true },
});
const actionPermissionsConfig = useActionRolePermissionFlagConfig({
assignmentCapabilities: { canBeAssignedToAgents: true },
});
const [, setWorkflowAiAgentPermissionsSelectedObjectId] = useRecoilState(
workflowAiAgentPermissionsSelectedObjectIdState,
);
const [, setWorkflowAiAgentPermissionsIsAddingPermission] = useRecoilState(
workflowAiAgentPermissionsIsAddingPermissionState,
);
const [createRole] = useCreateOneRoleMutation();
const [assignRoleToAgent] = useAssignRoleToAgentMutation();
const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation();
const [upsertPermissionFlags] = useUpsertPermissionFlagsMutation();
const roleId = workflowAiAgentActionAgent?.roleId;
const permissionFlagLabelMap = useMemo(
() =>
[...settingsPermissionsConfig, ...actionPermissionsConfig].reduce<
Partial<Record<PermissionFlagType, string>>
>((acc, permission) => {
acc[permission.key] = permission.name;
return acc;
}, {}),
[settingsPermissionsConfig, actionPermissionsConfig],
);
const ensureRoleId = async (): Promise<string | undefined> => {
if (isDefined(roleId)) {
return roleId;
}
if (!isDefined(workflowAiAgentActionAgent) || readonly) {
return undefined;
}
const agentDisplayName =
workflowAiAgentActionAgent.label ??
workflowAiAgentActionAgent.name ??
t`Agent`;
const roleName = `${agentDisplayName} role (${workflowAiAgentActionAgent.id.substring(0, 8)})`;
const generatedRoleId = v4();
await createRole({
variables: {
createRoleInput: {
id: generatedRoleId,
label: roleName,
description: t`Auto-generated role for ${agentDisplayName}`,
icon: 'IconRobot',
canAccessAllTools: false,
canUpdateAllSettings: false,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToAgents: true,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
},
},
});
await assignRoleToAgent({
variables: {
agentId: workflowAiAgentActionAgent.id,
roleId: generatedRoleId,
},
});
await refetchAgentAndRoles();
return generatedRoleId;
};
const handleAddPermission = async (
objectMetadataId: string,
permissionKey: SettingsRoleObjectPermissionKey,
) => {
const ensuredRoleId = await ensureRoleId();
if (!ensuredRoleId) {
return;
}
const objectPermission = objectPermissions.find(
(permission) => permission.objectMetadataId === objectMetadataId,
);
const updatedPermission: ObjectPermission = {
objectMetadataId,
canReadObjectRecords:
permissionKey === 'canReadObjectRecords'
? true
: (objectPermission?.canReadObjectRecords ??
(permissionKey === 'canUpdateObjectRecords' ||
permissionKey === 'canSoftDeleteObjectRecords' ||
permissionKey === 'canDestroyObjectRecords'
? true
: false)),
canUpdateObjectRecords:
permissionKey === 'canUpdateObjectRecords'
? true
: (objectPermission?.canUpdateObjectRecords ?? false),
canSoftDeleteObjectRecords:
permissionKey === 'canSoftDeleteObjectRecords'
? true
: (objectPermission?.canSoftDeleteObjectRecords ?? false),
canDestroyObjectRecords:
permissionKey === 'canDestroyObjectRecords'
? true
: (objectPermission?.canDestroyObjectRecords ?? false),
};
const allObjectPermissions = objectPermissions
.filter((permission) => permission.objectMetadataId !== objectMetadataId)
.map((permission) => ({
objectMetadataId: permission.objectMetadataId,
canReadObjectRecords: permission.canReadObjectRecords ?? false,
canUpdateObjectRecords: permission.canUpdateObjectRecords ?? false,
canSoftDeleteObjectRecords:
permission.canSoftDeleteObjectRecords ?? false,
canDestroyObjectRecords: permission.canDestroyObjectRecords ?? false,
}));
allObjectPermissions.push({
objectMetadataId: updatedPermission.objectMetadataId,
canReadObjectRecords: updatedPermission.canReadObjectRecords ?? false,
canUpdateObjectRecords: updatedPermission.canUpdateObjectRecords ?? false,
canSoftDeleteObjectRecords:
updatedPermission.canSoftDeleteObjectRecords ?? false,
canDestroyObjectRecords:
updatedPermission.canDestroyObjectRecords ?? false,
});
await upsertObjectPermissions({
variables: {
upsertObjectPermissionsInput: {
roleId: ensuredRoleId,
objectPermissions: allObjectPermissions,
},
},
});
await refetchAgentAndRoles();
setWorkflowAiAgentPermissionsIsAddingPermission(false);
setWorkflowAiAgentPermissionsSelectedObjectId(undefined);
};
const handleDeletePermission = async (
objectMetadataId: string,
permissionKey: SettingsRoleObjectPermissionKey,
) => {
if (!isDefined(roleId) || readonly) {
return;
}
const objectPermission = objectPermissions.find(
(permission) => permission.objectMetadataId === objectMetadataId,
);
if (!isDefined(objectPermission)) {
return;
}
const objectMetadata = objectMetadataItems.find(
(item) => item.id === objectMetadataId,
);
if (!isDefined(objectMetadata)) {
return;
}
const updatedPermission: ObjectPermission = {
objectMetadataId,
canReadObjectRecords:
permissionKey === 'canReadObjectRecords'
? false
: (objectPermission.canReadObjectRecords ?? false),
canUpdateObjectRecords:
permissionKey === 'canUpdateObjectRecords'
? false
: (objectPermission.canUpdateObjectRecords ?? false),
canSoftDeleteObjectRecords:
permissionKey === 'canSoftDeleteObjectRecords'
? false
: (objectPermission.canSoftDeleteObjectRecords ?? false),
canDestroyObjectRecords:
permissionKey === 'canDestroyObjectRecords'
? false
: (objectPermission.canDestroyObjectRecords ?? false),
};
if (permissionKey === 'canReadObjectRecords') {
updatedPermission.canUpdateObjectRecords = false;
updatedPermission.canSoftDeleteObjectRecords = false;
updatedPermission.canDestroyObjectRecords = false;
}
const allObjectPermissions = objectPermissions
.filter((permission) => permission.objectMetadataId !== objectMetadataId)
.map((permission) => ({
objectMetadataId: permission.objectMetadataId,
canReadObjectRecords: permission.canReadObjectRecords ?? false,
canUpdateObjectRecords: permission.canUpdateObjectRecords ?? false,
canSoftDeleteObjectRecords:
permission.canSoftDeleteObjectRecords ?? false,
canDestroyObjectRecords: permission.canDestroyObjectRecords ?? false,
}));
allObjectPermissions.push({
objectMetadataId: updatedPermission.objectMetadataId,
canReadObjectRecords: updatedPermission.canReadObjectRecords ?? false,
canUpdateObjectRecords: updatedPermission.canUpdateObjectRecords ?? false,
canSoftDeleteObjectRecords:
updatedPermission.canSoftDeleteObjectRecords ?? false,
canDestroyObjectRecords:
updatedPermission.canDestroyObjectRecords ?? false,
});
await upsertObjectPermissions({
variables: {
upsertObjectPermissionsInput: {
roleId,
objectPermissions: allObjectPermissions,
},
},
});
await refetchAgentAndRoles();
const permissionLabel = CRUD_PERMISSIONS.find(
(permission) => permission.key === permissionKey,
)?.label(objectMetadata.labelPlural);
if (isDefined(permissionLabel)) {
enqueueSuccessSnackBar({
message: t`${permissionLabel} Permission removed`,
});
}
};
const handleAddPermissionFlag = async (
permissionFlagKey: PermissionFlagType,
) => {
if (readonly) {
return;
}
const hadRoleBefore = isDefined(roleId);
const ensuredRoleId = await ensureRoleId();
if (!ensuredRoleId || permissionFlagKeys.includes(permissionFlagKey)) {
return;
}
await upsertPermissionFlags({
variables: {
upsertPermissionFlagsInput: {
roleId: ensuredRoleId,
permissionFlagKeys: [...permissionFlagKeys, permissionFlagKey],
},
},
});
const { refetchedAgent } = await refetchAgentAndRoles();
if (
!hadRoleBefore &&
isDefined(refetchedAgent) &&
isDefined(setWorkflowAiAgentActionAgent)
) {
setWorkflowAiAgentActionAgent(refetchedAgent);
}
setWorkflowAiAgentPermissionsIsAddingPermission(false);
setWorkflowAiAgentPermissionsSelectedObjectId(undefined);
};
const handleDeletePermissionFlag = async (
permissionFlagKey: PermissionFlagType,
) => {
if (!isDefined(roleId) || readonly) {
return;
}
if (!permissionFlagKeys.includes(permissionFlagKey)) {
return;
}
const permissionFlagKeysAfterRemoval = permissionFlagKeys.filter(
(flag) => flag !== permissionFlagKey,
);
await upsertPermissionFlags({
variables: {
upsertPermissionFlagsInput: {
roleId,
permissionFlagKeys: permissionFlagKeysAfterRemoval,
},
},
});
await refetchAgentAndRoles();
const permissionLabel = permissionFlagLabelMap[permissionFlagKey];
if (isDefined(permissionLabel)) {
enqueueSuccessSnackBar({
message: t`${permissionLabel} permission removed`,
});
}
};
return {
handleAddPermission,
handleDeletePermission,
handleAddPermissionFlag,
handleDeletePermissionFlag,
};
};
@@ -0,0 +1,7 @@
import { atom } from 'recoil';
import { type Agent } from '~/generated-metadata/graphql';
export const workflowAiAgentActionAgentState = atom<Agent | undefined>({
key: 'workflowAiAgentActionAgentState',
default: undefined,
});
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const workflowAiAgentPermissionsIsAddingPermissionState = atom<boolean>({
key: 'workflowAiAgentPermissionsIsAddingPermissionState',
default: false,
});
@@ -0,0 +1,8 @@
import { atom } from 'recoil';
export const workflowAiAgentPermissionsSelectedObjectIdState = atom<
string | undefined
>({
key: 'workflowAiAgentPermissionsSelectedObjectIdState',
default: undefined,
});
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { In, IsNull, Not, Repository } from 'typeorm';
import {
@@ -174,4 +175,39 @@ export class AiAgentRoleService {
roleToAssignIsSameAsCurrentRole: Boolean(existingRoleTarget),
};
}
public async deleteAgentOnlyRoleIfUnused({
roleId,
roleTargetId,
workspaceId,
}: {
roleId: string;
roleTargetId: string;
workspaceId: string;
}): Promise<void> {
const role = await this.roleRepository.findOne({
where: { id: roleId, workspaceId },
});
if (
!isDefined(role) ||
!role.canBeAssignedToAgents ||
role.canBeAssignedToUsers ||
role.canBeAssignedToApiKeys
) {
return;
}
const remainingAssignments = await this.roleTargetsRepository.count({
where: {
roleId,
workspaceId,
id: Not(roleTargetId),
},
});
if (remainingAssignments === 0) {
await this.roleRepository.delete({ id: roleId, workspaceId });
}
}
}
@@ -1,8 +1,11 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
@@ -21,8 +24,11 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let serverlessFunctionService: jest.Mocked<ServerlessFunctionService>;
let agentRepository: jest.Mocked<any>;
let roleTargetsRepository: jest.Mocked<any>;
let roleRepository: jest.Mocked<any>;
let objectMetadataRepository: jest.Mocked<any>;
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
beforeEach(async () => {
serverlessFunctionService = {
@@ -38,6 +44,16 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
delete: jest.fn(),
};
roleTargetsRepository = {
findOne: jest.fn(),
count: jest.fn(),
};
roleRepository = {
findOne: jest.fn(),
delete: jest.fn(),
};
objectMetadataRepository = {
findOne: jest.fn(),
};
@@ -46,6 +62,10 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
getObjectMetadataItemWithFieldsMaps: jest.fn(),
} as unknown as jest.Mocked<WorkflowCommonWorkspaceService>;
aiAgentRoleService = {
deleteAgentOnlyRoleIfUnused: jest.fn(),
} as unknown as jest.Mocked<AiAgentRoleService>;
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn(),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
@@ -65,6 +85,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: getRepositoryToken(AgentEntity),
useValue: agentRepository,
},
{
provide: getRepositoryToken(RoleTargetsEntity),
useValue: roleTargetsRepository,
},
{
provide: getRepositoryToken(RoleEntity),
useValue: roleRepository,
},
{
provide: getRepositoryToken(ObjectMetadataEntity),
useValue: objectMetadataRepository,
@@ -73,6 +101,10 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: WorkflowCommonWorkspaceService,
useValue: workflowCommonWorkspaceService,
},
{
provide: AiAgentRoleService,
useValue: aiAgentRoleService,
},
{
provide: ScopedWorkspaceContextFactory,
useValue: {},
@@ -154,6 +186,50 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
workspaceId: mockWorkspaceId,
});
});
it('should delete attached role when it is agent-only and unassigned elsewhere', async () => {
const step = {
id: 'step-id',
name: 'AI Agent Step',
type: WorkflowActionType.AI_AGENT,
valid: true,
nextStepIds: [],
settings: {
input: {
agentId: 'agent-id',
prompt: '',
},
outputSchema: {},
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
},
} as unknown as WorkflowAction;
agentRepository.findOne.mockResolvedValue({ id: 'agent-id' });
roleTargetsRepository.findOne.mockResolvedValue({
id: 'role-target-id',
roleId: 'role-id',
});
await service.runWorkflowVersionStepDeletionSideEffects({
step,
workspaceId: mockWorkspaceId,
});
expect(agentRepository.delete).toHaveBeenCalledWith({
id: 'agent-id',
workspaceId: mockWorkspaceId,
});
expect(
aiAgentRoleService.deleteAgentOnlyRoleIfUnused,
).toHaveBeenCalledWith({
roleId: 'role-id',
roleTargetId: 'role-target-id',
workspaceId: mockWorkspaceId,
});
});
});
describe('runStepCreationSideEffectsAndBuildStep', () => {
@@ -9,9 +9,11 @@ import { v4 } from 'uuid';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
@@ -53,9 +55,12 @@ export class WorkflowVersionStepOperationsWorkspaceService {
private readonly serverlessFunctionService: ServerlessFunctionService,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly aiAgentRoleService: AiAgentRoleService,
) {}
async runWorkflowVersionStepDeletionSideEffects({
@@ -90,7 +95,22 @@ export class WorkflowVersionStepOperationsWorkspaceService {
});
if (isDefined(agent)) {
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
agentId: agent.id,
workspaceId,
},
});
await this.agentRepository.delete({ id: agent.id, workspaceId });
if (isDefined(roleTarget?.roleId) && isDefined(roleTarget?.id)) {
await this.aiAgentRoleService.deleteAgentOnlyRoleIfUnused({
roleId: roleTarget.roleId,
roleTargetId: roleTarget.id,
workspaceId,
});
}
}
break;
}
@@ -2,8 +2,11 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
@@ -19,7 +22,13 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
WorkflowSchemaModule,
ServerlessFunctionModule,
WorkflowCommonModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity, AgentEntity]),
AiAgentRoleModule,
NestjsQueryTypeOrmModule.forFeature([
ObjectMetadataEntity,
AgentEntity,
RoleTargetsEntity,
RoleEntity,
]),
],
providers: [
WorkflowVersionStepWorkspaceService,