feat: workflow agent node permissions tab (#16092)
This commit is contained in:
+9
-191
@@ -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 ||
|
||||
|
||||
+7
-157
@@ -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 =
|
||||
|
||||
+184
@@ -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,
|
||||
]);
|
||||
};
|
||||
+214
@@ -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,
|
||||
]);
|
||||
};
|
||||
+3
-1
@@ -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={({
|
||||
|
||||
+57
-1
@@ -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
|
||||
|
||||
+37
-1
@@ -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 {
|
||||
|
||||
+120
@@ -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>
|
||||
);
|
||||
};
|
||||
+49
@@ -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>
|
||||
);
|
||||
};
|
||||
+61
@@ -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>
|
||||
);
|
||||
};
|
||||
+60
@@ -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>
|
||||
);
|
||||
};
|
||||
+44
@@ -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>
|
||||
);
|
||||
};
|
||||
+34
@@ -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>
|
||||
);
|
||||
};
|
||||
+67
@@ -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>
|
||||
);
|
||||
};
|
||||
+64
@@ -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};
|
||||
`;
|
||||
+264
@@ -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>
|
||||
);
|
||||
};
|
||||
+84
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+188
-68
@@ -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} />}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+40
@@ -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,
|
||||
};
|
||||
};
|
||||
+23
@@ -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}`,
|
||||
},
|
||||
];
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID =
|
||||
'workflow-ai-agent-tab-list';
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const WORKFLOW_AI_AGENT_TABS = {
|
||||
PROMPT: 'prompt',
|
||||
PERMISSIONS: 'permissions',
|
||||
} as const;
|
||||
+27
@@ -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,
|
||||
};
|
||||
};
|
||||
+365
@@ -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,
|
||||
};
|
||||
};
|
||||
+7
@@ -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,
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const workflowAiAgentPermissionsIsAddingPermissionState = atom<boolean>({
|
||||
key: 'workflowAiAgentPermissionsIsAddingPermissionState',
|
||||
default: false,
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const workflowAiAgentPermissionsSelectedObjectIdState = atom<
|
||||
string | undefined
|
||||
>({
|
||||
key: 'workflowAiAgentPermissionsSelectedObjectIdState',
|
||||
default: undefined,
|
||||
});
|
||||
+36
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+76
@@ -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', () => {
|
||||
|
||||
+20
@@ -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;
|
||||
}
|
||||
|
||||
+10
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user