30b8663a74
## Summary - AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from `FeatureFlagKey`, the public flag catalog, and the dev seeder. - Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent, agent chat, chat subscription, role-to-agent assignment, workflow AI step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard` wiring in the AI and workflow modules. - Removes frontend gating from settings nav, role permissions/assignment/applicability, command menu hotkeys, side panel, mobile/drawer nav, and the agent chat provider so AI UI is always on. Tests and generated GraphQL/SDK schemas updated accordingly. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx nx lint:diff-with-main twenty-server` - [x] `npx nx lint:diff-with-main twenty-front` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs feature-flag` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs workspace-entity-manager` - [ ] Manual smoke test: AI features still accessible without any flag row in `featureFlag` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
7.8 KiB
TypeScript
233 lines
7.8 KiB
TypeScript
import { isDefined } from 'twenty-shared/utils';
|
|
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
|
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 { type SettingsRoleAssignmentConfirmationModalSelectedRoleTarget } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedRoleTarget';
|
|
import { useSettingsAllRoles } from '@/settings/roles/hooks/useSettingsAllRoles';
|
|
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
|
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
|
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
|
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
|
import { useState } from 'react';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { SettingsPath } from 'twenty-shared/types';
|
|
import { useQuery } from '@apollo/client/react';
|
|
import {
|
|
type Agent,
|
|
type ApiKeyForRole,
|
|
FindManyAgentsDocument,
|
|
GetApiKeysDocument,
|
|
} from '~/generated-metadata/graphql';
|
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
|
import { type PartialWorkspaceMember } from '@/settings/roles/types/RoleWithPartialMembers';
|
|
import { ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID } from '@/settings/roles/role-assignment/constants/RoleAssignmentConfirmationModalId';
|
|
import { ROLE_TARGET_CONFIG } from '@/settings/roles/role-assignment/constants/RoleTargetConfig';
|
|
import { buildRoleMaps } from '@/settings/roles/role-assignment/utils/buildRoleMaps';
|
|
|
|
type SettingsRoleAssignmentProps = {
|
|
roleId: string;
|
|
isCreateMode?: boolean;
|
|
};
|
|
|
|
export const SettingsRoleAssignment = ({
|
|
roleId,
|
|
isCreateMode,
|
|
}: SettingsRoleAssignmentProps) => {
|
|
const settingsDraftRole = useAtomFamilyStateValue(
|
|
settingsDraftRoleFamilyState,
|
|
roleId,
|
|
);
|
|
|
|
const navigateSettings = useNavigateSettings();
|
|
const {
|
|
addWorkspaceMemberToRoleAndUpdateState,
|
|
updateWorkspaceMemberRoleDraftState,
|
|
} = useUpdateWorkspaceMemberRole(roleId);
|
|
|
|
const { addAgentToRoleAndUpdateState, updateAgentRoleDraftState } =
|
|
useUpdateAgentRole(roleId);
|
|
|
|
const { addApiKeyToRoleAndUpdateState, updateApiKeyRoleDraftState } =
|
|
useUpdateApiKeyRole(roleId);
|
|
|
|
const { data: agentsData } = useQuery(FindManyAgentsDocument);
|
|
const { data: apiKeysData } = useQuery(GetApiKeysDocument);
|
|
|
|
const { openModal, closeModal } = useModal();
|
|
const [selectedRoleTarget, setSelectRoleTarget] =
|
|
useState<SettingsRoleAssignmentConfirmationModalSelectedRoleTarget | null>(
|
|
null,
|
|
);
|
|
|
|
const currentWorkspaceMembers = useAtomStateValue(
|
|
currentWorkspaceMembersState,
|
|
);
|
|
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
|
const settingsAllRoles = useSettingsAllRoles();
|
|
|
|
const roleMaps = buildRoleMaps(settingsAllRoles);
|
|
|
|
const assignedWorkspaceMemberIds =
|
|
ROLE_TARGET_CONFIG.member.getAssignedIds(settingsDraftRole);
|
|
|
|
const assignableWorkspaceMembers = currentWorkspaceMembers.filter(
|
|
(member) => member.id !== currentWorkspaceMember?.id,
|
|
);
|
|
|
|
const allWorkspaceMembersHaveThisRole = assignableWorkspaceMembers.every(
|
|
(member) => assignedWorkspaceMemberIds.includes(member.id),
|
|
);
|
|
|
|
const handleModalClose = () => {
|
|
setSelectRoleTarget(null);
|
|
};
|
|
|
|
const isModalOpened = useAtomComponentStateValue(
|
|
isModalOpenedComponentState,
|
|
ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID,
|
|
);
|
|
|
|
const handleConfirm = async () => {
|
|
if (!selectedRoleTarget || !isModalOpened) return;
|
|
|
|
if (!isCreateMode) {
|
|
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,
|
|
);
|
|
|
|
if (!member) {
|
|
throw new Error('Workspace member not found');
|
|
}
|
|
|
|
updateWorkspaceMemberRoleDraftState({
|
|
workspaceMember: {
|
|
id: member.id,
|
|
name: member.name,
|
|
userEmail: member.userEmail,
|
|
avatarUrl: member.avatarUrl,
|
|
},
|
|
});
|
|
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: PartialWorkspaceMember | 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);
|
|
};
|
|
|
|
if (!isDefined(settingsDraftRole)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{Object.keys(ROLE_TARGET_CONFIG).map((roleTargetType) => (
|
|
<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
|
|
}
|
|
/>
|
|
))}
|
|
|
|
{selectedRoleTarget && (
|
|
<SettingsRoleAssignmentConfirmationModal
|
|
selectedRoleTarget={selectedRoleTarget}
|
|
onClose={handleModalClose}
|
|
onConfirm={handleConfirm}
|
|
onRoleClick={handleRoleClick}
|
|
newRoleName={settingsDraftRole.label}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|