Feat: role applicability controls (#14239)
Closes [#1404](https://github.com/twentyhq/core-team-issues/issues/1404) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -141,6 +141,14 @@ export type ApiKey = {
|
||||
workspaceId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type ApiKeyForRole = {
|
||||
__typename?: 'ApiKeyForRole';
|
||||
expiresAt: Scalars['DateTime'];
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
revokedAt?: Maybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
export type ApiKeyToken = {
|
||||
__typename?: 'ApiKeyToken';
|
||||
token: Scalars['String'];
|
||||
@@ -675,6 +683,9 @@ export type CreatePageLayoutInput = {
|
||||
|
||||
export type CreateRoleInput = {
|
||||
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
||||
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
||||
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
||||
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
||||
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -2788,7 +2799,12 @@ export type RevokeApiKeyDto = {
|
||||
|
||||
export type Role = {
|
||||
__typename?: 'Role';
|
||||
agents: Array<Agent>;
|
||||
apiKeys: Array<ApiKeyForRole>;
|
||||
canAccessAllTools: Scalars['Boolean'];
|
||||
canBeAssignedToAgents: Scalars['Boolean'];
|
||||
canBeAssignedToApiKeys: Scalars['Boolean'];
|
||||
canBeAssignedToUsers: Scalars['Boolean'];
|
||||
canDestroyAllObjectRecords: Scalars['Boolean'];
|
||||
canReadAllObjectRecords: Scalars['Boolean'];
|
||||
canSoftDeleteAllObjectRecords: Scalars['Boolean'];
|
||||
@@ -3212,6 +3228,9 @@ export type UpdateRoleInput = {
|
||||
|
||||
export type UpdateRolePayload = {
|
||||
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
|
||||
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
|
||||
canBeAssignedToApiKeys?: InputMaybe<Scalars['Boolean']>;
|
||||
canBeAssignedToUsers?: InputMaybe<Scalars['Boolean']>;
|
||||
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
|
||||
|
||||
@@ -43,7 +43,57 @@ const StyledMessageText = styled.div<{ isUser?: boolean }>`
|
||||
isUser ? theme.font.color.light : theme.font.color.primary};
|
||||
font-weight: ${({ isUser }) => (isUser ? 500 : 400)};
|
||||
width: fit-content;
|
||||
white-space: pre-line;
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
code {
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
line-height: 1.4;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
|
||||
pre {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: ${({ theme }) => theme.spacing(1)} 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: ${({ theme }) => theme.spacing(1)} 0;
|
||||
padding-left: ${({ theme }) => theme.spacing(4)};
|
||||
}
|
||||
|
||||
li {
|
||||
margin: ${({ theme }) => theme.spacing(0.5)} 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid ${({ theme }) => theme.border.color.medium};
|
||||
margin: ${({ theme }) => theme.spacing(2)} 0;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMessageFooter = styled.div`
|
||||
|
||||
@@ -118,6 +118,8 @@ export const AIChatTab = ({
|
||||
placeholder={t`Enter a question...`}
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
minRows={1}
|
||||
maxRows={20}
|
||||
/>
|
||||
<StyledButtonsContainer>
|
||||
{!isWorkflowAgentNodeChat && (
|
||||
|
||||
@@ -25,7 +25,7 @@ export type CurrentWorkspace = Pick<
|
||||
| 'metadataVersion'
|
||||
| 'isTwoFactorAuthenticationEnforced'
|
||||
> & {
|
||||
defaultRole?: Omit<Role, 'workspaceMembers'> | null;
|
||||
defaultRole?: Omit<Role, 'workspaceMembers' | 'agents' | 'apiKeys'> | null;
|
||||
defaultAgent?: { id: string } | null;
|
||||
};
|
||||
|
||||
|
||||
+15
-6
@@ -1,7 +1,7 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { type IconComponent, useIcons } from 'twenty-ui/display';
|
||||
import { type Role } from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsDevelopersRoleSelectorProps = {
|
||||
@@ -40,11 +40,20 @@ export const SettingsDevelopersRoleSelector = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = roles.map((role) => ({
|
||||
label: role.label,
|
||||
value: role.id,
|
||||
Icon: getIcon(role.icon) ?? undefined,
|
||||
}));
|
||||
const options = roles.reduce<
|
||||
Array<{ label: string; value: string; Icon?: IconComponent }>
|
||||
>((acc, role) => {
|
||||
{
|
||||
if (role.canBeAssignedToApiKeys) {
|
||||
acc.push({
|
||||
label: role.label,
|
||||
value: role.id,
|
||||
Icon: getIcon(role.icon) ?? undefined,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const selectValue =
|
||||
value || (doesDefaultRoleExistInRoles ? defaultRole?.id : roles[0].id);
|
||||
|
||||
+10
-1
@@ -6,9 +6,12 @@ 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 } from 'twenty-ui/display';
|
||||
@@ -16,6 +19,10 @@ 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);
|
||||
|
||||
@@ -36,7 +43,9 @@ export const SettingsRolesContainer = () => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsRolesList />
|
||||
<SettingsRoleDefaultRole roles={settingsAllRoles} />
|
||||
{activeTabId === ROLES_LIST_TABS.TABS_IDS.USER_ROLES && (
|
||||
<SettingsRoleDefaultRole roles={settingsAllRoles} />
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,25 @@ 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 { SettingsPath } from '@/types/SettingsPath';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H2Title, IconPlus } from 'twenty-ui/display';
|
||||
import {
|
||||
H2Title,
|
||||
IconKey,
|
||||
IconPlus,
|
||||
IconRobot,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
@@ -33,26 +45,74 @@ const StyledNoRoles = styled(TableCell)`
|
||||
|
||||
export const SettingsRolesList = () => {
|
||||
const navigateSettings = useNavigateSettings();
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
ROLES_LIST_TABS.COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const sortedSettingsAllRoles = [...settingsAllRoles].sort((a, b) =>
|
||||
sortByAscString(a.label, b.label),
|
||||
);
|
||||
|
||||
const filteredRoles = sortedSettingsAllRoles.filter((role) => {
|
||||
switch (activeTabId) {
|
||||
case ROLES_LIST_TABS.TABS_IDS.USER_ROLES:
|
||||
return role.canBeAssignedToUsers;
|
||||
case ROLES_LIST_TABS.TABS_IDS.AGENT_ROLES:
|
||||
return role.canBeAssignedToAgents;
|
||||
case ROLES_LIST_TABS.TABS_IDS.API_KEY_ROLES:
|
||||
return role.canBeAssignedToApiKeys;
|
||||
default:
|
||||
return role.canBeAssignedToUsers;
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
const description = isAiEnabled
|
||||
? t`Manage roles and permissions for team members, agents, and API keys`
|
||||
: t`Manage roles and permissions for team members and API keys`;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`All roles`}
|
||||
description={t`Assign roles to manage each member’s access and permissions`}
|
||||
<H2Title title={t`All roles`} description={description} />
|
||||
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
className="tab-list"
|
||||
componentInstanceId={ROLES_LIST_TABS.COMPONENT_INSTANCE_ID}
|
||||
/>
|
||||
|
||||
<Table>
|
||||
<SettingsRolesTableHeader />
|
||||
<StyledTableRows>
|
||||
{sortedSettingsAllRoles.length === 0 ? (
|
||||
{filteredRoles.length === 0 ? (
|
||||
<StyledNoRoles>{t`No roles found`}</StyledNoRoles>
|
||||
) : (
|
||||
sortedSettingsAllRoles.map((role) => (
|
||||
filteredRoles.map((role) => (
|
||||
<SettingsRolesTableRow key={role.id} role={role} />
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ROLES_LIST_TABS = {
|
||||
COMPONENT_INSTANCE_ID: 'settings-roles-list-tabs',
|
||||
TABS_IDS: {
|
||||
USER_ROLES: 'user-roles',
|
||||
AGENT_ROLES: 'agent-roles',
|
||||
API_KEY_ROLES: 'api-key-roles',
|
||||
},
|
||||
} as const;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const API_KEY_FOR_ROLE_FRAGMENT = gql`
|
||||
fragment ApiKeyForRoleFragment on ApiKeyForRole {
|
||||
id
|
||||
name
|
||||
expiresAt
|
||||
revokedAt
|
||||
}
|
||||
`;
|
||||
@@ -13,5 +13,8 @@ export const ROLE_FRAGMENT = gql`
|
||||
canUpdateAllObjectRecords
|
||||
canSoftDeleteAllObjectRecords
|
||||
canDestroyAllObjectRecords
|
||||
canBeAssignedToUsers
|
||||
canBeAssignedToAgents
|
||||
canBeAssignedToApiKeys
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AGENT_FRAGMENT } from '@/ai/graphql/fragments/agentFragment';
|
||||
import { API_KEY_FOR_ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/apiKeyForRoleFragment';
|
||||
import { FIELD_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/fieldPermissionFragment';
|
||||
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
|
||||
import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment';
|
||||
@@ -8,6 +10,8 @@ import { gql } from '@apollo/client';
|
||||
export const GET_ROLES = gql`
|
||||
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
${ROLE_FRAGMENT}
|
||||
${AGENT_FRAGMENT}
|
||||
${API_KEY_FOR_ROLE_FRAGMENT}
|
||||
${PERMISSION_FLAG_FRAGMENT}
|
||||
${OBJECT_PERMISSION_FRAGMENT}
|
||||
${FIELD_PERMISSION_FRAGMENT}
|
||||
@@ -17,6 +21,12 @@ export const GET_ROLES = gql`
|
||||
workspaceMembers {
|
||||
...WorkspaceMemberQueryFragment
|
||||
}
|
||||
agents {
|
||||
...AgentFields
|
||||
}
|
||||
apiKeys {
|
||||
...ApiKeyForRoleFragment
|
||||
}
|
||||
permissionFlags {
|
||||
...PermissionFlagFragment
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
useAssignRoleToAgentMutation,
|
||||
type Agent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateAgentRole = (roleId: string) => {
|
||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const [assignRoleToAgentMutation] = useAssignRoleToAgentMutation();
|
||||
|
||||
const updateAgentRoleDraftState = ({ agent }: { agent: Agent }) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
agents: [...settingsDraftRole.agents, agent],
|
||||
});
|
||||
};
|
||||
|
||||
const addAgentToRoleAndUpdateState = async ({
|
||||
agentId,
|
||||
}: {
|
||||
agentId: string;
|
||||
}) => {
|
||||
const { data } = await assignRoleToAgentMutation({
|
||||
variables: {
|
||||
agentId,
|
||||
roleId,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: ['GetRoles'],
|
||||
});
|
||||
|
||||
return data?.assignRoleToAgent;
|
||||
};
|
||||
|
||||
const addAgentsToRole = async ({
|
||||
roleId,
|
||||
agentIds,
|
||||
}: {
|
||||
roleId: string;
|
||||
agentIds: string[];
|
||||
}) => {
|
||||
await Promise.all(
|
||||
agentIds.map((agentId) =>
|
||||
assignRoleToAgentMutation({
|
||||
variables: {
|
||||
roleId,
|
||||
agentId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
addAgentToRoleAndUpdateState,
|
||||
updateAgentRoleDraftState,
|
||||
addAgentsToRole,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useAssignRoleToApiKeyMutation } from '~/generated-metadata/graphql';
|
||||
import { type ApiKeyForRole } from '~/generated/graphql';
|
||||
|
||||
export const useUpdateApiKeyRole = (roleId: string) => {
|
||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const [assignRoleToApiKeyMutation] = useAssignRoleToApiKeyMutation();
|
||||
|
||||
const updateApiKeyRoleDraftState = ({
|
||||
apiKey,
|
||||
}: {
|
||||
apiKey: ApiKeyForRole;
|
||||
}) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
apiKeys: [...settingsDraftRole.apiKeys, apiKey],
|
||||
});
|
||||
};
|
||||
|
||||
const addApiKeyToRoleAndUpdateState = async ({
|
||||
apiKeyId,
|
||||
}: {
|
||||
apiKeyId: string;
|
||||
}) => {
|
||||
const { data } = await assignRoleToApiKeyMutation({
|
||||
variables: {
|
||||
apiKeyId,
|
||||
roleId,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: ['GetRoles'],
|
||||
});
|
||||
|
||||
return data?.assignRoleToApiKey;
|
||||
};
|
||||
|
||||
const addApiKeysToRole = async ({
|
||||
roleId,
|
||||
apiKeyIds,
|
||||
}: {
|
||||
roleId: string;
|
||||
apiKeyIds: string[];
|
||||
}) => {
|
||||
await Promise.all(
|
||||
apiKeyIds.map((apiKeyId) =>
|
||||
assignRoleToApiKeyMutation({
|
||||
variables: {
|
||||
roleId,
|
||||
apiKeyId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
addApiKeyToRoleAndUpdateState,
|
||||
updateApiKeyRoleDraftState,
|
||||
addApiKeysToRole,
|
||||
};
|
||||
};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SettingsRoleAssignmentEntityPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentEntityPickerDropdown';
|
||||
import { SettingsRoleAssignmentTable } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentTable';
|
||||
import { SettingsRoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import styled from '@emotion/styled';
|
||||
import { AppTooltip, IconPlus, TooltipDelay } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type Agent, type Role } from '~/generated-metadata/graphql';
|
||||
import { type ApiKeyForRole } from '~/generated/graphql';
|
||||
import { ROLE_TARGET_CONFIG } from '../constants/RoleTargetConfig';
|
||||
|
||||
const StyledAssignToMemberContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-block: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type RoleAssignmentSectionProps = {
|
||||
roleTargetType: keyof typeof ROLE_TARGET_CONFIG;
|
||||
roleId: string;
|
||||
settingsDraftRole: Role;
|
||||
currentWorkspaceMember?: CurrentWorkspaceMember;
|
||||
onSelect: (
|
||||
roleTarget: CurrentWorkspaceMember | Agent | ApiKeyForRole,
|
||||
roleTargetType: keyof typeof ROLE_TARGET_CONFIG,
|
||||
) => void;
|
||||
allWorkspaceMembersHaveThisRole?: boolean;
|
||||
};
|
||||
|
||||
export const RoleAssignmentSection = ({
|
||||
roleTargetType,
|
||||
roleId,
|
||||
settingsDraftRole,
|
||||
currentWorkspaceMember,
|
||||
onSelect,
|
||||
allWorkspaceMembersHaveThisRole,
|
||||
}: RoleAssignmentSectionProps) => {
|
||||
const config = ROLE_TARGET_CONFIG[roleTargetType];
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
if (!config.canBeAssigned(settingsDraftRole)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assignedIds = config.getAssignedIds(settingsDraftRole);
|
||||
const excludedIds = config.getExcludedIds(
|
||||
assignedIds,
|
||||
currentWorkspaceMember?.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<SettingsRoleAssignmentTable
|
||||
roleId={roleId}
|
||||
roleTargetType={roleTargetType}
|
||||
/>
|
||||
<StyledAssignToMemberContainer>
|
||||
<Dropdown
|
||||
dropdownId={config.dropdownId}
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
clickableComponent={
|
||||
<>
|
||||
<div id={config.tooltip?.anchorId}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={config.buttonTitle()}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={allWorkspaceMembersHaveThisRole}
|
||||
/>
|
||||
</div>
|
||||
{config.tooltip && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${config.tooltip.anchorId}`}
|
||||
content={config.tooltip.content()}
|
||||
delay={TooltipDelay.noDelay}
|
||||
hidden={
|
||||
!config.tooltip.shouldShow(allWorkspaceMembersHaveThisRole)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
dropdownComponents={
|
||||
roleTargetType === 'member' ? (
|
||||
<SettingsRoleAssignmentWorkspaceMemberPickerDropdown
|
||||
excludedWorkspaceMemberIds={excludedIds}
|
||||
onSelect={(roleTarget: CurrentWorkspaceMember) => {
|
||||
closeDropdown(config.dropdownId);
|
||||
onSelect(roleTarget, roleTargetType);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SettingsRoleAssignmentEntityPickerDropdown
|
||||
entityType={roleTargetType}
|
||||
excludedIds={excludedIds}
|
||||
onSelect={(
|
||||
roleTarget: CurrentWorkspaceMember | Agent | ApiKeyForRole,
|
||||
) => {
|
||||
closeDropdown(config.dropdownId);
|
||||
onSelect(roleTarget, roleTargetType);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledAssignToMemberContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+142
-196
@@ -1,72 +1,33 @@
|
||||
import {
|
||||
type CurrentWorkspaceMember,
|
||||
currentWorkspaceMemberState,
|
||||
type CurrentWorkspaceMember,
|
||||
} from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { useUpdateAgentRole } from '@/settings/roles/hooks/useUpdateAgentRole';
|
||||
import { useUpdateApiKeyRole } from '@/settings/roles/hooks/useUpdateApiKeyRole';
|
||||
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
|
||||
import { RoleAssignmentSection } from '@/settings/roles/role-assignment/components/RoleAssignmentSection';
|
||||
import { SettingsRoleAssignmentConfirmationModal } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModal';
|
||||
import { SettingsRoleAssignmentTableHeader } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader';
|
||||
import { SettingsRoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown';
|
||||
import { type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||
import { type SettingsRoleAssignmentConfirmationModalSelectedRoleTarget } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedRoleTarget';
|
||||
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
AppTooltip,
|
||||
H2Title,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type Role, type WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
useFindManyAgentsQuery,
|
||||
useGetApiKeysQuery,
|
||||
type Agent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey, type ApiKeyForRole } from '~/generated/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID } from '../constants/RoleAssignmentConfirmationModalId';
|
||||
import { SettingsRoleAssignmentTableRow } from './SettingsRoleAssignmentTableRow';
|
||||
|
||||
const StyledAssignToMemberContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSearchContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
input {
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTable = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
const StyledTableRows = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledNoMembers = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
import { ROLE_TARGET_CONFIG } from '../constants/RoleTargetConfig';
|
||||
import { buildRoleMaps } from '../utils/build-role-maps';
|
||||
|
||||
type SettingsRoleAssignmentProps = {
|
||||
roleId: string;
|
||||
@@ -77,6 +38,8 @@ export const SettingsRoleAssignment = ({
|
||||
roleId,
|
||||
isCreateMode,
|
||||
}: SettingsRoleAssignmentProps) => {
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
@@ -87,49 +50,29 @@ export const SettingsRoleAssignment = ({
|
||||
updateWorkspaceMemberRoleDraftState,
|
||||
} = useUpdateWorkspaceMemberRole(roleId);
|
||||
|
||||
const { addAgentToRoleAndUpdateState, updateAgentRoleDraftState } =
|
||||
useUpdateAgentRole(roleId);
|
||||
|
||||
const { addApiKeyToRoleAndUpdateState, updateApiKeyRoleDraftState } =
|
||||
useUpdateApiKeyRole(roleId);
|
||||
|
||||
const { data: agentsData } = useFindManyAgentsQuery();
|
||||
const { data: apiKeysData } = useGetApiKeysQuery();
|
||||
|
||||
const { openModal, closeModal } = useModal();
|
||||
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
|
||||
useState<SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
|
||||
const [selectedRoleTarget, setSelectRoleTarget] =
|
||||
useState<SettingsRoleAssignmentConfirmationModalSelectedRoleTarget | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const dropdownId = 'role-member-select';
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
|
||||
|
||||
const workspaceMemberRoleMap = new Map<
|
||||
string,
|
||||
{ id: string; label: string }
|
||||
>();
|
||||
settingsAllRoles.forEach((role: Role) => {
|
||||
role.workspaceMembers.forEach((member: WorkspaceMember) => {
|
||||
workspaceMemberRoleMap.set(member.id, { id: role.id, label: role.label });
|
||||
});
|
||||
});
|
||||
const roleMaps = buildRoleMaps(settingsAllRoles);
|
||||
|
||||
const filteredWorkspaceMembers = !searchFilter
|
||||
? settingsDraftRole.workspaceMembers
|
||||
: settingsDraftRole.workspaceMembers.filter((member) => {
|
||||
const searchTerm = searchFilter.toLowerCase();
|
||||
const firstName = member.name.firstName?.toLowerCase() || '';
|
||||
const lastName = member.name.lastName?.toLowerCase() || '';
|
||||
const email = member.userEmail?.toLowerCase() || '';
|
||||
|
||||
return (
|
||||
firstName.includes(searchTerm) ||
|
||||
lastName.includes(searchTerm) ||
|
||||
email.includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
const assignedWorkspaceMemberIds = settingsDraftRole.workspaceMembers.map(
|
||||
(workspaceMember) => workspaceMember.id,
|
||||
);
|
||||
const assignedWorkspaceMemberIds =
|
||||
ROLE_TARGET_CONFIG.member.getAssignedIds(settingsDraftRole);
|
||||
|
||||
const assignableWorkspaceMembers = currentWorkspaceMembers.filter(
|
||||
(member) => member.id !== currentWorkspaceMember?.id,
|
||||
@@ -140,21 +83,7 @@ export const SettingsRoleAssignment = ({
|
||||
);
|
||||
|
||||
const handleModalClose = () => {
|
||||
setSelectedWorkspaceMember(null);
|
||||
};
|
||||
|
||||
const handleSelectWorkspaceMember = (
|
||||
workspaceMember: CurrentWorkspaceMember,
|
||||
) => {
|
||||
const existingRole = workspaceMemberRoleMap.get(workspaceMember.id);
|
||||
|
||||
setSelectedWorkspaceMember({
|
||||
id: workspaceMember.id,
|
||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||
role: existingRole,
|
||||
});
|
||||
openModal(ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID);
|
||||
closeDropdown(dropdownId);
|
||||
setSelectRoleTarget(null);
|
||||
};
|
||||
|
||||
const isModalOpened = useRecoilComponentValue(
|
||||
@@ -163,124 +92,141 @@ export const SettingsRoleAssignment = ({
|
||||
);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedWorkspaceMember || !isModalOpened) return;
|
||||
if (!selectedRoleTarget || !isModalOpened) return;
|
||||
|
||||
if (!isCreateMode) {
|
||||
await addWorkspaceMemberToRoleAndUpdateState({
|
||||
workspaceMemberId: selectedWorkspaceMember.id,
|
||||
});
|
||||
} else {
|
||||
const workspaceMember = currentWorkspaceMembers.find(
|
||||
(member) => member.id === selectedWorkspaceMember.id,
|
||||
);
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new Error('Workspace member not found');
|
||||
switch (selectedRoleTarget.entityType) {
|
||||
case 'member':
|
||||
await addWorkspaceMemberToRoleAndUpdateState({
|
||||
workspaceMemberId: selectedRoleTarget.id,
|
||||
});
|
||||
break;
|
||||
case 'agent':
|
||||
await addAgentToRoleAndUpdateState({
|
||||
agentId: selectedRoleTarget.id,
|
||||
});
|
||||
break;
|
||||
case 'apiKey':
|
||||
await addApiKeyToRoleAndUpdateState({
|
||||
apiKeyId: selectedRoleTarget.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (selectedRoleTarget.entityType) {
|
||||
case 'member': {
|
||||
const member = currentWorkspaceMembers.find(
|
||||
(member) => member.id === selectedRoleTarget.id,
|
||||
);
|
||||
|
||||
updateWorkspaceMemberRoleDraftState({
|
||||
workspaceMember: {
|
||||
id: workspaceMember.id,
|
||||
name: workspaceMember.name,
|
||||
colorScheme: '',
|
||||
userEmail: '',
|
||||
},
|
||||
});
|
||||
if (!member) {
|
||||
throw new Error('Workspace member not found');
|
||||
}
|
||||
|
||||
updateWorkspaceMemberRoleDraftState({
|
||||
workspaceMember: {
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
colorScheme: '',
|
||||
userEmail: '',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'agent': {
|
||||
const agent = agentsData?.findManyAgents.find(
|
||||
(agent) => agent.id === selectedRoleTarget.id,
|
||||
);
|
||||
|
||||
if (!agent) {
|
||||
throw new Error('Agent not found');
|
||||
}
|
||||
|
||||
updateAgentRoleDraftState({
|
||||
agent,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'apiKey': {
|
||||
const apiKeyData = apiKeysData?.apiKeys.find(
|
||||
(apiKey) => apiKey.id === selectedRoleTarget.id,
|
||||
);
|
||||
|
||||
if (!apiKeyData) {
|
||||
throw new Error('API key not found');
|
||||
}
|
||||
|
||||
updateApiKeyRoleDraftState({
|
||||
apiKey: apiKeyData as ApiKeyForRole,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleModalClose();
|
||||
};
|
||||
|
||||
const handleSelectEntity = (
|
||||
entity: CurrentWorkspaceMember | Agent | ApiKeyForRole,
|
||||
entityType: keyof typeof ROLE_TARGET_CONFIG,
|
||||
) => {
|
||||
const config = ROLE_TARGET_CONFIG[entityType];
|
||||
const existingRole = config.getRoleMap(roleMaps).get(entity.id);
|
||||
const name = config.getName(entity as never);
|
||||
|
||||
setSelectRoleTarget({
|
||||
id: entity.id,
|
||||
name,
|
||||
role: existingRole,
|
||||
entityType: entityType as 'member' | 'agent' | 'apiKey',
|
||||
});
|
||||
|
||||
openModal(ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleRoleClick = (roleId: string) => {
|
||||
navigateSettings(SettingsPath.RoleDetail, { roleId });
|
||||
handleModalClose();
|
||||
closeModal(ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleSearchChange = (text: string) => {
|
||||
setSearchFilter(text);
|
||||
};
|
||||
if (!settingsDraftRole) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Assigned members`}
|
||||
description={t`This role is assigned to these workspace members.`}
|
||||
/>
|
||||
<StyledSearchContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="role-assignment-member-search"
|
||||
value={searchFilter}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t`Search an assigned team member...`}
|
||||
fullWidth
|
||||
LeftIcon={IconSearch}
|
||||
sizeVariant="lg"
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
<StyledTable>
|
||||
<SettingsRoleAssignmentTableHeader />
|
||||
<StyledTableRows>
|
||||
{filteredWorkspaceMembers.length > 0 ? (
|
||||
filteredWorkspaceMembers.map((workspaceMember) => (
|
||||
<SettingsRoleAssignmentTableRow
|
||||
key={workspaceMember.id}
|
||||
workspaceMember={workspaceMember}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<StyledNoMembers>
|
||||
{!searchFilter
|
||||
? t`No members assigned`
|
||||
: t`No members match your search`}
|
||||
</StyledNoMembers>
|
||||
)}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
{Object.keys(ROLE_TARGET_CONFIG).map(
|
||||
(roleTargetType) =>
|
||||
(isAiEnabled || roleTargetType !== 'agent') && (
|
||||
<RoleAssignmentSection
|
||||
key={roleTargetType}
|
||||
roleTargetType={roleTargetType as keyof typeof ROLE_TARGET_CONFIG}
|
||||
roleId={roleId}
|
||||
settingsDraftRole={settingsDraftRole}
|
||||
currentWorkspaceMember={
|
||||
roleTargetType === 'member'
|
||||
? currentWorkspaceMember || undefined
|
||||
: undefined
|
||||
}
|
||||
onSelect={handleSelectEntity}
|
||||
allWorkspaceMembersHaveThisRole={
|
||||
roleTargetType === 'member'
|
||||
? allWorkspaceMembersHaveThisRole
|
||||
: false
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
<StyledAssignToMemberContainer>
|
||||
<Dropdown
|
||||
dropdownId="role-member-select"
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
clickableComponent={
|
||||
<>
|
||||
<div id="assign-member">
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Assign to member`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={allWorkspaceMembersHaveThisRole}
|
||||
/>
|
||||
</div>
|
||||
<AppTooltip
|
||||
anchorSelect="#assign-member"
|
||||
content={t`The workspace needs at least one Admin`}
|
||||
delay={TooltipDelay.noDelay}
|
||||
hidden={!allWorkspaceMembersHaveThisRole}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
dropdownComponents={
|
||||
<SettingsRoleAssignmentWorkspaceMemberPickerDropdown
|
||||
excludedWorkspaceMemberIds={[
|
||||
...assignedWorkspaceMemberIds,
|
||||
currentWorkspaceMember?.id,
|
||||
]}
|
||||
onSelect={handleSelectWorkspaceMember}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledAssignToMemberContainer>
|
||||
</Section>
|
||||
|
||||
{selectedWorkspaceMember && (
|
||||
{selectedRoleTarget && (
|
||||
<SettingsRoleAssignmentConfirmationModal
|
||||
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||
selectedRoleTarget={selectedRoleTarget}
|
||||
onClose={handleModalClose}
|
||||
onConfirm={handleConfirm}
|
||||
onRoleClick={handleRoleClick}
|
||||
newRoleName={settingsDraftRole.label}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+15
-9
@@ -1,35 +1,41 @@
|
||||
import { SettingsRoleAssignmentConfirmationModalSubtitle } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModalSubtitle';
|
||||
import { ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID } from '@/settings/roles/role-assignment/constants/RoleAssignmentConfirmationModalId';
|
||||
import { type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||
import { type SettingsRoleAssignmentConfirmationModalSelectedRoleTarget } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedRoleTarget';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
type SettingsRoleAssignmentConfirmationModalProps = {
|
||||
selectedWorkspaceMember: SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
||||
selectedRoleTarget: SettingsRoleAssignmentConfirmationModalSelectedRoleTarget;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
onRoleClick: (roleId: string) => void;
|
||||
newRoleName: string;
|
||||
};
|
||||
|
||||
export const SettingsRoleAssignmentConfirmationModal = ({
|
||||
selectedWorkspaceMember,
|
||||
selectedRoleTarget,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onRoleClick,
|
||||
newRoleName,
|
||||
}: SettingsRoleAssignmentConfirmationModalProps) => {
|
||||
const workspaceMemberName = selectedWorkspaceMember.name;
|
||||
const roleTargetName = selectedRoleTarget.name;
|
||||
|
||||
const title = t`Assign ${workspaceMemberName}?`;
|
||||
const title = t`Assign ${roleTargetName}?`;
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
modalId={ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID}
|
||||
title={title}
|
||||
subtitle={
|
||||
<SettingsRoleAssignmentConfirmationModalSubtitle
|
||||
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||
onRoleClick={onRoleClick}
|
||||
/>
|
||||
selectedRoleTarget.role ? (
|
||||
<SettingsRoleAssignmentConfirmationModalSubtitle
|
||||
selectedRoleTarget={selectedRoleTarget}
|
||||
onRoleClick={onRoleClick}
|
||||
/>
|
||||
) : (
|
||||
t`${roleTargetName} will be assigned to the "${newRoleName}" role.`
|
||||
)
|
||||
}
|
||||
onClose={onClose}
|
||||
onConfirmClick={onConfirm}
|
||||
|
||||
+10
-8
@@ -1,6 +1,7 @@
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||
import { type SettingsRoleAssignmentConfirmationModalSelectedRoleTarget } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedRoleTarget';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -11,28 +12,30 @@ const StyledSettingsCardContainer = styled.div`
|
||||
`;
|
||||
|
||||
type SettingsRoleAssignmentConfirmationModalSubtitleProps = {
|
||||
selectedWorkspaceMember: SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
||||
selectedRoleTarget: SettingsRoleAssignmentConfirmationModalSelectedRoleTarget;
|
||||
onRoleClick: (roleId: string) => void;
|
||||
};
|
||||
|
||||
export const SettingsRoleAssignmentConfirmationModalSubtitle = ({
|
||||
selectedWorkspaceMember,
|
||||
selectedRoleTarget,
|
||||
onRoleClick,
|
||||
}: SettingsRoleAssignmentConfirmationModalSubtitleProps) => {
|
||||
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
|
||||
|
||||
const enrichedSelectedWorkspaceMember = currentWorkspaceMembers.find(
|
||||
(member) => member.id === selectedWorkspaceMember.id,
|
||||
(member) => member.id === selectedRoleTarget.id,
|
||||
);
|
||||
|
||||
const workspaceMemberName = `${enrichedSelectedWorkspaceMember?.name.firstName} ${enrichedSelectedWorkspaceMember?.name.lastName}`;
|
||||
const workspaceMemberName = enrichedSelectedWorkspaceMember
|
||||
? `${enrichedSelectedWorkspaceMember?.name.firstName} ${enrichedSelectedWorkspaceMember?.name.lastName}`
|
||||
: selectedRoleTarget.name;
|
||||
|
||||
return (
|
||||
<>
|
||||
{t`${workspaceMemberName} will be unassigned from the following role:`}
|
||||
<StyledSettingsCardContainer>
|
||||
<SettingsCard
|
||||
title={selectedWorkspaceMember.role?.label || ''}
|
||||
title={selectedRoleTarget.role?.label || ''}
|
||||
Icon={
|
||||
<Avatar
|
||||
avatarUrl={enrichedSelectedWorkspaceMember?.avatarUrl}
|
||||
@@ -43,8 +46,7 @@ export const SettingsRoleAssignmentConfirmationModalSubtitle = ({
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
selectedWorkspaceMember.role &&
|
||||
onRoleClick(selectedWorkspaceMember.role.id)
|
||||
selectedRoleTarget.role && onRoleClick(selectedRoleTarget.role.id)
|
||||
}
|
||||
/>
|
||||
</StyledSettingsCardContainer>
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
type Agent,
|
||||
useFindManyAgentsQuery,
|
||||
useGetApiKeysQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type ApiKeyForRole } from '~/generated/graphql';
|
||||
|
||||
const StyledLoadingContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledDropdownItem = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledItemName = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type EntityData = Agent | ApiKeyForRole;
|
||||
|
||||
type SettingsRoleAssignmentEntityPickerDropdownProps = {
|
||||
entityType: 'agent' | 'apiKey';
|
||||
excludedIds: string[];
|
||||
onSelect: (entity: EntityData) => void;
|
||||
};
|
||||
|
||||
export const SettingsRoleAssignmentEntityPickerDropdown = ({
|
||||
entityType,
|
||||
excludedIds,
|
||||
onSelect,
|
||||
}: SettingsRoleAssignmentEntityPickerDropdownProps) => {
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const { t } = useLingui();
|
||||
|
||||
const isAgent = entityType === 'agent';
|
||||
|
||||
const { data: agentsData, loading: agentsLoading } = useFindManyAgentsQuery({
|
||||
skip: !isAgent,
|
||||
});
|
||||
const { data: apiKeysData, loading: apiKeysLoading } = useGetApiKeysQuery({
|
||||
skip: isAgent,
|
||||
});
|
||||
|
||||
const loading = isAgent ? agentsLoading : apiKeysLoading;
|
||||
|
||||
const entities = ((isAgent
|
||||
? agentsData?.findManyAgents.filter((agent) => agent.isCustom)
|
||||
: apiKeysData?.apiKeys) || []) as EntityData[];
|
||||
|
||||
const placeholder = isAgent ? t`Search agents` : t`Search API keys`;
|
||||
|
||||
const getEmptyStateMessage = () => {
|
||||
if (searchFilter !== '') {
|
||||
return isAgent
|
||||
? t`No agents match your search`
|
||||
: t`No API keys match your search`;
|
||||
} else {
|
||||
return isAgent ? t`No agents available` : t`No API keys available`;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEntities = entities.filter((entity) => {
|
||||
const isExcluded = excludedIds.includes(entity.id);
|
||||
|
||||
if (isExcluded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAgent) {
|
||||
const agent = entity as Agent;
|
||||
return (
|
||||
agent.name.toLowerCase().includes(searchFilter.toLowerCase()) ||
|
||||
agent.label.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
return (entity as ApiKeyForRole).name
|
||||
.toLowerCase()
|
||||
.includes(searchFilter.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.Medium}>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={(event) => setSearchFilter(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
<StyledLoadingContainer>{t`Loading...`}</StyledLoadingContainer>
|
||||
) : filteredEntities.length > 0 ? (
|
||||
filteredEntities.map((entity) => (
|
||||
<StyledDropdownItem
|
||||
key={entity.id}
|
||||
onClick={() => onSelect(entity as EntityData)}
|
||||
>
|
||||
<StyledItemName>
|
||||
{isAgent ? (entity as Agent).label : entity.name}
|
||||
</StyledItemName>
|
||||
</StyledDropdownItem>
|
||||
))
|
||||
) : (
|
||||
<StyledEmptyState>{getEmptyStateMessage()}</StyledEmptyState>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
SettingsRoleAssignmentTableRow,
|
||||
type RoleTarget,
|
||||
} from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentTableRow';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { H2Title, IconSearch } from 'twenty-ui/display';
|
||||
import { type Agent, type WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
import { type ApiKeyForRole } from '~/generated/graphql';
|
||||
|
||||
const StyledTable = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
const StyledTableRows = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
padding-block: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledSearchContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
input {
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
}
|
||||
`;
|
||||
|
||||
type RoleTargetType = 'member' | 'agent' | 'apiKey';
|
||||
|
||||
type SettingsRoleAssignmentTableProps<T extends RoleTargetType> = {
|
||||
roleTargetType: T;
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export const SettingsRoleAssignmentTable = <T extends RoleTargetType>({
|
||||
roleTargetType,
|
||||
roleId,
|
||||
}: SettingsRoleAssignmentTableProps<T>) => {
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const handleSearchChange = (text: string) => {
|
||||
setSearchFilter(text);
|
||||
};
|
||||
|
||||
const tableConfig = {
|
||||
member: {
|
||||
columns: [t`Name`, t`Email`],
|
||||
displayName: t`Members`,
|
||||
emptyStateText: t`No members assigned`,
|
||||
roleTargets: settingsDraftRole.workspaceMembers,
|
||||
},
|
||||
agent: {
|
||||
columns: [t`Name`, t`Description`],
|
||||
displayName: t`Agents`,
|
||||
emptyStateText: t`No agents assigned`,
|
||||
roleTargets: settingsDraftRole.agents,
|
||||
},
|
||||
apiKey: {
|
||||
columns: [t`Name`, t`Expires`],
|
||||
displayName: t`API keys`,
|
||||
emptyStateText: t`No API keys assigned`,
|
||||
roleTargets: settingsDraftRole.apiKeys,
|
||||
},
|
||||
};
|
||||
|
||||
const roleTargetDisplayName = tableConfig[roleTargetType].displayName;
|
||||
|
||||
const roleTargets = tableConfig[roleTargetType].roleTargets;
|
||||
|
||||
const getSearchableFields = (
|
||||
roleTarget: WorkspaceMember | Agent | ApiKeyForRole,
|
||||
): string[] => {
|
||||
switch (roleTargetType) {
|
||||
case 'member': {
|
||||
const member = roleTarget as WorkspaceMember;
|
||||
return [
|
||||
member.name.firstName?.toLowerCase() || '',
|
||||
member.name.lastName?.toLowerCase() || '',
|
||||
member.userEmail?.toLowerCase() || '',
|
||||
];
|
||||
}
|
||||
case 'agent': {
|
||||
const agent = roleTarget as Agent;
|
||||
return [
|
||||
agent.name?.toLowerCase() || '',
|
||||
agent.label?.toLowerCase() || '',
|
||||
agent.description?.toLowerCase() || '',
|
||||
];
|
||||
}
|
||||
case 'apiKey': {
|
||||
const apiKey = roleTarget as ApiKeyForRole;
|
||||
return [apiKey.name?.toLowerCase() || ''];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRoleTargets = !searchFilter
|
||||
? roleTargets
|
||||
: roleTargets.filter((roleTarget) => {
|
||||
const searchTerm = searchFilter.toLowerCase();
|
||||
const searchableFields = getSearchableFields(roleTarget);
|
||||
return searchableFields.some((field) => field.includes(searchTerm));
|
||||
});
|
||||
|
||||
const createRoleTarget = (
|
||||
roleTarget: WorkspaceMember | Agent | ApiKeyForRole,
|
||||
): RoleTarget => {
|
||||
switch (roleTargetType) {
|
||||
case 'member':
|
||||
return { type: roleTargetType, data: roleTarget as WorkspaceMember };
|
||||
case 'agent':
|
||||
return { type: roleTargetType, data: roleTarget as Agent };
|
||||
case 'apiKey':
|
||||
return { type: roleTargetType, data: roleTarget as ApiKeyForRole };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<H2Title
|
||||
title={t`Assigned ${roleTargetDisplayName}`}
|
||||
description={t`This role is assigned to these ${roleTargetDisplayName}.`}
|
||||
/>
|
||||
<StyledSearchContainer>
|
||||
<StyledSearchInput
|
||||
instanceId={`role-assignment-${roleTargetType}-search`}
|
||||
value={searchFilter}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t`Search an assigned ${roleTargetDisplayName}...`}
|
||||
fullWidth
|
||||
LeftIcon={IconSearch}
|
||||
sizeVariant="lg"
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
<StyledTable>
|
||||
<TableRow gridAutoColumns="2fr 4fr">
|
||||
{tableConfig[roleTargetType].columns.map((column) => (
|
||||
<TableHeader key={column}>{column}</TableHeader>
|
||||
))}
|
||||
</TableRow>
|
||||
<StyledTableRows>
|
||||
{filteredRoleTargets.map((roleTarget) => (
|
||||
<SettingsRoleAssignmentTableRow
|
||||
key={roleTarget.id}
|
||||
roleTarget={createRoleTarget(roleTarget)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{filteredRoleTargets.length === 0 && (
|
||||
<StyledEmptyState>
|
||||
{tableConfig[roleTargetType].emptyStateText}
|
||||
</StyledEmptyState>
|
||||
)}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+83
-21
@@ -1,16 +1,26 @@
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { type WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
Avatar,
|
||||
IconKey,
|
||||
OverflowingTextWithTooltip,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { type Agent, type WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
import { type ApiKeyForRole } from '~/generated/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { formatDateString } from '~/utils/string/formatDateString';
|
||||
|
||||
const StyledIconWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledNameCell = styled.div`
|
||||
@@ -23,6 +33,7 @@ const StyledNameContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@@ -30,40 +41,91 @@ const StyledTableCell = styled(TableCell)`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export type RoleTarget =
|
||||
| { type: 'member'; data: WorkspaceMember }
|
||||
| { type: 'agent'; data: Agent }
|
||||
| { type: 'apiKey'; data: ApiKeyForRole };
|
||||
|
||||
type SettingsRoleAssignmentTableRowProps = {
|
||||
workspaceMember: WorkspaceMember;
|
||||
roleTarget: RoleTarget;
|
||||
};
|
||||
|
||||
export const SettingsRoleAssignmentTableRow = ({
|
||||
workspaceMember,
|
||||
roleTarget,
|
||||
}: SettingsRoleAssignmentTableRowProps) => {
|
||||
const theme = useTheme();
|
||||
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
|
||||
const enrichedWorkspaceMember = currentWorkspaceMembers.find(
|
||||
(member) => member.id === workspaceMember.id,
|
||||
);
|
||||
const { getIcon } = useIcons();
|
||||
const { dateFormat, timeZone } = useContext(UserContext);
|
||||
const dateLocale = useRecoilValue(dateLocaleState);
|
||||
|
||||
const renderIcon = () => {
|
||||
switch (roleTarget.type) {
|
||||
case 'member': {
|
||||
const enrichedWorkspaceMember = currentWorkspaceMembers.find(
|
||||
(member) => member.id === roleTarget.data.id,
|
||||
);
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={enrichedWorkspaceMember?.avatarUrl}
|
||||
placeholderColorSeed={enrichedWorkspaceMember?.id}
|
||||
placeholder={enrichedWorkspaceMember?.name.firstName ?? ''}
|
||||
type="rounded"
|
||||
size="md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'agent': {
|
||||
const Icon = getIcon(roleTarget.data.icon || 'IconRobot');
|
||||
return <Icon size={theme.icon.size.md} />;
|
||||
}
|
||||
case 'apiKey': {
|
||||
return <IconKey size={theme.icon.size.md} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderName = () => {
|
||||
switch (roleTarget.type) {
|
||||
case 'member':
|
||||
return `${roleTarget.data.name.firstName} ${roleTarget.data.name.lastName}`;
|
||||
case 'agent':
|
||||
return roleTarget.data.label;
|
||||
case 'apiKey':
|
||||
return roleTarget.data.name;
|
||||
}
|
||||
};
|
||||
|
||||
const renderSecondaryInfo = () => {
|
||||
switch (roleTarget.type) {
|
||||
case 'member':
|
||||
return roleTarget.data.userEmail;
|
||||
case 'agent':
|
||||
return roleTarget.data.description;
|
||||
case 'apiKey':
|
||||
return roleTarget.data.expiresAt
|
||||
? formatDateString({
|
||||
value: roleTarget.data.expiresAt,
|
||||
timeZone,
|
||||
dateFormat,
|
||||
localeCatalog: dateLocale.localeCatalog,
|
||||
})
|
||||
: 'Never expires';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow gridAutoColumns="2fr 4fr">
|
||||
<StyledTableCell>
|
||||
<StyledNameContainer>
|
||||
<StyledIconWrapper>
|
||||
<Avatar
|
||||
avatarUrl={enrichedWorkspaceMember?.avatarUrl}
|
||||
placeholderColorSeed={enrichedWorkspaceMember?.id}
|
||||
placeholder={enrichedWorkspaceMember?.name.firstName ?? ''}
|
||||
type="rounded"
|
||||
size="md"
|
||||
/>
|
||||
</StyledIconWrapper>
|
||||
<StyledIconWrapper>{renderIcon()}</StyledIconWrapper>
|
||||
<StyledNameCell>
|
||||
<OverflowingTextWithTooltip
|
||||
text={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
|
||||
/>
|
||||
<OverflowingTextWithTooltip text={renderName()} />
|
||||
</StyledNameCell>
|
||||
</StyledNameContainer>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<OverflowingTextWithTooltip text={workspaceMember.userEmail} />
|
||||
<OverflowingTextWithTooltip text={renderSecondaryInfo()} />
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SettingsRoleAssignmentEntityPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentEntityPickerDropdown';
|
||||
import { SettingsRoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
type Agent,
|
||||
type Role,
|
||||
type WorkspaceMember,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type ApiKeyForRole } from '~/generated/graphql';
|
||||
import { type RoleMaps } from '../types/role-maps';
|
||||
|
||||
export const ROLE_TARGET_CONFIG = {
|
||||
member: {
|
||||
dropdownId: 'role-member-select',
|
||||
getRoleMap: (maps: RoleMaps) => maps.member,
|
||||
getName: (entity: CurrentWorkspaceMember) =>
|
||||
`${entity.name.firstName} ${entity.name.lastName}`,
|
||||
getAssignedIds: (settingsDraftRole: Role) =>
|
||||
settingsDraftRole.workspaceMembers.map(
|
||||
(member: WorkspaceMember) => member.id,
|
||||
),
|
||||
getExcludedIds: (assignedIds: string[], currentMemberId?: string) =>
|
||||
currentMemberId ? [...assignedIds, currentMemberId] : assignedIds,
|
||||
canBeAssigned: (settingsDraftRole: Role) =>
|
||||
settingsDraftRole.canBeAssignedToUsers,
|
||||
buttonTitle: () => t`Assign to member`,
|
||||
dropdownComponent: SettingsRoleAssignmentWorkspaceMemberPickerDropdown,
|
||||
// Tooltip configuration for member type
|
||||
tooltip: {
|
||||
anchorId: 'assign-member',
|
||||
content: () => t`All workspace members already have this role`,
|
||||
shouldShow: (allWorkspaceMembersHaveThisRole?: boolean) =>
|
||||
allWorkspaceMembersHaveThisRole ?? false,
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
dropdownId: 'role-agent-select',
|
||||
getRoleMap: (maps: RoleMaps) => maps.agent,
|
||||
getName: (entity: Agent) => entity.label,
|
||||
getAssignedIds: (settingsDraftRole: Role) =>
|
||||
(settingsDraftRole.agents || []).map((agent: Agent) => agent.id),
|
||||
getExcludedIds: (assignedIds: string[]) => assignedIds,
|
||||
canBeAssigned: (settingsDraftRole: Role) =>
|
||||
settingsDraftRole.canBeAssignedToAgents,
|
||||
buttonTitle: () => t`Assign to agent`,
|
||||
dropdownComponent: SettingsRoleAssignmentEntityPickerDropdown,
|
||||
// No tooltip for agent type
|
||||
tooltip: null,
|
||||
},
|
||||
apiKey: {
|
||||
dropdownId: 'role-api-key-select',
|
||||
getRoleMap: (maps: RoleMaps) => maps.apiKey,
|
||||
getName: (entity: ApiKeyForRole) => entity.name,
|
||||
getAssignedIds: (settingsDraftRole: Role) =>
|
||||
(settingsDraftRole.apiKeys || []).map(
|
||||
(apiKey: ApiKeyForRole) => apiKey.id,
|
||||
),
|
||||
getExcludedIds: (assignedIds: string[]) => assignedIds,
|
||||
canBeAssigned: (settingsDraftRole: Role) =>
|
||||
settingsDraftRole.canBeAssignedToApiKeys,
|
||||
buttonTitle: () => t`Assign to API key`,
|
||||
dropdownComponent: SettingsRoleAssignmentEntityPickerDropdown,
|
||||
// No tooltip for apiKey type
|
||||
tooltip: null,
|
||||
},
|
||||
} as const;
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
export type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember = {
|
||||
export type SettingsRoleAssignmentConfirmationModalSelectedRoleTarget = {
|
||||
id: string;
|
||||
name: string;
|
||||
role?: { id: string; label: string };
|
||||
colorScheme?: string;
|
||||
userEmail?: string;
|
||||
entityType: 'member' | 'agent' | 'apiKey';
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type RoleMaps = {
|
||||
member: Map<string, { id: string; label: string }>;
|
||||
agent: Map<string, { id: string; label: string }>;
|
||||
apiKey: Map<string, { id: string; label: string }>;
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { type Role } from '~/generated-metadata/graphql';
|
||||
import { type RoleMaps } from '../types/role-maps';
|
||||
|
||||
export const buildRoleMaps = (roles: Role[]): RoleMaps => {
|
||||
const maps: RoleMaps = {
|
||||
member: new Map(),
|
||||
agent: new Map(),
|
||||
apiKey: new Map(),
|
||||
};
|
||||
|
||||
roles.forEach((role) => {
|
||||
role.workspaceMembers.forEach((member) => {
|
||||
maps.member.set(member.id, { id: role.id, label: role.label });
|
||||
});
|
||||
role.agents?.forEach((agent) => {
|
||||
maps.agent.set(agent.id, { id: role.id, label: role.label });
|
||||
});
|
||||
role.apiKeys?.forEach((apiKey) => {
|
||||
maps.apiKey.set(apiKey.id, { id: role.id, label: role.label });
|
||||
});
|
||||
});
|
||||
|
||||
return maps;
|
||||
};
|
||||
+5
@@ -17,9 +17,14 @@ const BASE_ROLE_MOCK: Role = {
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canUpdateAllSettings: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
id: '1',
|
||||
isEditable: true,
|
||||
label: 'Role base',
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
workspaceMembers: [],
|
||||
fieldPermissions: [BASE_FIELD_PERMISSION],
|
||||
};
|
||||
|
||||
+5
@@ -8,9 +8,14 @@ const BASE_ROLE_MOCK: Role = {
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canUpdateAllSettings: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
id: '1',
|
||||
isEditable: true,
|
||||
label: 'Role base',
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
workspaceMembers: [],
|
||||
};
|
||||
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { H2Title, IconKey, IconRobot, IconUsers } from 'twenty-ui/display';
|
||||
import { Checkbox } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const StyledCheckboxContainer = styled.div<{ disabled: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
transition: background-color
|
||||
${({ theme }) => theme.animation.duration.normal}s ease;
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCheckboxLabel = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type SettingsRoleApplicabilityValues = {
|
||||
canBeAssignedToUsers: boolean;
|
||||
canBeAssignedToAgents: boolean;
|
||||
canBeAssignedToApiKeys: boolean;
|
||||
};
|
||||
|
||||
type SettingsRoleApplicabilityProps = {
|
||||
values: SettingsRoleApplicabilityValues;
|
||||
onApplicabilityChange: (
|
||||
key: keyof SettingsRoleApplicabilityValues,
|
||||
value: boolean,
|
||||
) => void;
|
||||
isEditable: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRoleApplicability = ({
|
||||
values,
|
||||
onApplicabilityChange,
|
||||
isEditable,
|
||||
}: SettingsRoleApplicabilityProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const options = [
|
||||
{
|
||||
key: 'canBeAssignedToUsers' as const,
|
||||
label: t`Assignable to team members`,
|
||||
Icon: IconUsers,
|
||||
},
|
||||
{
|
||||
key: 'canBeAssignedToAgents' as const,
|
||||
label: t`Assignable to Agents`,
|
||||
Icon: IconRobot,
|
||||
},
|
||||
...(isAiEnabled
|
||||
? [
|
||||
{
|
||||
key: 'canBeAssignedToApiKeys' as const,
|
||||
label: t`Assignable to API Keys`,
|
||||
Icon: IconKey,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Applicability`}
|
||||
description={t`Control which types of entities this role can be assigned to`}
|
||||
/>
|
||||
<div>
|
||||
{options.map((option) => (
|
||||
<StyledCheckboxContainer
|
||||
disabled={!isEditable}
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
isEditable &&
|
||||
onApplicabilityChange(option.key, !values[option.key])
|
||||
}
|
||||
>
|
||||
<StyledCheckboxLabel>
|
||||
<option.Icon size={theme.icon.size.sm} />
|
||||
<span>{option.label}</span>
|
||||
</StyledCheckboxLabel>
|
||||
<Checkbox
|
||||
checked={values[option.key]}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation();
|
||||
onApplicabilityChange(option.key, event.target.checked);
|
||||
}}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+16
@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { SettingsRoleApplicability } from '@/settings/roles/role-settings/components/SettingsRoleApplicability';
|
||||
import { SettingsRoleSettingsDeleteRoleConfirmationModal } from '@/settings/roles/role-settings/components/SettingsRoleSettingsDeleteRoleConfirmationModal';
|
||||
import { ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID } from '@/settings/roles/role-settings/components/constants/RoleSettingsDeleteRoleConfirmationModalId';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
@@ -91,6 +92,21 @@ export const SettingsRoleSettings = ({
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<SettingsRoleApplicability
|
||||
values={{
|
||||
canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
|
||||
canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
|
||||
canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
|
||||
}}
|
||||
onApplicabilityChange={(key, value) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
[key]: value,
|
||||
});
|
||||
}}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
|
||||
{!isCreateMode && (
|
||||
<>
|
||||
<Section>
|
||||
|
||||
+5
@@ -47,8 +47,13 @@ export const SettingsRoleCreateEffect = ({
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
isEditable: true,
|
||||
workspaceMembers: [],
|
||||
apiKeys: [],
|
||||
agents: [],
|
||||
};
|
||||
|
||||
setSettingsPersistedRole(undefined);
|
||||
|
||||
+38
-1
@@ -1,4 +1,6 @@
|
||||
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
|
||||
import { useUpdateAgentRole } from '@/settings/roles/hooks/useUpdateAgentRole';
|
||||
import { useUpdateApiKeyRole } from '@/settings/roles/hooks/useUpdateApiKeyRole';
|
||||
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
|
||||
import { useRemoveFieldPermissionInDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useRemoveFieldPermissionInDraftRole';
|
||||
import { newFieldPermissionsFilter } from '@/settings/roles/role/hooks/utils/newFieldPermissionsFilter.util';
|
||||
@@ -30,6 +32,9 @@ const ROLE_BASIC_KEYS: Array<keyof Role> = [
|
||||
'canUpdateAllObjectRecords',
|
||||
'canSoftDeleteAllObjectRecords',
|
||||
'canDestroyAllObjectRecords',
|
||||
'canBeAssignedToUsers',
|
||||
'canBeAssignedToAgents',
|
||||
'canBeAssignedToApiKeys',
|
||||
];
|
||||
|
||||
export const useSaveDraftRoleToDB = ({
|
||||
@@ -45,6 +50,8 @@ export const useSaveDraftRoleToDB = ({
|
||||
const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation();
|
||||
const [upsertFieldPermissions] = useUpsertFieldPermissionsMutation();
|
||||
const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId);
|
||||
const { addAgentsToRole } = useUpdateAgentRole(roleId);
|
||||
const { addApiKeysToRole } = useUpdateApiKeyRole(roleId);
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const settingsPersistedRole = useRecoilValue(
|
||||
@@ -125,6 +132,9 @@ export const useSaveDraftRoleToDB = ({
|
||||
settingsDraftRole.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords:
|
||||
settingsDraftRole.canDestroyAllObjectRecords,
|
||||
canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
|
||||
canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
|
||||
canBeAssignedToApiKeys: settingsDraftRole.canBeAssignedToApiKeys,
|
||||
} satisfies Partial<Role>,
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
|
||||
@@ -191,7 +201,10 @@ export const useSaveDraftRoleToDB = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(dirtyFields.workspaceMembers)) {
|
||||
if (
|
||||
isDefined(dirtyFields.workspaceMembers) &&
|
||||
settingsDraftRole.canBeAssignedToUsers
|
||||
) {
|
||||
await addWorkspaceMembersToRole({
|
||||
roleId: data.createOneRole.id,
|
||||
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
|
||||
@@ -200,6 +213,26 @@ export const useSaveDraftRoleToDB = ({
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
@@ -238,6 +271,10 @@ export const useSaveDraftRoleToDB = ({
|
||||
settingsDraftRole.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords:
|
||||
settingsDraftRole.canDestroyAllObjectRecords,
|
||||
canBeAssignedToUsers: settingsDraftRole.canBeAssignedToUsers,
|
||||
canBeAssignedToAgents: settingsDraftRole.canBeAssignedToAgents,
|
||||
canBeAssignedToApiKeys:
|
||||
settingsDraftRole.canBeAssignedToApiKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+5
@@ -19,5 +19,10 @@ export const settingsDraftRoleFamilyState = createFamilyState<Role, string>({
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
canBeAssignedToUsers: false,
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
<SubMenuTopBarContainer
|
||||
title={title}
|
||||
actionButton={
|
||||
!isEditMode || !isAskAIAgent ? (
|
||||
!isEditMode || agent?.isCustom ? (
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSave}
|
||||
onCancel={() => navigate(SettingsPath.AI)}
|
||||
@@ -210,6 +210,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
<SettingsAIAgentForm
|
||||
formValues={formValues}
|
||||
onFieldChange={handleFieldChange}
|
||||
disabled={isEditMode ? !agent?.isCustom : false}
|
||||
/>
|
||||
{isEditMode && agent && formValues.isCustom && (
|
||||
<Section>
|
||||
|
||||
+14
-5
@@ -39,11 +39,13 @@ type SettingsAIAgentFormProps = {
|
||||
field: keyof SettingsAIAgentFormValues,
|
||||
value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues],
|
||||
) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SettingsAIAgentForm = ({
|
||||
formValues,
|
||||
onFieldChange,
|
||||
disabled,
|
||||
}: SettingsAIAgentFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -51,10 +53,12 @@ export const SettingsAIAgentForm = ({
|
||||
const { data: rolesData } = useGetRolesQuery();
|
||||
|
||||
const rolesOptions =
|
||||
rolesData?.getRoles?.map((role) => ({
|
||||
label: role.label,
|
||||
value: role.id,
|
||||
})) || [];
|
||||
rolesData?.getRoles
|
||||
?.filter((role) => role.canBeAssignedToAgents)
|
||||
.map((role) => ({
|
||||
label: role.label,
|
||||
value: role.id,
|
||||
})) || [];
|
||||
|
||||
const noModelsAvailable = modelOptions.length === 0;
|
||||
|
||||
@@ -72,6 +76,7 @@ export const SettingsAIAgentForm = ({
|
||||
onChange={({ iconKey }) => {
|
||||
onFieldChange('icon', iconKey);
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<StyledNameContainer>
|
||||
@@ -84,6 +89,7 @@ export const SettingsAIAgentForm = ({
|
||||
fillNameFromLabel(value);
|
||||
}}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledNameContainer>
|
||||
</StyledIconNameRow>
|
||||
@@ -96,6 +102,7 @@ export const SettingsAIAgentForm = ({
|
||||
minRows={3}
|
||||
value={formValues.description || ''}
|
||||
onChange={(value) => onFieldChange('description', value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledFormContainer>
|
||||
|
||||
@@ -106,7 +113,7 @@ export const SettingsAIAgentForm = ({
|
||||
value={formValues.modelId}
|
||||
onChange={(value) => onFieldChange('modelId', value)}
|
||||
options={modelOptions}
|
||||
disabled={noModelsAvailable}
|
||||
disabled={noModelsAvailable || disabled}
|
||||
/>
|
||||
{noModelsAvailable && (
|
||||
<StyledErrorMessage>
|
||||
@@ -126,6 +133,7 @@ export const SettingsAIAgentForm = ({
|
||||
label: t`Select a role`,
|
||||
value: '',
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledFormContainer>
|
||||
|
||||
@@ -137,6 +145,7 @@ export const SettingsAIAgentForm = ({
|
||||
minRows={6}
|
||||
value={formValues.prompt}
|
||||
onChange={(value) => onFieldChange('prompt', value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledFormContainer>
|
||||
</StyledFormContainer>
|
||||
|
||||
+5
-1
@@ -39,7 +39,11 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
const defaultRole = data.getRoles.find(
|
||||
(role) => role.id === currentWorkspace?.defaultRole?.id,
|
||||
);
|
||||
if (isDefined(defaultRole) && !formValues.roleId) {
|
||||
if (
|
||||
isDefined(defaultRole) &&
|
||||
defaultRole.canBeAssignedToApiKeys &&
|
||||
!formValues.roleId
|
||||
) {
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
roleId: defaultRole.id,
|
||||
|
||||
@@ -15,6 +15,11 @@ const rolesMock: Role[] = [
|
||||
canUpdateAllSettings: true,
|
||||
canAccessAllTools: true,
|
||||
isEditable: false,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
workspaceMembers: [mockWorkspaceMembers[0]],
|
||||
permissionFlags: [
|
||||
{
|
||||
@@ -48,6 +53,11 @@ const rolesMock: Role[] = [
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: false,
|
||||
isEditable: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
workspaceMembers: [mockWorkspaceMembers[1]],
|
||||
permissionFlags: [],
|
||||
objectPermissions: [
|
||||
|
||||
@@ -59,9 +59,14 @@ export const BASE_ROLE_MOCK_GRANTS_ALL: Role = {
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
canUpdateAllSettings: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
id: MOCK_ROLE_ID_GRANTS_ALL,
|
||||
isEditable: true,
|
||||
label: 'Role grants all',
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
workspaceMembers: [],
|
||||
fieldPermissions: [
|
||||
{ ...BASE_FIELD_PERMISSION, roleId: MOCK_ROLE_ID_GRANTS_ALL },
|
||||
@@ -84,9 +89,14 @@ export const BASE_ROLE_MOCK_REVOKES_ALL: Role = {
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
id: MOCK_ROLE_ID_REVOKES_ALL,
|
||||
isEditable: true,
|
||||
label: 'Role revokes all',
|
||||
agents: [],
|
||||
apiKeys: [],
|
||||
workspaceMembers: [],
|
||||
fieldPermissions: [
|
||||
{ ...BASE_FIELD_PERMISSION, roleId: MOCK_ROLE_ID_REVOKES_ALL },
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddRoleApplicabilityFields1756434280823
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddRoleApplicabilityFields1756434280823';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."role" ADD "canBeAssignedToUsers" boolean NOT NULL DEFAULT true`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."role" ADD "canBeAssignedToAgents" boolean NOT NULL DEFAULT true`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."role" ADD "canBeAssignedToApiKeys" boolean NOT NULL DEFAULT true`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`UPDATE "core"."role" SET "canBeAssignedToUsers" = true, "canBeAssignedToAgents" = true, "canBeAssignedToApiKeys" = true`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."role" DROP COLUMN "canBeAssignedToApiKeys"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."role" DROP COLUMN "canBeAssignedToAgents"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."role" DROP COLUMN "canBeAssignedToUsers"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,9 @@ describe('ApiKeyRoleService', () => {
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
};
|
||||
|
||||
const mockNewRole: Partial<RoleEntity> = {
|
||||
@@ -427,6 +430,10 @@ describe('ApiKeyRoleService', () => {
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
standardId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource, type EntityManager, In, Repository } from 'typeorm';
|
||||
import {
|
||||
DataSource,
|
||||
type EntityManager,
|
||||
In,
|
||||
IsNull,
|
||||
Not,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import {
|
||||
@@ -145,6 +152,13 @@ export class ApiKeyRoleService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!role.canBeAssignedToApiKeys) {
|
||||
throw new ApiKeyException(
|
||||
`Role "${role.label}" cannot be assigned to API keys`,
|
||||
ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS,
|
||||
);
|
||||
}
|
||||
|
||||
const existingRoleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
apiKeyId,
|
||||
@@ -190,4 +204,35 @@ export class ApiKeyRoleService {
|
||||
|
||||
return rolesMap;
|
||||
}
|
||||
|
||||
public async getApiKeysAssignedToRole(
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ApiKey[]> {
|
||||
const roleTargets = await this.roleTargetsRepository.find({
|
||||
where: {
|
||||
roleId,
|
||||
workspaceId,
|
||||
apiKeyId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const apiKeyIds = roleTargets
|
||||
.map((roleTarget) => roleTarget.apiKeyId)
|
||||
.filter((apiKeyId): apiKeyId is string => apiKeyId !== null);
|
||||
|
||||
if (!apiKeyIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const apiKeys = await this.apiKeyRepository.find({
|
||||
where: {
|
||||
id: In(apiKeyIds),
|
||||
workspaceId,
|
||||
revokedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
return apiKeys;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ export enum ApiKeyExceptionCode {
|
||||
API_KEY_REVOKED = 'API_KEY_REVOKED',
|
||||
API_KEY_EXPIRED = 'API_KEY_EXPIRED',
|
||||
API_KEY_NO_ROLE_ASSIGNED = 'API_KEY_NO_ROLE_ASSIGNED',
|
||||
ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS = 'ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS',
|
||||
}
|
||||
|
||||
+4
@@ -27,6 +27,10 @@ export const apiKeyGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new ForbiddenError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS:
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
+7
-1
@@ -87,6 +87,9 @@ describe('AgentRoleService', () => {
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -102,6 +105,9 @@ describe('AgentRoleService', () => {
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -288,7 +294,7 @@ describe('AgentRoleService', () => {
|
||||
roleId: nonExistentRoleId,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
code: AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
message: `Role with id ${nonExistentRoleId} not found in workspace`,
|
||||
});
|
||||
});
|
||||
|
||||
+66
-2
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import {
|
||||
@@ -54,6 +54,33 @@ export class AgentRoleService {
|
||||
});
|
||||
}
|
||||
|
||||
public async assignStandardRoleToAgent({
|
||||
workspaceId,
|
||||
agentId,
|
||||
standardRoleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
standardRoleId: string;
|
||||
}) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { standardId: standardRoleId, workspaceId },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new AgentException(
|
||||
`Standard role with standard ID ${standardRoleId} not found in workspace`,
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.assignRoleToAgent({
|
||||
workspaceId,
|
||||
agentId,
|
||||
roleId: role.id,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeRoleFromAgent({
|
||||
workspaceId,
|
||||
agentId,
|
||||
@@ -67,6 +94,36 @@ export class AgentRoleService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getAgentsAssignedToRole(
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
): Promise<AgentEntity[]> {
|
||||
const roleTargets = await this.roleTargetsRepository.find({
|
||||
where: {
|
||||
roleId,
|
||||
workspaceId,
|
||||
agentId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const agentIds = roleTargets
|
||||
.map((roleTarget) => roleTarget.agentId)
|
||||
.filter((agentId): agentId is string => agentId !== null);
|
||||
|
||||
if (!agentIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const agents = await this.agentRepository.find({
|
||||
where: {
|
||||
id: In(agentIds),
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
private async validateAssignRoleInput({
|
||||
agentId,
|
||||
workspaceId,
|
||||
@@ -94,7 +151,14 @@ export class AgentRoleService {
|
||||
if (!role) {
|
||||
throw new AgentException(
|
||||
`Role with id ${roleId} not found in workspace`,
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!role.canBeAssignedToAgents) {
|
||||
throw new AgentException(
|
||||
`Role "${role.label}" cannot be assigned to agents`,
|
||||
AgentExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ export class AgentToolGeneratorService {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['permissionFlags'],
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
|
||||
@@ -9,4 +9,5 @@ export enum AgentExceptionCode {
|
||||
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
|
||||
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
||||
HANDOFF_ALREADY_EXISTS = 'HANDOFF_ALREADY_EXISTS',
|
||||
ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS = 'ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS',
|
||||
}
|
||||
|
||||
@@ -38,10 +38,7 @@ export class AgentResolver {
|
||||
@Query(() => [AgentDTO])
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async findManyAgents(@AuthWorkspace() { id: workspaceId }: Workspace) {
|
||||
return this.agentRepository.find({
|
||||
where: { workspaceId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
return this.agentService.findManyAgents(workspaceId);
|
||||
}
|
||||
|
||||
@Query(() => AgentDTO)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
|
||||
import { type CreateAgentInput } from 'src/engine/metadata-modules/agent/dtos/create-agent.input';
|
||||
@@ -23,6 +23,37 @@ export class AgentService {
|
||||
private readonly agentRoleService: AgentRoleService,
|
||||
) {}
|
||||
|
||||
async findManyAgents(workspaceId: string) {
|
||||
const agents = await this.agentRepository.find({
|
||||
where: { workspaceId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (agents.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const roleTargets = await this.roleTargetsRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
agentId: In(agents.map((agent) => agent.id)),
|
||||
},
|
||||
});
|
||||
|
||||
const agentRoleMap = new Map<string, string>();
|
||||
|
||||
roleTargets.forEach((roleTarget) => {
|
||||
if (roleTarget.agentId) {
|
||||
agentRoleMap.set(roleTarget.agentId, roleTarget.roleId);
|
||||
}
|
||||
});
|
||||
|
||||
return agents.map((agent) => ({
|
||||
...agent,
|
||||
roleId: agentRoleMap.get(agent.id) || null,
|
||||
}));
|
||||
}
|
||||
|
||||
async findOneAgent(id: string, workspaceId: string) {
|
||||
const agent = await this.agentRepository.findOne({
|
||||
where: { id, workspaceId },
|
||||
|
||||
+3
@@ -15,6 +15,9 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => {
|
||||
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
|
||||
canBeAssignedToUsers: role.canBeAssignedToUsers,
|
||||
canBeAssignedToAgents: role.canBeAssignedToAgents,
|
||||
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
|
||||
workspaceId: role.workspaceId,
|
||||
universalIdentifier: role.standardId || role.id,
|
||||
};
|
||||
|
||||
+4
@@ -49,6 +49,8 @@ export enum PermissionsExceptionCode {
|
||||
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'EMPTY_FIELD_PERMISSION_NOT_ALLOWED',
|
||||
JOIN_COLUMN_NAME_REQUIRED = 'JOIN_COLUMN_NAME_REQUIRED',
|
||||
COMPOSITE_TYPE_NOT_FOUND = 'COMPOSITE_TYPE_NOT_FOUND',
|
||||
ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET = 'ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET',
|
||||
ROLE_CANNOT_BE_ASSIGNED_TO_USERS = 'ROLE_CANNOT_BE_ASSIGNED_TO_USERS',
|
||||
}
|
||||
|
||||
export enum PermissionsExceptionMessage {
|
||||
@@ -78,4 +80,6 @@ export enum PermissionsExceptionMessage {
|
||||
FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT = 'Field restriction on update only makes sense on updatable object',
|
||||
OBJECT_PERMISSION_NOT_FOUND = 'Object permission not found',
|
||||
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'Empty field permission not allowed',
|
||||
ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET = 'Role must be assignable to at least one target type',
|
||||
ROLE_CANNOT_BE_ASSIGNED_TO_USERS = 'Role cannot be assigned to users',
|
||||
}
|
||||
|
||||
+2
@@ -41,6 +41,8 @@ export const permissionGraphqlApiExceptionHandler = (
|
||||
case PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT:
|
||||
case PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT:
|
||||
case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED:
|
||||
case PermissionsExceptionCode.ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET:
|
||||
case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS:
|
||||
throw new UserInputError(error);
|
||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
|
||||
@@ -52,4 +52,19 @@ export class CreateRoleInput {
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canDestroyAllObjectRecords?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canBeAssignedToUsers?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canBeAssignedToAgents?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canBeAssignedToApiKeys?: boolean;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,27 @@ import { Relation } from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
|
||||
import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
|
||||
@ObjectType('ApiKeyForRole')
|
||||
export class ApiKeyForRoleDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
id: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Field(() => Date, { nullable: false })
|
||||
expiresAt: Date;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
revokedAt?: Date | null;
|
||||
}
|
||||
|
||||
@ObjectType('Role')
|
||||
export class RoleDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
@@ -29,12 +45,27 @@ export class RoleDTO {
|
||||
@Field({ nullable: false })
|
||||
isEditable: boolean;
|
||||
|
||||
@Field({ nullable: false })
|
||||
canBeAssignedToUsers: boolean;
|
||||
|
||||
@Field({ nullable: false })
|
||||
canBeAssignedToAgents: boolean;
|
||||
|
||||
@Field({ nullable: false })
|
||||
canBeAssignedToApiKeys: boolean;
|
||||
|
||||
@HideField()
|
||||
roleTargets: Relation<RoleTargetsEntity[]>;
|
||||
|
||||
@Field(() => [WorkspaceMember], { nullable: true })
|
||||
workspaceMembers?: WorkspaceMember[];
|
||||
|
||||
@Field(() => [AgentDTO], { nullable: true })
|
||||
agents?: AgentDTO[];
|
||||
|
||||
@Field(() => [ApiKeyForRoleDTO], { nullable: true })
|
||||
apiKeys?: ApiKeyForRoleDTO[];
|
||||
|
||||
@Field({ nullable: false })
|
||||
canUpdateAllSettings: boolean;
|
||||
|
||||
|
||||
@@ -56,6 +56,21 @@ export class UpdateRolePayload {
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canDestroyAllObjectRecords?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canBeAssignedToUsers?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canBeAssignedToAgents?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canBeAssignedToApiKeys?: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
@@ -62,6 +62,15 @@ export class RoleEntity {
|
||||
@Column({ nullable: false, default: true })
|
||||
isEditable: boolean;
|
||||
|
||||
@Column({ nullable: false, default: true })
|
||||
canBeAssignedToUsers: boolean;
|
||||
|
||||
@Column({ nullable: false, default: true })
|
||||
canBeAssignedToAgents: boolean;
|
||||
|
||||
@Column({ nullable: false, default: true })
|
||||
canBeAssignedToApiKeys: boolean;
|
||||
|
||||
@OneToMany(
|
||||
() => RoleTargetsEntity,
|
||||
(roleTargets: RoleTargetsEntity) => roleTargets.role,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
@@ -22,6 +23,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
||||
TypeOrmModule.forFeature([UserWorkspace, Workspace]),
|
||||
UserRoleModule,
|
||||
AgentRoleModule,
|
||||
ApiKeyModule,
|
||||
PermissionsModule,
|
||||
UserWorkspaceModule,
|
||||
ObjectPermissionModule,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -22,6 +23,7 @@ import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
|
||||
@@ -39,7 +41,10 @@ import {
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import {
|
||||
ApiKeyForRoleDTO,
|
||||
RoleDTO,
|
||||
} from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
@@ -63,6 +68,7 @@ export class RoleResolver {
|
||||
private readonly objectPermissionService: ObjectPermissionService,
|
||||
private readonly settingPermissionService: PermissionFlagService,
|
||||
private readonly agentRoleService: AgentRoleService,
|
||||
private readonly apiKeyRoleService: ApiKeyRoleService,
|
||||
private readonly fieldPermissionService: FieldPermissionService,
|
||||
) {}
|
||||
|
||||
@@ -243,4 +249,35 @@ export class RoleResolver {
|
||||
|
||||
return workspaceMembers;
|
||||
}
|
||||
|
||||
@ResolveField('agents', () => [AgentDTO])
|
||||
async getAgentsAssignedToRole(
|
||||
@Parent() role: RoleDTO,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<AgentDTO[]> {
|
||||
const agents = await this.agentRoleService.getAgentsAssignedToRole(
|
||||
role.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
@ResolveField('apiKeys', () => [ApiKeyForRoleDTO])
|
||||
async getApiKeysAssignedToRole(
|
||||
@Parent() role: RoleDTO,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ApiKeyForRoleDTO[]> {
|
||||
const apiKeys = await this.apiKeyRoleService.getApiKeysAssignedToRole(
|
||||
role.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return apiKeys.map((apiKey) => ({
|
||||
id: apiKey.id,
|
||||
name: apiKey.name,
|
||||
expiresAt: apiKey.expiresAt,
|
||||
revokedAt: apiKey.revokedAt,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ export class RoleService {
|
||||
canUpdateAllObjectRecords: input.canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords: input.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords: input.canDestroyAllObjectRecords,
|
||||
canBeAssignedToUsers: input.canBeAssignedToUsers,
|
||||
canBeAssignedToAgents: input.canBeAssignedToAgents,
|
||||
canBeAssignedToApiKeys: input.canBeAssignedToApiKeys,
|
||||
isEditable: true,
|
||||
workspaceId,
|
||||
});
|
||||
@@ -231,6 +234,9 @@ export class RoleService {
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
isEditable: false,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
+3
@@ -15,6 +15,9 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
|
||||
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
|
||||
canBeAssignedToUsers: role.canBeAssignedToUsers,
|
||||
canBeAssignedToAgents: role.canBeAssignedToAgents,
|
||||
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
|
||||
roleTargets: role.roleTargets,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -258,6 +258,17 @@ export class UserRoleService {
|
||||
);
|
||||
}
|
||||
|
||||
if (!role.canBeAssignedToUsers) {
|
||||
throw new PermissionsException(
|
||||
`Role "${role.label}" cannot be assigned to users`,
|
||||
PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'This role cannot be assigned to users. Please select a different role.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const roles = await this.getRolesByUserWorkspaces({
|
||||
userWorkspaceIds: [userWorkspace.id],
|
||||
workspaceId,
|
||||
|
||||
+17
-1
@@ -49,11 +49,27 @@ export class WorkspaceAgentComparator {
|
||||
|
||||
switch (difference.type) {
|
||||
case 'CREATE': {
|
||||
const fromAgent = fromFlatAgents.find(
|
||||
(agent) => keyFactory(agent) === universalIdentifier,
|
||||
);
|
||||
const toAgent = toFlatAgents.find(
|
||||
(agent) => keyFactory(agent) === universalIdentifier,
|
||||
);
|
||||
|
||||
if (toAgent) {
|
||||
if (!toAgent) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (fromAgent) {
|
||||
fromAgent &&
|
||||
results.push({
|
||||
action: ComparatorAction.UPDATE,
|
||||
object: {
|
||||
...toAgent,
|
||||
id: fromAgent.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
results.push({
|
||||
action: ComparatorAction.CREATE,
|
||||
object: toAgent,
|
||||
|
||||
+76
-63
@@ -1,11 +1,12 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, type EntityManager, type Repository } from 'typeorm';
|
||||
import { IsNull, Not, type EntityManager } from 'typeorm';
|
||||
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
@@ -19,7 +20,6 @@ import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace
|
||||
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
|
||||
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
|
||||
import { WORKFLOW_CREATION_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent';
|
||||
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSyncAgentService {
|
||||
@@ -28,6 +28,7 @@ export class WorkspaceSyncAgentService {
|
||||
constructor(
|
||||
private readonly standardAgentFactory: StandardAgentFactory,
|
||||
private readonly workspaceAgentComparator: WorkspaceAgentComparator,
|
||||
private readonly agentRoleService: AgentRoleService,
|
||||
) {}
|
||||
|
||||
async synchronize(
|
||||
@@ -37,8 +38,6 @@ export class WorkspaceSyncAgentService {
|
||||
this.logger.log('Syncing standard agent.');
|
||||
|
||||
const agentRepository = manager.getRepository(AgentEntity);
|
||||
const roleRepository = manager.getRepository(RoleEntity);
|
||||
const roleTargetsRepository = manager.getRepository(RoleTargetsEntity);
|
||||
|
||||
const existingStandardAgentEntities = await agentRepository.find({
|
||||
where: {
|
||||
@@ -75,13 +74,43 @@ export class WorkspaceSyncAgentService {
|
||||
workspaceId: context.workspaceId,
|
||||
});
|
||||
|
||||
await this.assignAdminRoleToAgent(
|
||||
createdAgent.id,
|
||||
context.workspaceId,
|
||||
roleRepository,
|
||||
roleTargetsRepository,
|
||||
const agentDefinition = standardAgentDefinitions.find(
|
||||
(def) => def.standardId === createdAgent.standardId,
|
||||
);
|
||||
|
||||
if (agentDefinition?.standardRoleId) {
|
||||
try {
|
||||
const roleRepository = manager.getRepository(RoleEntity);
|
||||
const role = await roleRepository.findOne({
|
||||
where: {
|
||||
standardId: agentDefinition.standardRoleId,
|
||||
workspaceId: context.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new Error(
|
||||
`Standard role with standard ID ${agentDefinition.standardRoleId} not found in workspace`,
|
||||
);
|
||||
}
|
||||
|
||||
const roleTargetsRepository =
|
||||
manager.getRepository(RoleTargetsEntity);
|
||||
|
||||
await roleTargetsRepository.save({
|
||||
roleId: role.id,
|
||||
agentId: createdAgent.id,
|
||||
workspaceId: context.workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to assign standard role ${agentDefinition.standardRoleId} to agent ${createdAgent.id}: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (createdAgent.standardId === WORKFLOW_CREATION_AGENT.standardId) {
|
||||
await this.createAgentHandoffToWorkflowCreationAgent(
|
||||
createdAgent.id,
|
||||
@@ -99,9 +128,47 @@ export class WorkspaceSyncAgentService {
|
||||
'id',
|
||||
'universalIdentifier',
|
||||
'workspaceId',
|
||||
'standardRoleId' as keyof typeof agentToUpdate,
|
||||
]);
|
||||
|
||||
await agentRepository.update({ id: agentToUpdate.id }, flatAgentData);
|
||||
|
||||
const agentDefinition = standardAgentDefinitions.find(
|
||||
(def) => def.standardId === agentToUpdate.standardId,
|
||||
);
|
||||
|
||||
if (agentDefinition?.standardRoleId) {
|
||||
try {
|
||||
const roleRepository = manager.getRepository(RoleEntity);
|
||||
const role = await roleRepository.findOne({
|
||||
where: {
|
||||
standardId: agentDefinition.standardRoleId,
|
||||
workspaceId: context.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new Error(
|
||||
`Standard role with standard ID ${agentDefinition.standardRoleId} not found in workspace`,
|
||||
);
|
||||
}
|
||||
|
||||
const roleTargetsRepository =
|
||||
manager.getRepository(RoleTargetsEntity);
|
||||
|
||||
await roleTargetsRepository.save({
|
||||
roleId: role.id,
|
||||
agentId: agentToUpdate.id,
|
||||
workspaceId: context.workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to assign standard role ${agentDefinition.standardRoleId} to agent ${agentToUpdate.id}: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -115,60 +182,6 @@ export class WorkspaceSyncAgentService {
|
||||
}
|
||||
}
|
||||
|
||||
private async assignAdminRoleToAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
roleRepository: Repository<RoleEntity>,
|
||||
roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const adminRole = await roleRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
standardId: ADMIN_ROLE.standardId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
this.logger.warn(
|
||||
`Admin role not found for workspace ${workspaceId}, cannot assign to agent ${agentId}.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const existingRoleTarget = await roleTargetsRepository.findOne({
|
||||
where: {
|
||||
agentId,
|
||||
roleId: adminRole.id,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRoleTarget) {
|
||||
this.logger.log(
|
||||
`Workflow creation agent already has admin role assigned`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await roleTargetsRepository.save({
|
||||
roleId: adminRole.id,
|
||||
agentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully assigned admin role to workflow creation agent`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to assign admin role to workflow creation agent: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createAgentHandoffToWorkflowCreationAgent(
|
||||
workflowCreationAgentId: string,
|
||||
workspaceId: string,
|
||||
|
||||
+57
-2
@@ -1,12 +1,14 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, type EntityManager } from 'typeorm';
|
||||
import { IsNull, Not, type EntityManager, type Repository } from 'typeorm';
|
||||
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
|
||||
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator';
|
||||
import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory';
|
||||
@@ -28,12 +30,15 @@ export class WorkspaceSyncRoleService {
|
||||
this.logger.log('Syncing standard role metadata');
|
||||
|
||||
const roleRepository = manager.getRepository(RoleEntity);
|
||||
const permissionFlagRepository =
|
||||
manager.getRepository(PermissionFlagEntity);
|
||||
|
||||
const existingStandardRoleEntities = await roleRepository.find({
|
||||
where: {
|
||||
workspaceId: context.workspaceId,
|
||||
standardId: Not(IsNull()),
|
||||
},
|
||||
relations: ['permissionFlags'],
|
||||
});
|
||||
|
||||
const targetStandardRoles = this.standardRoleFactory.create(
|
||||
@@ -57,10 +62,23 @@ export class WorkspaceSyncRoleService {
|
||||
'id',
|
||||
]);
|
||||
|
||||
await roleRepository.save({
|
||||
const createdRole = await roleRepository.save({
|
||||
...flatRoleData,
|
||||
workspaceId: context.workspaceId,
|
||||
});
|
||||
|
||||
const roleDefinition = standardRoleDefinitions.find(
|
||||
(def) => def.standardId === roleToCreate.standardId,
|
||||
);
|
||||
|
||||
if (roleDefinition?.permissionFlags?.length) {
|
||||
await this.syncPermissionFlags(
|
||||
permissionFlagRepository,
|
||||
createdRole.id,
|
||||
context.workspaceId,
|
||||
roleDefinition.permissionFlags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -74,6 +92,19 @@ export class WorkspaceSyncRoleService {
|
||||
]);
|
||||
|
||||
await roleRepository.update({ id: roleToUpdate.id }, flatRoleData);
|
||||
|
||||
const roleDefinition = standardRoleDefinitions.find(
|
||||
(def) => def.standardId === roleToUpdate.standardId,
|
||||
);
|
||||
|
||||
if (roleDefinition?.permissionFlags) {
|
||||
await this.syncPermissionFlags(
|
||||
permissionFlagRepository,
|
||||
roleToUpdate.id,
|
||||
context.workspaceId,
|
||||
roleDefinition.permissionFlags,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -86,4 +117,28 @@ export class WorkspaceSyncRoleService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPermissionFlags(
|
||||
permissionFlagRepository: Repository<PermissionFlagEntity>,
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
permissionFlags: PermissionFlagType[],
|
||||
): Promise<void> {
|
||||
await permissionFlagRepository.delete({
|
||||
roleId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (permissionFlags.length > 0) {
|
||||
const newPermissionFlags = permissionFlags.map((flag) =>
|
||||
permissionFlagRepository.create({
|
||||
roleId,
|
||||
workspaceId,
|
||||
flag,
|
||||
}),
|
||||
);
|
||||
|
||||
await permissionFlagRepository.save(newPermissionFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
|
||||
import { WORKFLOW_MANAGER_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/workflow-manager-role';
|
||||
|
||||
export const WORKFLOW_CREATION_AGENT: StandardAgentDefinition = {
|
||||
standardId: '20202020-0002-0001-0001-000000000001',
|
||||
@@ -44,4 +45,5 @@ Be helpful, thorough, and always prioritize user understanding and workflow effe
|
||||
modelId: 'auto',
|
||||
responseFormat: {},
|
||||
isCustom: false,
|
||||
standardRoleId: WORKFLOW_MANAGER_ROLE.standardId,
|
||||
};
|
||||
|
||||
+1
@@ -5,4 +5,5 @@ export type StandardAgentDefinition = Omit<
|
||||
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
|
||||
> & {
|
||||
standardId: string;
|
||||
standardRoleId?: string;
|
||||
};
|
||||
|
||||
+2
@@ -1,6 +1,8 @@
|
||||
import { ADMIN_ROLE } from './roles/admin-role';
|
||||
import { WORKFLOW_MANAGER_ROLE } from './roles/workflow-manager-role';
|
||||
import { type StandardRoleDefinition } from './types/standard-role-definition.interface';
|
||||
|
||||
export const standardRoleDefinitions = [
|
||||
ADMIN_ROLE,
|
||||
WORKFLOW_MANAGER_ROLE,
|
||||
] as const satisfies StandardRoleDefinition[];
|
||||
|
||||
+3
@@ -12,4 +12,7 @@ export const ADMIN_ROLE: StandardRoleDefinition = {
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToApiKeys: true,
|
||||
};
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
|
||||
|
||||
export const WORKFLOW_MANAGER_ROLE: StandardRoleDefinition = {
|
||||
standardId: '20202020-0001-0001-0001-000000000002',
|
||||
label: 'Workflow Manager',
|
||||
description: 'Role for managing workflows',
|
||||
icon: 'IconSettingsAutomation',
|
||||
isEditable: false,
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: true,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: false,
|
||||
permissionFlags: [PermissionFlagType.WORKFLOWS],
|
||||
};
|
||||
+2
@@ -1,8 +1,10 @@
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
|
||||
export type StandardRoleDefinition = Omit<
|
||||
FlatRole,
|
||||
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
|
||||
> & {
|
||||
standardId: string;
|
||||
permissionFlags?: PermissionFlagType[];
|
||||
};
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -38,6 +39,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
DataSourceModule,
|
||||
TypeOrmModule.forFeature([Workspace, FeatureFlag]),
|
||||
WorkspaceMetadataVersionModule,
|
||||
AgentRoleModule,
|
||||
],
|
||||
providers: [
|
||||
...workspaceSyncMetadataFactories,
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ describe('roles permissions', () => {
|
||||
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.body.errors).toBeUndefined();
|
||||
expect(resp.body.data.getRoles).toHaveLength(4);
|
||||
expect(resp.body.data.getRoles).toHaveLength(5);
|
||||
expect(resp.body.data.getRoles).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
|
||||
@@ -222,7 +222,7 @@ export const workflowFilterActionSettingsSchema =
|
||||
export const workflowIteratorActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
items: z.union([z.array(z.any()), z.string()]).optional(),
|
||||
items: z.union([z.array(z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.any()), z.any()])), z.string()]).optional(),
|
||||
// TODO: should never be a string once fix the UI
|
||||
initialLoopStepIds: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user