Agent role edition (#15914)
## 🐛 Issues Fixed ### 1. Role Editing Not Working for New Agents When creating a new agent and then creating a role for it, the role permissions appeared as non-editable. This was caused by: - In create mode, `agentId = ''` (empty string from `useParams`) - Empty string is **falsy** but `isDefined('')` returns **true** - This caused incorrect behavior in role exclusivity checks and API calls ### 2. Missing Role Data Loading The agent form wasn't loading role data needed for permission editing because it was missing the `SettingsRolesQueryEffect` component. ### 3. Navigation Conflicts The `useSaveDraftRoleToDB` hook contained navigation logic that caused conflicts when used from different contexts (role detail page vs agent form). ### 4. Linting Errors Unused `useIcons` import in `SettingsAgentRoleTab.tsx`. --- ## 🔧 Changes Made ### Agent Role Tab (`SettingsAgentRoleTab.tsx`) - ✅ Use `isNonEmptyString(agentId)` to validate agentId (follows codebase patterns) - ✅ Improved role exclusivity logic to handle both create and edit modes: - **Edit mode**: Role must be assigned exclusively to this agent - **Create mode**: Role must not be assigned to anyone yet - ✅ Only call `assignRoleToAgent` when a valid agentId exists - ✅ Pass `undefined` instead of empty string for `fromAgentId` prop ### Role Hook (`useSaveDraftRoleToDB.ts`) - ✅ Removed navigation logic from the hook (better separation of concerns) - ✅ Removed `useNavigateSettings` and `SettingsPath` dependencies - ✅ Hook now only handles data persistence, not navigation ### Role Component (`SettingsRole.tsx`) - ✅ Added navigation after successful role creation in create mode - ✅ Navigation now handled by the component using the hook ### Agent Form (`SettingsAgentForm.tsx`) - ✅ Added `SettingsRolesQueryEffect` to ensure role data is loaded - ✅ Agent form handles its own navigation after save --- ## ✅ Testing Scenarios All these scenarios now work correctly: 1. ✅ Create new agent → Create role → Edit permissions → Save agent 2. ✅ Edit existing agent → Create role → Edit permissions → Save 3. ✅ Edit existing agent → Try to edit shared role (shows warning message) 4. ✅ Navigate to object-level permissions from agent form with proper breadcrumbs --- ## 📝 Technical Details ### Root Cause Analysis The main issue was that empty string was being treated differently in different checks: - `agentId &&` → evaluates to `false` (empty string is falsy) - `isDefined(agentId)` → returns `true` (empty string is defined) This inconsistency caused the role to appear as non-editable and attempted invalid API calls. ### Solution Used `isNonEmptyString()` from `@sniptt/guards` which properly checks both that the value is defined AND not empty, following codebase conventions. --- ## 🎯 Impact - Fixes critical bug preventing role permission configuration for new agents - Improves code quality with better separation of concerns - Makes navigation logic more predictable and maintainable - Follows codebase patterns and guidelines
This commit is contained in:
+2
-42
@@ -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 = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
className="tab-list"
|
||||
componentInstanceId={ROLES_LIST_TABS.COMPONENT_INSTANCE_ID}
|
||||
/>
|
||||
<SettingsRolesList />
|
||||
{activeTabId === ROLES_LIST_TABS.TABS_IDS.USER_ROLES && (
|
||||
<SettingsRoleDefaultRole roles={settingsAllRoles} />
|
||||
)}
|
||||
<SettingsRoleDefaultRole roles={settingsAllRoles} />
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@@ -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<string, string> = {
|
||||
[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 (
|
||||
<Section>
|
||||
<H2Title title={t`All roles`} description={description} />
|
||||
|
||||
<StyledSearchInput
|
||||
instanceId="settings-objects-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a role...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
<H2Title
|
||||
title={t`All roles`}
|
||||
description={t`Assign roles to specify access permissions`}
|
||||
/>
|
||||
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="settings-roles-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a role...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
<Dropdown
|
||||
dropdownId="settings-roles-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={
|
||||
<Button
|
||||
Icon={IconFilter}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
ariaLabel={t`Filter`}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconRobot}
|
||||
onToggleChange={() => setShowAgentRoles(!showAgentRoles)}
|
||||
toggled={showAgentRoles}
|
||||
text={t`Agent roles`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconKey}
|
||||
onToggleChange={() => setShowApiKeyRoles(!showApiKeyRoles)}
|
||||
toggled={showApiKeyRoles}
|
||||
text={t`API key roles`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</StyledSearchAndFilterContainer>
|
||||
|
||||
<Table>
|
||||
<SettingsRolesTableHeader />
|
||||
<StyledTableRows>
|
||||
|
||||
+3
-1
@@ -13,11 +13,13 @@ const StyledRolePermissionsContainer = styled.div`
|
||||
type SettingsRolePermissionsProps = {
|
||||
roleId: string;
|
||||
isEditable: boolean;
|
||||
fromAgentId?: string;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissions = ({
|
||||
roleId,
|
||||
isEditable,
|
||||
fromAgentId,
|
||||
}: SettingsRolePermissionsProps) => {
|
||||
return (
|
||||
<StyledRolePermissionsContainer>
|
||||
@@ -27,7 +29,7 @@ export const SettingsRolePermissions = ({
|
||||
/>
|
||||
<SettingsRolePermissionsObjectLevelSection
|
||||
roleId={roleId}
|
||||
isEditable={isEditable}
|
||||
fromAgentId={fromAgentId}
|
||||
/>
|
||||
<SettingsRolePermissionsSettingsSection
|
||||
roleId={roleId}
|
||||
|
||||
+8
-4
@@ -7,6 +7,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { H2Title, IconSearch, useIcons } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
@@ -52,6 +53,8 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
|
||||
}) => {
|
||||
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 } =
|
||||
|
||||
+17
-6
@@ -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}
|
||||
/>
|
||||
</StyledCreateObjectOverrideSection>
|
||||
|
||||
+12
-4
@@ -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 (
|
||||
<TableRow
|
||||
to={getSettingsPath(SettingsPath.RoleObjectLevel, {
|
||||
roleId: roleId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
})}
|
||||
to={navigationUrl}
|
||||
gridAutoColumns={OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
|
||||
+61
-23
@@ -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 (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`2. Set ${objectLabelPlural} permissions`}
|
||||
links={[
|
||||
{
|
||||
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}`,
|
||||
},
|
||||
]}
|
||||
links={breadcrumbLinks}
|
||||
actionButton={
|
||||
<Button
|
||||
title={t`Finish`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
accent="blue"
|
||||
to={getSettingsPath(SettingsPath.RoleDetail, {
|
||||
roleId,
|
||||
})}
|
||||
to={finishButtonPath}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+220
-203
@@ -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<keyof Role> = [
|
||||
@@ -39,9 +37,11 @@ const ROLE_BASIC_KEYS: Array<keyof Role> = [
|
||||
export const useSaveDraftRoleToDB = ({
|
||||
roleId,
|
||||
isCreateMode,
|
||||
onSuccess,
|
||||
}: {
|
||||
roleId: string;
|
||||
isCreateMode: boolean;
|
||||
onSuccess?: (savedRoleId: string) => void | Promise<void>;
|
||||
}) => {
|
||||
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<Role>,
|
||||
},
|
||||
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<Role>,
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SettingsRolesQueryEffect />
|
||||
<SubMenuTopBarContainer
|
||||
title={title}
|
||||
actionButton={
|
||||
isCreateMode || (isEditMode && agent?.isCustom) ? (
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSave}
|
||||
onCancel={() => navigate(SettingsPath.AI)}
|
||||
onCancel={handleCancel}
|
||||
isSaveDisabled={!canSave}
|
||||
isLoading={isSubmitting}
|
||||
isCancelDisabled={isSubmitting}
|
||||
@@ -253,7 +304,6 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title={pageTitle} description={pageDescription} />
|
||||
{isEditMode && loading ? (
|
||||
<SettingsAgentDetailSkeletonLoader />
|
||||
) : (
|
||||
|
||||
@@ -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 (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Role`}
|
||||
description={t`The agent can perform all actions defined by the following role.`}
|
||||
/>
|
||||
<StyledRoleContainer>
|
||||
<StyledRoleSelector>
|
||||
<Select
|
||||
dropdownId="agent-role-select"
|
||||
options={rolesOptions}
|
||||
value={formValues.role || ''}
|
||||
onChange={(value) => onFieldChange('role', value)}
|
||||
disabled={disabled}
|
||||
withSearchInput
|
||||
fullWidth
|
||||
/>
|
||||
</StyledRoleSelector>
|
||||
<Button
|
||||
Icon={IconArrowUpRight}
|
||||
title={t`Open`}
|
||||
variant="secondary"
|
||||
onClick={handleOpenRole}
|
||||
disabled={!selectedRole}
|
||||
/>
|
||||
</StyledRoleContainer>
|
||||
{selectedRole?.id && (
|
||||
{!formValues.role ? (
|
||||
<>
|
||||
<H1Title
|
||||
title={t`Role Permissions`}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
<H2Title
|
||||
title={t`Role`}
|
||||
description={t`Create a role to define permissions for this agent.`}
|
||||
/>
|
||||
<SettingsRolePermissions
|
||||
roleId={selectedRole.id}
|
||||
isEditable={false}
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Create Role`}
|
||||
variant="secondary"
|
||||
onClick={handleCreateRole}
|
||||
disabled={disabled || isCreatingRole}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{selectedRole?.id && (
|
||||
<>
|
||||
{isRoleShared && (
|
||||
<StyledWarningText>
|
||||
{t`This role is shared with other users or agents and cannot be edited here.`}
|
||||
</StyledWarningText>
|
||||
)}
|
||||
<SettingsRolePermissions
|
||||
roleId={selectedRole.id}
|
||||
isEditable={isRoleEditable}
|
||||
fromAgentId={hasValidAgentId ? agentId : undefined}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Applications`}
|
||||
@@ -35,14 +30,7 @@ export const SettingsApplications = () => {
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
{applications.length > 0 ? (
|
||||
<>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={APPLICATIONS_ID}
|
||||
/>
|
||||
<SettingsApplicationsTable applications={applications} />
|
||||
</>
|
||||
<SettingsApplicationsTable applications={applications} />
|
||||
) : (
|
||||
<StyledNoApplicationContainer>
|
||||
No installed application. Please check our{' '}
|
||||
|
||||
@@ -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<SettingsOb
|
||||
},
|
||||
};
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
const StyledSearchAndFilterContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => 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 (
|
||||
<>
|
||||
<StyledSearchInput
|
||||
instanceId="object-field-table-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a field...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="object-field-table-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a field...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
<Dropdown
|
||||
dropdownId="settings-fields-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={
|
||||
<Button
|
||||
Icon={IconFilter}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
ariaLabel={t`Filter`}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconArchive}
|
||||
onToggleChange={() => setShowInactive(!showInactive)}
|
||||
toggled={showInactive}
|
||||
text={t`Inactive`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</StyledSearchAndFilterContainer>
|
||||
<Table>
|
||||
<StyledObjectFieldTableRow>
|
||||
{tableMetadata.fields.map((item) => (
|
||||
@@ -198,33 +211,20 @@ export const SettingsObjectFieldTable = ({
|
||||
))}
|
||||
<TableHeader></TableHeader>
|
||||
</StyledObjectFieldTableRow>
|
||||
{isNonEmptyArray(filteredActiveItems) && (
|
||||
<TableSection title={t`Active`}>
|
||||
{filteredActiveItems.map((objectSettingsDetailItem) => (
|
||||
<SettingsObjectFieldItemTableRow
|
||||
key={objectSettingsDetailItem.fieldMetadataItem.id}
|
||||
settingsObjectDetailTableItem={objectSettingsDetailItem}
|
||||
status="active"
|
||||
mode={mode}
|
||||
/>
|
||||
))}
|
||||
</TableSection>
|
||||
)}
|
||||
{isNonEmptyArray(filteredDisabledItems) && (
|
||||
<TableSection
|
||||
isInitiallyExpanded={mode === 'new-field' ? true : false}
|
||||
title={t`Inactive`}
|
||||
>
|
||||
{filteredDisabledItems.map((objectSettingsDetailItem) => (
|
||||
<SettingsObjectFieldItemTableRow
|
||||
key={objectSettingsDetailItem.fieldMetadataItem.id}
|
||||
settingsObjectDetailTableItem={objectSettingsDetailItem}
|
||||
status="disabled"
|
||||
mode={mode}
|
||||
/>
|
||||
))}
|
||||
</TableSection>
|
||||
)}
|
||||
{filteredItems.map((objectSettingsDetailItem) => {
|
||||
const status = objectSettingsDetailItem.fieldMetadataItem.isActive
|
||||
? 'active'
|
||||
: 'disabled';
|
||||
|
||||
return (
|
||||
<SettingsObjectFieldItemTableRow
|
||||
key={objectSettingsDetailItem.fieldMetadataItem.id}
|
||||
settingsObjectDetailTableItem={objectSettingsDetailItem}
|
||||
status={status}
|
||||
mode={mode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRolesQueryEffect />
|
||||
<SubMenuTopBarContainer
|
||||
title={t`1. Select an object`}
|
||||
links={[
|
||||
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`Add object permission`,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{ children: t`Roles`, href: getSettingsPath(SettingsPath.Roles) },
|
||||
{
|
||||
children: settingsDraftRole.label ?? '',
|
||||
@@ -34,7 +61,14 @@ export const SettingsRoleAddObjectLevel = () => {
|
||||
children: t`Add object permission`,
|
||||
href: getSettingsPath(SettingsPath.RoleAddObjectLevel, { roleId }),
|
||||
},
|
||||
]}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRolesQueryEffect />
|
||||
<SubMenuTopBarContainer
|
||||
title={t`1. Select an object`}
|
||||
links={breadcrumbLinks}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsRolePermissionsObjectLevelObjectPicker roleId={roleId} />
|
||||
|
||||
Reference in New Issue
Block a user