diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx
index ba2e139694..e4765822ed 100644
--- a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx
@@ -6,51 +6,18 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsRoleDefaultRole } from '@/settings/roles/components/SettingsRolesDefaultRole';
import { SettingsRolesList } from '@/settings/roles/components/SettingsRolesList';
-import { ROLES_LIST_TABS } from '@/settings/roles/constants/RolesListTabs';
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
-import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
-import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
-import { H3Title, IconUser, IconRobot, IconKey } from 'twenty-ui/display';
-import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
-import { FeatureFlagKey } from '~/generated/graphql';
-import { TabList } from '@/ui/layout/tab-list/components/TabList';
+import { H3Title } from 'twenty-ui/display';
export const SettingsRolesContainer = () => {
const { t } = useLingui();
- const activeTabId = useRecoilComponentValue(
- activeTabIdComponentState,
- ROLES_LIST_TABS.COMPONENT_INSTANCE_ID,
- );
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
- const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
-
- const tabs = [
- {
- id: ROLES_LIST_TABS.TABS_IDS.USER_ROLES,
- title: t`User Roles`,
- Icon: IconUser,
- },
- ...(isAiEnabled
- ? [
- {
- id: ROLES_LIST_TABS.TABS_IDS.AGENT_ROLES,
- title: t`Agent Roles`,
- Icon: IconRobot,
- },
- ]
- : []),
- {
- id: ROLES_LIST_TABS.TABS_IDS.API_KEY_ROLES,
- title: t`API Key Roles`,
- Icon: IconKey,
- },
- ];
if (settingsRolesIsLoading && !settingsAllRoles) {
return null;
@@ -68,15 +35,8 @@ export const SettingsRolesContainer = () => {
]}
>
-
- {activeTabId === ROLES_LIST_TABS.TABS_IDS.USER_ROLES && (
-
- )}
+
);
diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx
index 3c494b3ff2..48f6ed6721 100644
--- a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx
@@ -1,23 +1,31 @@
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { SettingsRolesTableHeader } from '@/settings/roles/components/SettingsRolesTableHeader';
import { SettingsRolesTableRow } from '@/settings/roles/components/SettingsRolesTableRow';
-import { ROLES_LIST_TABS } from '@/settings/roles/constants/RolesListTabs';
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
-import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
+import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TableCell } from '@/ui/layout/table/components/TableCell';
-import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
-import { H2Title, IconPlus, IconSearch } from 'twenty-ui/display';
+import {
+ H2Title,
+ IconFilter,
+ IconKey,
+ IconPlus,
+ IconRobot,
+ IconSearch,
+} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
+import { MenuItemToggle } from 'twenty-ui/navigation';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { sortByAscString } from '~/utils/array/sortByAscString';
-import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
-import { useState } from 'react';
const StyledCreateRoleSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
@@ -36,19 +44,22 @@ const StyledNoRoles = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
-const StyledSearchInput = styled(SettingsTextInput)`
+const StyledSearchAndFilterContainer = styled.div`
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
+const StyledSearchInput = styled(SettingsTextInput)`
+ flex: 1;
+`;
+
export const SettingsRolesList = () => {
const navigateSettings = useNavigateSettings();
- const activeTabId = useRecoilComponentValue(
- activeTabIdComponentState,
- ROLES_LIST_TABS.COMPONENT_INSTANCE_ID,
- );
-
const [searchTerm, setSearchTerm] = useState('');
+ const [showAgentRoles, setShowAgentRoles] = useState(false);
+ const [showApiKeyRoles, setShowApiKeyRoles] = useState(false);
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
@@ -57,52 +68,69 @@ export const SettingsRolesList = () => {
);
const filteredRoles = sortedSettingsAllRoles.filter((role) => {
- let matchesTab = false;
+ const matchesType =
+ role.canBeAssignedToUsers ||
+ (showAgentRoles && role.canBeAssignedToAgents) ||
+ (showApiKeyRoles && role.canBeAssignedToApiKeys);
- switch (activeTabId) {
- case ROLES_LIST_TABS.TABS_IDS.USER_ROLES:
- matchesTab = role.canBeAssignedToUsers;
- break;
- case ROLES_LIST_TABS.TABS_IDS.AGENT_ROLES:
- matchesTab = role.canBeAssignedToAgents;
- break;
- case ROLES_LIST_TABS.TABS_IDS.API_KEY_ROLES:
- matchesTab = role.canBeAssignedToApiKeys;
- break;
- default:
- matchesTab = role.canBeAssignedToUsers;
- }
+ const matchesSearch = role.label
+ ?.toLowerCase()
+ .includes(searchTerm.toLowerCase());
- return (
- matchesTab && role.label?.toLowerCase().includes(searchTerm.toLowerCase())
- );
+ return matchesType && matchesSearch;
});
- const tabDescriptions: Record = {
- [ROLES_LIST_TABS.TABS_IDS.USER_ROLES]:
- t`Assign roles to specify each member's access permissions`,
- [ROLES_LIST_TABS.TABS_IDS.AGENT_ROLES]:
- t`Assign roles to specify each agent's access permissions`,
- [ROLES_LIST_TABS.TABS_IDS.API_KEY_ROLES]:
- t`Assign roles to specify each API key's access permissions`,
- };
-
- const description =
- (activeTabId && tabDescriptions[activeTabId]) ??
- t`Assign roles to specify each member's access permissions`;
-
return (
-
-
-
+
+
+
+ }
+ dropdownComponents={
+
+
+ setShowAgentRoles(!showAgentRoles)}
+ toggled={showAgentRoles}
+ text={t`Agent roles`}
+ toggleSize="small"
+ />
+ setShowApiKeyRoles(!showApiKeyRoles)}
+ toggled={showApiKeyRoles}
+ text={t`API key roles`}
+ toggleSize="small"
+ />
+
+
+ }
+ />
+
+
diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx
index bf5a323e5f..97612c3baa 100644
--- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx
@@ -13,11 +13,13 @@ const StyledRolePermissionsContainer = styled.div`
type SettingsRolePermissionsProps = {
roleId: string;
isEditable: boolean;
+ fromAgentId?: string;
};
export const SettingsRolePermissions = ({
roleId,
isEditable,
+ fromAgentId,
}: SettingsRolePermissionsProps) => {
return (
@@ -27,7 +29,7 @@ export const SettingsRolePermissions = ({
/>
{
const theme = useTheme();
const navigate = useNavigateSettings();
+ const [searchParams] = useSearchParams();
+ const fromAgentId = searchParams.get('fromAgent');
const [searchFilter, setSearchFilter] = useState('');
const { objectMetadataItemsThatCanHavePermission } =
@@ -64,10 +67,11 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
};
const handleSelectObjectMetadata = (objectMetadataId: string) => {
- navigate(SettingsPath.RoleObjectLevel, {
- roleId,
- objectMetadataId,
- });
+ navigate(
+ SettingsPath.RoleObjectLevel,
+ { roleId, objectMetadataId },
+ fromAgentId ? { fromAgent: fromAgentId } : undefined,
+ );
};
const { filterObjectMetadataItemsWithPermissionOverride } =
diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx
index d36717b028..c9450ce938 100644
--- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx
@@ -2,10 +2,12 @@ import { SettingsRolePermissionsObjectLevelTableHeader } from '@/settings/roles/
import { SettingsRolePermissionsObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow';
import { useFilterObjectMetadataItemsWithPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useFilterObjectWithPermissionOverride';
import { useObjectMetadataItemsThatCanHavePermission } from '@/settings/roles/role-permissions/object-level-permissions/hooks/useObjectMetadataItemsThatCanHavePermission';
+import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
+import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
@@ -28,7 +30,7 @@ const StyledTableRows = styled.div`
type SettingsRolePermissionsObjectLevelSectionProps = {
roleId: string;
- isEditable: boolean;
+ fromAgentId?: string;
};
const StyledNoOverride = styled(TableCell)`
@@ -37,10 +39,14 @@ const StyledNoOverride = styled(TableCell)`
export const SettingsRolePermissionsObjectLevelSection = ({
roleId,
- isEditable,
+ fromAgentId,
}: SettingsRolePermissionsObjectLevelSectionProps) => {
const navigateSettings = useNavigateSettings();
+ const settingsDraftRole = useRecoilValue(
+ settingsDraftRoleFamilyState(roleId),
+ );
+
const { objectMetadataItemsThatCanHavePermission } =
useObjectMetadataItemsThatCanHavePermission();
@@ -59,9 +65,11 @@ export const SettingsRolePermissionsObjectLevelSection = ({
objectMetadataItemsThatCanHavePermission.length;
const handleAddRule = () => {
- navigateSettings(SettingsPath.RoleAddObjectLevel, {
- roleId,
- });
+ navigateSettings(
+ SettingsPath.RoleAddObjectLevel,
+ { roleId },
+ fromAgentId ? { fromAgent: fromAgentId } : undefined,
+ );
};
const hasObjectPermissions =
@@ -82,6 +90,7 @@ export const SettingsRolePermissionsObjectLevelSection = ({
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
roleId={roleId}
+ fromAgentId={fromAgentId}
/>
),
)
@@ -98,7 +107,9 @@ export const SettingsRolePermissionsObjectLevelSection = ({
title={t`Add rule`}
variant="secondary"
size="small"
- disabled={!isEditable || allObjectsHaveSetPermission}
+ disabled={
+ !settingsDraftRole.isEditable || allObjectsHaveSetPermission
+ }
onClick={handleAddRule}
/>
diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx
index 1e914f6dba..a629a6860e 100644
--- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx
@@ -29,11 +29,13 @@ const StyledNameLabel = styled.div`
type SettingsRolePermissionsObjectLevelTableRowProps = {
objectMetadataItem: ObjectMetadataItem;
roleId: string;
+ fromAgentId?: string;
};
export const SettingsRolePermissionsObjectLevelTableRow = ({
objectMetadataItem,
roleId,
+ fromAgentId,
}: SettingsRolePermissionsObjectLevelTableRowProps) => {
const { getIcon } = useIcons();
const theme = useTheme();
@@ -42,12 +44,18 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
const objectLabelPlural = objectMetadataItem.labelPlural;
+ const navigationPath = getSettingsPath(SettingsPath.RoleObjectLevel, {
+ roleId: roleId,
+ objectMetadataId: objectMetadataItem.id,
+ });
+
+ const navigationUrl = fromAgentId
+ ? `${navigationPath}?fromAgent=${fromAgentId}`
+ : navigationPath;
+
return (
diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx
index 77e2ead965..36bd051c69 100644
--- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx
@@ -5,10 +5,12 @@ import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/setti
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { t } from '@lingui/core/macro';
+import { useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath } from 'twenty-shared/utils';
+import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';
+import { useFindOneAgentQuery } from '~/generated-metadata/graphql';
type SettingsRolePermissionsObjectLevelObjectFormProps = {
roleId: string;
@@ -19,10 +21,18 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
roleId,
objectMetadataId,
}: SettingsRolePermissionsObjectLevelObjectFormProps) => {
+ const [searchParams] = useSearchParams();
+ const fromAgentId = searchParams.get('fromAgent');
+
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
+ const { data: agentData } = useFindOneAgentQuery({
+ variables: { id: fromAgentId || '' },
+ skip: !fromAgentId,
+ });
+
const objectMetadata = useObjectMetadataItemById({
objectId: objectMetadataId,
});
@@ -32,37 +42,65 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
const objectLabelSingular = objectMetadataItem.labelSingular;
const objectLabelPlural = objectMetadataItem.labelPlural;
+ const agent = agentData?.findOneAgent;
+
+ const breadcrumbLinks =
+ fromAgentId && isDefined(agent)
+ ? [
+ {
+ children: t`Workspace`,
+ href: getSettingsPath(SettingsPath.Workspace),
+ },
+ {
+ children: t`AI`,
+ href: getSettingsPath(SettingsPath.AI),
+ },
+ {
+ children: agent.label,
+ href: getSettingsPath(SettingsPath.AIAgentDetail, {
+ agentId: agent.id,
+ }),
+ },
+ {
+ children: t`Permissions · ${objectLabelSingular}`,
+ },
+ ]
+ : [
+ {
+ children: t`Workspace`,
+ href: getSettingsPath(SettingsPath.Workspace),
+ },
+ {
+ children: t`Roles`,
+ href: getSettingsPath(SettingsPath.Roles),
+ },
+ {
+ children: settingsDraftRole.label,
+ href: getSettingsPath(SettingsPath.RoleDetail, {
+ roleId,
+ }),
+ },
+ {
+ children: t`Permissions · ${objectLabelSingular}`,
+ },
+ ];
+
+ const finishButtonPath =
+ fromAgentId && isDefined(agent)
+ ? getSettingsPath(SettingsPath.AIAgentDetail, { agentId: agent.id })
+ : getSettingsPath(SettingsPath.RoleDetail, { roleId });
+
return (
}
>
diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx
index 047bef5a57..3eacc4bc5b 100644
--- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx
+++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx
@@ -20,7 +20,7 @@ import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
-import { isDefined, getSettingsPath } from 'twenty-shared/utils';
+import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui/display';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@@ -60,6 +60,11 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const { saveDraftRoleToDB } = useSaveDraftRoleToDB({
isCreateMode,
roleId,
+ onSuccess: async (savedRoleId) => {
+ if (isCreateMode) {
+ navigateSettings(SettingsPath.RoleDetail, { roleId: savedRoleId });
+ }
+ },
});
const isRoleEditable = settingsDraftRole.isEditable;
diff --git a/packages/twenty-front/src/modules/settings/roles/role/hooks/useSaveDraftRoleToDB.ts b/packages/twenty-front/src/modules/settings/roles/role/hooks/useSaveDraftRoleToDB.ts
index 174a25767f..05d4655df5 100644
--- a/packages/twenty-front/src/modules/settings/roles/role/hooks/useSaveDraftRoleToDB.ts
+++ b/packages/twenty-front/src/modules/settings/roles/role/hooks/useSaveDraftRoleToDB.ts
@@ -8,7 +8,6 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
-import { SettingsPath } from 'twenty-shared/types';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import {
useCreateOneRoleMutation,
@@ -18,7 +17,6 @@ import {
useUpsertPermissionFlagsMutation,
} from '~/generated-metadata/graphql';
import { type Role } from '~/generated/graphql';
-import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getDirtyFields } from '~/utils/getDirtyFields';
const ROLE_BASIC_KEYS: Array = [
@@ -39,9 +37,11 @@ const ROLE_BASIC_KEYS: Array = [
export const useSaveDraftRoleToDB = ({
roleId,
isCreateMode,
+ onSuccess,
}: {
roleId: string;
isCreateMode: boolean;
+ onSuccess?: (savedRoleId: string) => void | Promise;
}) => {
const [createRole] = useCreateOneRoleMutation();
const [updateRole] = useUpdateOneRoleMutation();
@@ -51,7 +51,6 @@ export const useSaveDraftRoleToDB = ({
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
const { addAgentsToRole } = useUpdateAgentRole(roleId);
const { addApiKeysToRole } = useUpdateApiKeyRole(roleId);
- const navigateSettings = useNavigateSettings();
const settingsPersistedRole = useRecoilValue(
settingsPersistedRoleFamilyState(roleId),
@@ -100,7 +99,7 @@ export const useSaveDraftRoleToDB = ({
const { removeFieldPermissionInDraftRole } =
useRemoveFieldPermissionInDraftRole();
- const saveDraftRoleToDB = async () => {
+ const removeUselessFieldPermissions = () => {
if (
isNonEmptyArray(
fieldPermissionsThatShouldntBeCreatedBecauseTheyAreUseless,
@@ -113,215 +112,233 @@ export const useSaveDraftRoleToDB = ({
);
}
}
+ };
- if (isCreateMode) {
- const { data } = await createRole({
+ const createNewRole = async () => {
+ const { data } = await createRole({
+ variables: {
+ createRoleInput: {
+ id: roleId,
+ label: settingsDraftRole.label,
+ description: settingsDraftRole.description,
+ icon: settingsDraftRole.icon,
+ canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
+ canAccessAllTools: settingsDraftRole.canAccessAllTools,
+ canReadAllObjectRecords: settingsDraftRole.canReadAllObjectRecords,
+ canUpdateAllObjectRecords:
+ settingsDraftRole.canUpdateAllObjectRecords,
+ canSoftDeleteAllObjectRecords:
+ settingsDraftRole.canSoftDeleteAllObjectRecords,
+ canDestroyAllObjectRecords:
+ settingsDraftRole.canDestroyAllObjectRecords,
+ canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
+ canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
+ canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
+ } satisfies Partial,
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+
+ if (!data) {
+ return;
+ }
+
+ const createdRoleId = data.createOneRole.id;
+
+ await upsertRolePermissions(createdRoleId);
+ await assignEntitiesToRole(createdRoleId);
+
+ if (isDefined(onSuccess)) {
+ await onSuccess(createdRoleId);
+ }
+ };
+
+ const updateExistingRole = async () => {
+ if (isDefined(dirtyFields.permissionFlags)) {
+ await upsertPermissionFlags({
variables: {
- createRoleInput: {
- id: roleId,
- label: settingsDraftRole.label,
- description: settingsDraftRole.description,
- icon: settingsDraftRole.icon,
- canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
- canAccessAllTools: settingsDraftRole.canAccessAllTools,
- canReadAllObjectRecords: settingsDraftRole.canReadAllObjectRecords,
- canUpdateAllObjectRecords:
- settingsDraftRole.canUpdateAllObjectRecords,
- canSoftDeleteAllObjectRecords:
- settingsDraftRole.canSoftDeleteAllObjectRecords,
- canDestroyAllObjectRecords:
- settingsDraftRole.canDestroyAllObjectRecords,
- canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
- canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
- canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
- } satisfies Partial,
+ upsertPermissionFlagsInput: {
+ roleId: roleId,
+ permissionFlagKeys:
+ settingsDraftRole.permissionFlags?.map(
+ (permissionFlag) => permissionFlag.flag,
+ ) ?? [],
+ },
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
+ }
- if (!data) {
- return;
- }
-
- if (isDefined(dirtyFields.permissionFlags)) {
- await upsertPermissionFlags({
- variables: {
- upsertPermissionFlagsInput: {
- roleId: data.createOneRole.id,
- permissionFlagKeys:
- settingsDraftRole.permissionFlags?.map(
- (permissionFlag) => permissionFlag.flag,
- ) ?? [],
+ if (ROLE_BASIC_KEYS.some((key) => key in dirtyFields)) {
+ await updateRole({
+ variables: {
+ updateRoleInput: {
+ id: roleId,
+ update: {
+ label: settingsDraftRole.label,
+ description: settingsDraftRole.description,
+ icon: settingsDraftRole.icon,
+ canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
+ canAccessAllTools: settingsDraftRole.canAccessAllTools,
+ canReadAllObjectRecords:
+ settingsDraftRole.canReadAllObjectRecords,
+ canUpdateAllObjectRecords:
+ settingsDraftRole.canUpdateAllObjectRecords,
+ canSoftDeleteAllObjectRecords:
+ settingsDraftRole.canSoftDeleteAllObjectRecords,
+ canDestroyAllObjectRecords:
+ settingsDraftRole.canDestroyAllObjectRecords,
+ canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
+ canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
+ canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
},
},
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
-
- if (isDefined(dirtyFields.objectPermissions)) {
- await upsertObjectPermissions({
- variables: {
- upsertObjectPermissionsInput: {
- roleId: data.createOneRole.id,
- objectPermissions:
- settingsDraftRole.objectPermissions?.map(
- (objectPermission) => ({
- objectMetadataId: objectPermission.objectMetadataId,
- canReadObjectRecords: objectPermission.canReadObjectRecords,
- canUpdateObjectRecords:
- objectPermission.canUpdateObjectRecords,
- canSoftDeleteObjectRecords:
- objectPermission.canSoftDeleteObjectRecords,
- canDestroyObjectRecords:
- objectPermission.canDestroyObjectRecords,
- }),
- ) ?? [],
- },
- },
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
-
- if (isNonEmptyArray(fieldPermissionsToUpsert) === true) {
- await upsertFieldPermissions({
- variables: {
- upsertFieldPermissionsInput: {
- roleId: data.createOneRole.id,
- fieldPermissions:
- fieldPermissionsToUpsert.map((fieldPermission) => ({
- objectMetadataId: fieldPermission.objectMetadataId,
- fieldMetadataId: fieldPermission.fieldMetadataId,
- canReadFieldValue: fieldPermission.canReadFieldValue,
- canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
- })) ?? [],
- },
- },
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
-
- if (
- isDefined(dirtyFields.workspaceMembers) &&
- settingsDraftRole.canBeAssignedToUsers
- ) {
- await addWorkspaceMembersToRole({
- roleId: data.createOneRole.id,
- workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
- (member) => member.id,
- ),
- });
- }
-
- if (
- isDefined(dirtyFields.agents) &&
- settingsDraftRole.canBeAssignedToAgents
- ) {
- await addAgentsToRole({
- roleId: data.createOneRole.id,
- agentIds: settingsDraftRole.agents.map((agent) => agent.id),
- });
- }
-
- if (
- isDefined(dirtyFields.apiKeys) &&
- settingsDraftRole.canBeAssignedToApiKeys
- ) {
- await addApiKeysToRole({
- roleId: data.createOneRole.id,
- apiKeyIds: settingsDraftRole.apiKeys.map((apiKey) => apiKey.id),
- });
- }
-
- navigateSettings(SettingsPath.RoleDetail, {
- roleId: data.createOneRole.id,
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
+ }
+
+ if (isDefined(dirtyFields.objectPermissions)) {
+ await upsertObjectPermissions({
+ variables: {
+ upsertObjectPermissionsInput: {
+ roleId: roleId,
+ objectPermissions:
+ settingsDraftRole.objectPermissions?.map((objectPermission) => ({
+ objectMetadataId: objectPermission.objectMetadataId,
+ canReadObjectRecords: objectPermission.canReadObjectRecords,
+ canUpdateObjectRecords: objectPermission.canUpdateObjectRecords,
+ canSoftDeleteObjectRecords:
+ objectPermission.canSoftDeleteObjectRecords,
+ canDestroyObjectRecords:
+ objectPermission.canDestroyObjectRecords,
+ })) ?? [],
+ },
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+ }
+
+ if (isNonEmptyArray(fieldPermissionsToUpsert) === true) {
+ await upsertFieldPermissions({
+ variables: {
+ upsertFieldPermissionsInput: {
+ roleId: roleId,
+ fieldPermissions:
+ fieldPermissionsToUpsert.map((fieldPermission) => ({
+ objectMetadataId: fieldPermission.objectMetadataId,
+ fieldMetadataId: fieldPermission.fieldMetadataId,
+ canReadFieldValue: fieldPermission.canReadFieldValue,
+ canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
+ })) ?? [],
+ },
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+ }
+ };
+
+ const upsertRolePermissions = async (targetRoleId: string) => {
+ if (isDefined(dirtyFields.permissionFlags)) {
+ await upsertPermissionFlags({
+ variables: {
+ upsertPermissionFlagsInput: {
+ roleId: targetRoleId,
+ permissionFlagKeys:
+ settingsDraftRole.permissionFlags?.map(
+ (permissionFlag) => permissionFlag.flag,
+ ) ?? [],
+ },
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+ }
+
+ if (isDefined(dirtyFields.objectPermissions)) {
+ await upsertObjectPermissions({
+ variables: {
+ upsertObjectPermissionsInput: {
+ roleId: targetRoleId,
+ objectPermissions:
+ settingsDraftRole.objectPermissions?.map((objectPermission) => ({
+ objectMetadataId: objectPermission.objectMetadataId,
+ canReadObjectRecords: objectPermission.canReadObjectRecords,
+ canUpdateObjectRecords: objectPermission.canUpdateObjectRecords,
+ canSoftDeleteObjectRecords:
+ objectPermission.canSoftDeleteObjectRecords,
+ canDestroyObjectRecords:
+ objectPermission.canDestroyObjectRecords,
+ })) ?? [],
+ },
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+ }
+
+ if (isNonEmptyArray(fieldPermissionsToUpsert) === true) {
+ await upsertFieldPermissions({
+ variables: {
+ upsertFieldPermissionsInput: {
+ roleId: targetRoleId,
+ fieldPermissions:
+ fieldPermissionsToUpsert.map((fieldPermission) => ({
+ objectMetadataId: fieldPermission.objectMetadataId,
+ fieldMetadataId: fieldPermission.fieldMetadataId,
+ canReadFieldValue: fieldPermission.canReadFieldValue,
+ canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
+ })) ?? [],
+ },
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+ }
+ };
+
+ const assignEntitiesToRole = async (targetRoleId: string) => {
+ if (
+ isDefined(dirtyFields.workspaceMembers) &&
+ settingsDraftRole.canBeAssignedToUsers
+ ) {
+ await addWorkspaceMembersToRole({
+ roleId: targetRoleId,
+ workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
+ (member) => member.id,
+ ),
+ });
+ }
+
+ if (
+ isDefined(dirtyFields.agents) &&
+ settingsDraftRole.canBeAssignedToAgents
+ ) {
+ await addAgentsToRole({
+ roleId: targetRoleId,
+ agentIds: settingsDraftRole.agents.map((agent) => agent.id),
+ });
+ }
+
+ if (
+ isDefined(dirtyFields.apiKeys) &&
+ settingsDraftRole.canBeAssignedToApiKeys
+ ) {
+ await addApiKeysToRole({
+ roleId: targetRoleId,
+ apiKeyIds: settingsDraftRole.apiKeys.map((apiKey) => apiKey.id),
+ });
+ }
+
+ if (isDefined(onSuccess)) {
+ await onSuccess(roleId);
+ }
+ };
+
+ const saveDraftRoleToDB = async () => {
+ removeUselessFieldPermissions();
+
+ if (isCreateMode) {
+ await createNewRole();
} else {
- if (isDefined(dirtyFields.permissionFlags)) {
- await upsertPermissionFlags({
- variables: {
- upsertPermissionFlagsInput: {
- roleId: roleId,
- permissionFlagKeys:
- settingsDraftRole.permissionFlags?.map(
- (permissionFlag) => permissionFlag.flag,
- ) ?? [],
- },
- },
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
-
- if (ROLE_BASIC_KEYS.some((key) => key in dirtyFields)) {
- await updateRole({
- variables: {
- updateRoleInput: {
- id: roleId,
- update: {
- label: settingsDraftRole.label,
- description: settingsDraftRole.description,
- icon: settingsDraftRole.icon,
- canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings,
- canAccessAllTools: settingsDraftRole.canAccessAllTools,
- canReadAllObjectRecords:
- settingsDraftRole.canReadAllObjectRecords,
- canUpdateAllObjectRecords:
- settingsDraftRole.canUpdateAllObjectRecords,
- canSoftDeleteAllObjectRecords:
- settingsDraftRole.canSoftDeleteAllObjectRecords,
- canDestroyAllObjectRecords:
- settingsDraftRole.canDestroyAllObjectRecords,
- canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
- canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
- canBeAssignedToApiKeys:
- settingsDraftRole.canBeAssignedToApiKeys,
- },
- },
- },
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
-
- if (isDefined(dirtyFields.objectPermissions)) {
- await upsertObjectPermissions({
- variables: {
- upsertObjectPermissionsInput: {
- roleId: roleId,
- objectPermissions:
- settingsDraftRole.objectPermissions?.map(
- (objectPermission) => ({
- objectMetadataId: objectPermission.objectMetadataId,
- canReadObjectRecords: objectPermission.canReadObjectRecords,
- canUpdateObjectRecords:
- objectPermission.canUpdateObjectRecords,
- canSoftDeleteObjectRecords:
- objectPermission.canSoftDeleteObjectRecords,
- canDestroyObjectRecords:
- objectPermission.canDestroyObjectRecords,
- }),
- ) ?? [],
- },
- },
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
-
- if (isNonEmptyArray(fieldPermissionsToUpsert) === true) {
- await upsertFieldPermissions({
- variables: {
- upsertFieldPermissionsInput: {
- roleId: roleId,
- fieldPermissions:
- fieldPermissionsToUpsert.map((fieldPermission) => ({
- objectMetadataId: fieldPermission.objectMetadataId,
- fieldMetadataId: fieldPermission.fieldMetadataId,
- canReadFieldValue: fieldPermission.canReadFieldValue,
- canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
- })) ?? [],
- },
- },
- refetchQueries: [getOperationName(GET_ROLES) ?? ''],
- });
- }
+ await updateExistingRole();
}
};
diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx
index 57f9de8882..57a3c1c655 100644
--- a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx
@@ -4,6 +4,10 @@ import { useParams } from 'react-router-dom';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
+import { useSaveDraftRoleToDB } from '@/settings/roles/role/hooks/useSaveDraftRoleToDB';
+import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
+import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
@@ -12,7 +16,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { t } from '@lingui/core/macro';
import { AppPath, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
-import { H2Title, IconLock, IconSettings } from 'twenty-ui/display';
+import { IconLock, IconSettings } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import {
type CreateAgentInput,
@@ -24,6 +28,8 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { useState } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { SettingsAgentDetailSkeletonLoader } from './components/SettingsAgentDetailSkeletonLoader';
import { SettingsAgentRoleTab } from './components/SettingsAgentRoleTab';
import { SettingsAgentSettingsTab } from './components/SettingsAgentSettingsTab';
@@ -107,6 +113,22 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
const agent = data?.findOneAgent;
+ const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
+ settingsDraftRoleFamilyState(formValues.role || ''),
+ );
+ const settingsPersistedRole = useRecoilValue(
+ settingsPersistedRoleFamilyState(formValues.role || ''),
+ );
+
+ const { saveDraftRoleToDB } = useSaveDraftRoleToDB({
+ roleId: formValues.role || '',
+ isCreateMode: false,
+ });
+
+ const isRoleDirty =
+ isDefined(formValues.role) &&
+ !isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
+
if (!isCreateMode && !loading && !agent) {
return null;
}
@@ -138,6 +160,26 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
setIsSubmitting(true);
try {
+ if (isRoleDirty && isDefined(formValues.role)) {
+ try {
+ await saveDraftRoleToDB();
+ } catch (error) {
+ if (error instanceof ApolloError) {
+ enqueueErrorSnackBar({
+ apolloError: error,
+ });
+ } else {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ enqueueErrorSnackBar({
+ message: t`Failed to save role permissions: ${errorMessage}`,
+ });
+ }
+ setIsSubmitting(false);
+ return;
+ }
+ }
+
if (isCreateMode) {
const input: CreateAgentInput = {
name: formValues.name,
@@ -187,15 +229,21 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
}
};
+ const handleCancel = () => {
+ resetForm();
+
+ if (isRoleDirty && isDefined(settingsPersistedRole)) {
+ setSettingsDraftRole(settingsPersistedRole);
+ }
+
+ navigate(SettingsPath.AI);
+ };
+
const title = !isCreateMode
? loading
? t`Agent`
: agent?.label
: t`New Agent`;
- const pageTitle = !isCreateMode ? t`Edit Agent` : t`New Agent`;
- const pageDescription = !isCreateMode
- ? t`Update agent information`
- : t`Create a new AI agent`;
const breadcrumbText = !isCreateMode
? loading
? t`Agent`
@@ -210,6 +258,8 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
formValues={formValues}
onFieldChange={handleFieldChange}
disabled={isReadonlyMode || (isEditMode ? !agent?.isCustom : false)}
+ agentId={agentId}
+ agentLabel={formValues.label}
/>
);
@@ -229,13 +279,14 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
return (
<>
+
navigate(SettingsPath.AI)}
+ onCancel={handleCancel}
isSaveDisabled={!canSave}
isLoading={isSubmitting}
isCancelDisabled={isSubmitting}
@@ -253,7 +304,6 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
>
-
{isEditMode && loading ? (
) : (
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx
index 3f2130dca5..d8d6abba95 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx
@@ -1,33 +1,29 @@
+import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
+import { useState } from 'react';
+import { useSetRecoilState } from 'recoil';
+import { v4 } from 'uuid';
+import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
-import { Select } from '@/ui/input/components/Select';
-import { SettingsPath } from 'twenty-shared/types';
+import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
+import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
-import {
- H1Title,
- H1TitleFontColor,
- H2Title,
- IconArrowUpRight,
- IconUser,
- useIcons,
-} from 'twenty-ui/display';
+import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
-import { useGetRolesQuery } from '~/generated-metadata/graphql';
-import { useNavigateSettings } from '~/hooks/useNavigateSettings';
+import {
+ useAssignRoleToAgentMutation,
+ useCreateOneRoleMutation,
+ useGetRolesQuery,
+} from '~/generated-metadata/graphql';
import { type SettingsAIAgentFormValues } from '../hooks/useSettingsAgentFormState';
-const StyledRoleContainer = styled.div`
- align-items: flex-end;
- display: flex;
- gap: ${({ theme }) => theme.spacing(2)};
- margin-bottom: ${({ theme }) => theme.spacing(8)};
-`;
-
-const StyledRoleSelector = styled.div`
- flex: 1;
+const StyledWarningText = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
type SettingsAgentRoleTabProps = {
@@ -37,81 +33,146 @@ type SettingsAgentRoleTabProps = {
value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues],
) => void;
disabled: boolean;
+ agentId?: string;
+ agentLabel: string;
};
export const SettingsAgentRoleTab = ({
formValues,
onFieldChange,
disabled,
+ agentId,
+ agentLabel,
}: SettingsAgentRoleTabProps) => {
const { t } = useLingui();
- const { getIcon } = useIcons();
- const navigateSettings = useNavigateSettings();
+ const [isCreatingRole, setIsCreatingRole] = useState(false);
const { data: rolesData } = useGetRolesQuery();
-
- const rolesOptions = [
- {
- label: t`None`,
- value: null,
- Icon: IconUser,
- },
- ...(rolesData?.getRoles
- ?.filter((role) => role.canBeAssignedToAgents)
- .map((role) => ({
- label: role.label,
- value: role.id,
- Icon: getIcon(role.icon) ?? IconUser,
- })) || []),
- ];
+ const [createRole] = useCreateOneRoleMutation();
+ const [assignRoleToAgent] = useAssignRoleToAgentMutation();
+ const setSettingsDraftRole = useSetRecoilState(
+ settingsDraftRoleFamilyState(formValues.role || ''),
+ );
const selectedRole = rolesData?.getRoles?.find(
(role) => role.id === formValues.role,
);
- const handleOpenRole = () => {
- if (isDefined(selectedRole)) {
- navigateSettings(SettingsPath.RoleDetail, { roleId: selectedRole.id });
+ const hasValidAgentId = isNonEmptyString(agentId);
+
+ const isRoleShared = selectedRole
+ ? (selectedRole.workspaceMembers?.length || 0) +
+ (selectedRole.agents?.length || 0) +
+ (selectedRole.apiKeys?.length || 0) >
+ 1
+ : false;
+
+ // Role is only editable if it's not shared and either:
+ // 1. Assigned exclusively to this agent (edit mode)
+ // 2. Not yet assigned to anyone (create mode)
+ const isRoleExclusiveToThisAgent =
+ !isRoleShared &&
+ selectedRole &&
+ (selectedRole.workspaceMembers?.length || 0) === 0 &&
+ (selectedRole.apiKeys?.length || 0) === 0 &&
+ (hasValidAgentId
+ ? selectedRole.agents?.length === 1 &&
+ selectedRole.agents[0].id === agentId
+ : (selectedRole.agents?.length || 0) === 0);
+
+ const handleCreateRole = async () => {
+ setIsCreatingRole(true);
+ try {
+ const roleId = v4();
+ const roleName = `${agentLabel} Agent Role`;
+
+ const { data } = await createRole({
+ variables: {
+ createRoleInput: {
+ id: roleId,
+ label: roleName,
+ description: t`Role for ${agentLabel} agent`,
+ icon: 'IconLock',
+ canUpdateAllSettings: false,
+ canAccessAllTools: false,
+ canReadAllObjectRecords: false,
+ canUpdateAllObjectRecords: false,
+ canSoftDeleteAllObjectRecords: false,
+ canDestroyAllObjectRecords: false,
+ canBeAssignedToUsers: false,
+ canBeAssignedToAgents: true,
+ canBeAssignedToApiKeys: false,
+ },
+ },
+ refetchQueries: [getOperationName(GET_ROLES) ?? ''],
+ });
+
+ if (isDefined(data?.createOneRole)) {
+ onFieldChange('role', data.createOneRole.id);
+
+ if (hasValidAgentId) {
+ await assignRoleToAgent({
+ variables: {
+ agentId,
+ roleId: data.createOneRole.id,
+ },
+ refetchQueries: ['GetRoles'],
+ });
+ }
+
+ setSettingsDraftRole({
+ ...data.createOneRole,
+ workspaceMembers: [],
+ agents: [],
+ apiKeys: [],
+ objectPermissions: [],
+ fieldPermissions: [],
+ permissionFlags: [],
+ });
+ }
+ } finally {
+ setIsCreatingRole(false);
}
};
+ const isRoleEditable =
+ Boolean(selectedRole?.isEditable) &&
+ !disabled &&
+ Boolean(isRoleExclusiveToThisAgent);
+
return (
-
-
-
-
-
-
- {selectedRole?.id && (
+ {!formValues.role ? (
<>
-
-
>
+ ) : (
+ <>
+ {selectedRole?.id && (
+ <>
+ {isRoleShared && (
+
+ {t`This role is shared with other users or agents and cannot be edited here.`}
+
+ )}
+
+ >
+ )}
+ >
)}
);
diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx
index a1402b3b86..13190dacac 100644
--- a/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx
@@ -1,16 +1,13 @@
-import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
-import { t } from '@lingui/core/macro';
-import { getSettingsPath } from 'twenty-shared/utils';
-import { SettingsPath } from 'twenty-shared/types';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
-import { Section } from 'twenty-ui/layout';
-import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
-import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
-import { TabList } from '@/ui/layout/tab-list/components/TabList';
-import styled from '@emotion/styled';
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
-
-const APPLICATIONS_ID = 'applications';
+import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
+import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { SettingsPath } from 'twenty-shared/types';
+import { getSettingsPath } from 'twenty-shared/utils';
+import { Section } from 'twenty-ui/layout';
+import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
+import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
const StyledNoApplicationContainer = styled.div``;
@@ -19,8 +16,6 @@ export const SettingsApplications = () => {
const applications = data?.findManyApplications ?? [];
- const tabs = [{ id: 'inUsed', title: 'In used' }];
-
return (
{
{applications.length > 0 ? (
- <>
-
-
- >
+
) : (
No installed application. Please check our{' '}
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx
index f3b59d61f5..d3b65eeee1 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx
@@ -5,19 +5,22 @@ import {
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
-import { TableSection } from '@/ui/layout/table/components/TableSection';
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata';
import styled from '@emotion/styled';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
-import { isNonEmptyArray } from '@sniptt/guards';
import { useEffect, useMemo, useState } from 'react';
import { useRecoilState } from 'recoil';
-import { IconSearch } from 'twenty-ui/display';
+import { IconArchive, IconFilter, IconSearch } from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+import { MenuItemToggle } from 'twenty-ui/navigation';
import { useMapFieldMetadataItemToSettingsObjectDetailTableItem } from '~/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem';
import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
@@ -80,10 +83,17 @@ const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM: TableMetadata theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
+
+const StyledSearchInput = styled(SettingsTextInput)`
+ flex: 1;
+`;
+
export type SettingsObjectFieldTableProps = {
objectMetadataItem: ObjectMetadataItem;
mode: 'view' | 'new-field';
@@ -96,6 +106,7 @@ export const SettingsObjectFieldTable = ({
}: SettingsObjectFieldTableProps) => {
const { t } = useLingui();
const [searchTerm, setSearchTerm] = useState('');
+ const [showInactive, setShowInactive] = useState(mode === 'new-field');
const tableMetadata = objectMetadataItem.isCustom
? GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM
@@ -114,14 +125,13 @@ export const SettingsObjectFieldTable = ({
setSettingsObjectFields(objectMetadataItem.fields);
}, [objectMetadataItem, setSettingsObjectFields]);
- const activeObjectSettingsDetailItems = useMemo(() => {
- const activeMetadataFields = settingsObjectFields?.filter(
- (fieldMetadataItem) =>
- fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
+ const allObjectSettingsDetailItems = useMemo(() => {
+ const nonSystemFields = settingsObjectFields?.filter(
+ (fieldMetadataItem) => !fieldMetadataItem.isSystem,
);
return (
- activeMetadataFields?.map(
+ nonSystemFields?.map(
mapFieldMetadataItemToSettingsObjectDetailTableItem,
) ?? []
);
@@ -130,61 +140,64 @@ export const SettingsObjectFieldTable = ({
mapFieldMetadataItemToSettingsObjectDetailTableItem,
]);
- const disabledObjectSettingsDetailItems = useMemo(() => {
- const disabledFieldMetadataItems = settingsObjectFields?.filter(
- (fieldMetadataItem) =>
- !fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
- );
-
- return (
- disabledFieldMetadataItems?.map(
- mapFieldMetadataItemToSettingsObjectDetailTableItem,
- ) ?? []
- );
- }, [
- settingsObjectFields,
- mapFieldMetadataItemToSettingsObjectDetailTableItem,
- ]);
-
- const sortedActiveObjectSettingsDetailItems = useSortedArray(
- activeObjectSettingsDetailItems,
+ const sortedAllObjectSettingsDetailItems = useSortedArray(
+ allObjectSettingsDetailItems,
tableMetadata,
);
- const sortedDisabledObjectSettingsDetailItems = useSortedArray(
- disabledObjectSettingsDetailItems,
- tableMetadata,
- );
-
- const filteredActiveItems = useMemo(() => {
+ const filteredItems = useMemo(() => {
const searchNormalized = normalizeSearchText(searchTerm);
- return sortedActiveObjectSettingsDetailItems.filter((item) => {
- return (
- normalizeSearchText(item.label).includes(searchNormalized) ||
- normalizeSearchText(item.dataType).includes(searchNormalized)
- );
- });
- }, [sortedActiveObjectSettingsDetailItems, searchTerm]);
- const filteredDisabledItems = useMemo(() => {
- const searchNormalized = normalizeSearchText(searchTerm);
- return sortedDisabledObjectSettingsDetailItems.filter((item) => {
- return (
+ return sortedAllObjectSettingsDetailItems.filter((item) => {
+ const matchesActiveFilter =
+ showInactive || item.fieldMetadataItem.isActive;
+
+ const matchesSearch =
normalizeSearchText(item.label).includes(searchNormalized) ||
- normalizeSearchText(item.dataType).includes(searchNormalized)
- );
+ normalizeSearchText(item.dataType).includes(searchNormalized);
+
+ return matchesActiveFilter && matchesSearch;
});
- }, [sortedDisabledObjectSettingsDetailItems, searchTerm]);
+ }, [sortedAllObjectSettingsDetailItems, searchTerm, showInactive]);
return (
<>
-
+
+
+
+ }
+ dropdownComponents={
+
+
+ setShowInactive(!showInactive)}
+ toggled={showInactive}
+ text={t`Inactive`}
+ toggleSize="small"
+ />
+
+
+ }
+ />
+
{tableMetadata.fields.map((item) => (
@@ -198,33 +211,20 @@ export const SettingsObjectFieldTable = ({
))}
- {isNonEmptyArray(filteredActiveItems) && (
-
- {filteredActiveItems.map((objectSettingsDetailItem) => (
-
- ))}
-
- )}
- {isNonEmptyArray(filteredDisabledItems) && (
-
- {filteredDisabledItems.map((objectSettingsDetailItem) => (
-
- ))}
-
- )}
+ {filteredItems.map((objectSettingsDetailItem) => {
+ const status = objectSettingsDetailItem.fieldMetadataItem.isActive
+ ? 'active'
+ : 'disabled';
+
+ return (
+
+ );
+ })}
>
);
diff --git a/packages/twenty-front/src/pages/settings/roles/SettingsRoleAddObjectLevel.tsx b/packages/twenty-front/src/pages/settings/roles/SettingsRoleAddObjectLevel.tsx
index 76e40a64a1..5166e8f270 100644
--- a/packages/twenty-front/src/pages/settings/roles/SettingsRoleAddObjectLevel.tsx
+++ b/packages/twenty-front/src/pages/settings/roles/SettingsRoleAddObjectLevel.tsx
@@ -4,27 +4,54 @@ import { SettingsRolePermissionsObjectLevelObjectPicker } from '@/settings/roles
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { t } from '@lingui/core/macro';
-import { Navigate, useParams } from 'react-router-dom';
+import { Navigate, useParams, useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath } from 'twenty-shared/utils';
+import { getSettingsPath, isDefined } from 'twenty-shared/utils';
+import { useFindOneAgentQuery } from '~/generated-metadata/graphql';
export const SettingsRoleAddObjectLevel = () => {
const { roleId } = useParams();
+ const [searchParams] = useSearchParams();
+ const fromAgentId = searchParams.get('fromAgent');
+
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId ?? ''),
);
+ const { data: agentData } = useFindOneAgentQuery({
+ variables: { id: fromAgentId || '' },
+ skip: !fromAgentId,
+ });
+
+ const agent = agentData?.findOneAgent;
+
if (!roleId) {
return ;
}
- return (
- <>
-
- {
children: t`Add object permission`,
href: getSettingsPath(SettingsPath.RoleAddObjectLevel, { roleId }),
},
- ]}
+ ];
+
+ return (
+ <>
+
+