From 0220672fa91609a10b828c1402f1628dacdc32ee Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 25 Feb 2025 11:26:35 +0100 Subject: [PATCH] Add default role to workspace (#10444) ## Context Adding a defaultRole to each workspace, this role will be automatically added when a member joins a workspace via invite link or public link (seeds work differently though). Took the occasion to refactor a bit the frontend components, splitting them in smaller components for more readability. ## Test Screenshot 2025-02-24 at 14 54 02 --- .../src/generated-metadata/graphql.ts | 2 + .../twenty-front/src/generated/graphql.tsx | 45 +++-- .../auth/states/currentWorkspaceState.ts | 6 +- .../hooks/__mocks__/useFieldMetadataItem.ts | 11 ++ .../__tests__/useFieldMetadataItem.test.tsx | 41 +--- .../users/components/UserProviderEffect.tsx | 5 +- .../graphql/fragments/userQueryFragment.ts | 5 + .../graphql/mutations/updateWorkspace.ts | 5 + .../pages/settings/roles/SettingsRoles.tsx | 181 +----------------- .../RolePermissionsObjectsTableRow.tsx | 4 +- .../pages/settings/roles/components/Roles.tsx | 46 +++++ .../roles/components/RolesDefaultRole.tsx | 80 ++++++++ .../roles/components/RolesTableHeader.tsx | 25 +++ .../roles/components/RolesTableRow.tsx | 117 +++++++++++ ...1740390801418-addDefaultRoleToWorkspace.ts | 19 ++ .../engine/core-modules/auth/auth.module.ts | 4 +- .../auth/services/sign-in-up.service.spec.ts | 83 ++++++-- .../auth/services/sign-in-up.service.ts | 27 ++- .../user-workspace/user-workspace.service.ts | 11 +- .../workspace/dtos/update-workspace-input.ts | 6 + .../workspace/workspace.entity.ts | 7 + .../workspace/workspace.module.ts | 2 + .../workspace/workspace.resolver.ts | 17 ++ .../metadata-modules/role/role.resolver.ts | 18 +- .../metadata-modules/role/role.service.ts | 13 ++ .../workspace-manager.module.ts | 3 +- .../workspace-manager.service.ts | 15 ++ .../display/icon/components/TablerIcons.ts | 5 +- .../src/input/components/Checkbox.tsx | 8 +- 29 files changed, 538 insertions(+), 273 deletions(-) create mode 100644 packages/twenty-front/src/pages/settings/roles/components/Roles.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RolesDefaultRole.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RolesTableHeader.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RolesTableRow.tsx create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1740390801418-addDefaultRoleToWorkspace.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f79f37cf7..6b352773f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2045,6 +2045,7 @@ export type UpdateWorkflowVersionStepInput = { export type UpdateWorkspaceInput = { allowImpersonation?: InputMaybe; customDomain?: InputMaybe; + defaultRoleId?: InputMaybe; displayName?: InputMaybe; inviteHash?: InputMaybe; isGoogleAuthEnabled?: InputMaybe; @@ -2197,6 +2198,7 @@ export type Workspace = { customDomain?: Maybe; databaseSchema: Scalars['String']['output']; databaseUrl: Scalars['String']['output']; + defaultRole?: Maybe; deletedAt?: Maybe; displayName?: Maybe; featureFlags?: Maybe>; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 601cc686a..7594156ed 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1833,6 +1833,7 @@ export type UpdateWorkflowVersionStepInput = { export type UpdateWorkspaceInput = { allowImpersonation?: InputMaybe; customDomain?: InputMaybe; + defaultRoleId?: InputMaybe; displayName?: InputMaybe; inviteHash?: InputMaybe; isGoogleAuthEnabled?: InputMaybe; @@ -1975,6 +1976,7 @@ export type Workspace = { customDomain?: Maybe; databaseSchema: Scalars['String']; databaseUrl: Scalars['String']; + defaultRole?: Maybe; deletedAt?: Maybe; displayName?: Maybe; featureFlags?: Maybe>; @@ -2436,7 +2438,7 @@ export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2453,7 +2455,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2556,7 +2558,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{ }>; -export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, customDomain?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean } }; +export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, customDomain?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } }; export type UploadWorkspaceLogoMutationVariables = Exact<{ file: Scalars['Upload']; @@ -2676,19 +2678,6 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql` } } `; -export const RoleFragmentFragmentDoc = gql` - fragment RoleFragment on Role { - id - label - description - canUpdateAllSettings - isEditable - canReadAllObjectRecords - canUpdateAllObjectRecords - canSoftDeleteAllObjectRecords - canDestroyAllObjectRecords -} - `; export const WorkspaceMemberQueryFragmentFragmentDoc = gql` fragment WorkspaceMemberQueryFragment on WorkspaceMember { id @@ -2705,6 +2694,19 @@ export const WorkspaceMemberQueryFragmentFragmentDoc = gql` timeFormat } `; +export const RoleFragmentFragmentDoc = gql` + fragment RoleFragment on Role { + id + label + description + canUpdateAllSettings + isEditable + canReadAllObjectRecords + canUpdateAllObjectRecords + canSoftDeleteAllObjectRecords + canDestroyAllObjectRecords +} + `; export const UserQueryFragmentFragmentDoc = gql` fragment UserQueryFragment on User { id @@ -2768,6 +2770,9 @@ export const UserQueryFragmentFragmentDoc = gql` status } workspaceMembersCount + defaultRole { + ...RoleFragment + } } workspaces { workspace { @@ -2784,7 +2789,8 @@ export const UserQueryFragmentFragmentDoc = gql` } userVars } - ${WorkspaceMemberQueryFragmentFragmentDoc}`; + ${WorkspaceMemberQueryFragmentFragmentDoc} +${RoleFragmentFragmentDoc}`; export const GetTimelineCalendarEventsFromCompanyIdDocument = gql` query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) { getTimelineCalendarEventsFromCompanyId( @@ -5217,9 +5223,12 @@ export const UpdateWorkspaceDocument = gql` isGoogleAuthEnabled isMicrosoftAuthEnabled isPasswordAuthEnabled + defaultRole { + ...RoleFragment + } } } - `; + ${RoleFragmentFragmentDoc}`; export type UpdateWorkspaceMutationFn = Apollo.MutationFunction; /** diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index 6a34705f1..7fa96d5ea 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -1,6 +1,6 @@ import { createState } from '@ui/utilities/state/utils/createState'; -import { Workspace } from '~/generated/graphql'; +import { Role, Workspace } from '~/generated/graphql'; export type CurrentWorkspace = Pick< Workspace, @@ -23,7 +23,9 @@ export type CurrentWorkspace = Pick< | 'customDomain' | 'workspaceUrls' | 'metadataVersion' ->; +> & { + defaultRole?: Omit | null; +}; export const currentWorkspaceState = createState({ key: 'currentWorkspaceState', diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index a4f13bf7c..6bba8b5d8 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -342,6 +342,17 @@ export const responseData = { metadataVersion: 1, currentBillingSubscription: null, workspaceMembersCount: 1, + defaultRole: { + id: 'default-role-id', + label: 'Default Role', + description: 'Default Role Description', + canUpdateAllSettings: true, + isEditable: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, + } }, currentBillingSubscription: null, billingSubscriptions: [], diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx index dff377a32..474ed60cd 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx @@ -21,6 +21,16 @@ import { responseData as findManyObjectMetadataItemsResponseData, } from '../__mocks__/useFindManyObjectMetadataItems'; +jest.mock('@/object-metadata/hooks/useUpdateOneFieldMetadataItem', () => ({ + useUpdateOneFieldMetadataItem: () => ({ + updateOneFieldMetadataItem: jest.fn().mockResolvedValue({ + data: { + updateOneField: responseData.default, + }, + }), + }), +})); + const fieldMetadataItem: FieldMetadataItem = { id: FIELD_METADATA_ID, createdAt: '', @@ -111,17 +121,6 @@ const mocks = [ }, })), }, - { - request: { - query: queries.activateMetadataField, - variables: variables.activateMetadataField, - }, - result: jest.fn(() => ({ - data: { - updateOneField: responseData.default, - }, - })), - }, { request: { query: queries.createMetadataField, @@ -133,26 +132,6 @@ const mocks = [ }, })), }, - { - request: { - query: queries.activateMetadataField, - variables: variables.deactivateMetadataField, - }, - result: jest.fn(() => ({ - data: { - updateOneField: responseData.default, - }, - })), - }, - { - request: { - query: queries.getCurrentUser, - variables: {}, - }, - result: jest.fn(() => ({ - data: responseData.getCurrentUser, - })), - }, { request: { query: queries.getCurrentUser, diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx index ff6580a16..62edf6def 100644 --- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx @@ -57,7 +57,10 @@ export const UserProviderEffect = () => { setCurrentUser(queryData.currentUser); if (isDefined(queryData.currentUser.currentWorkspace)) { - setCurrentWorkspace(queryData.currentUser.currentWorkspace); + setCurrentWorkspace({ + ...queryData.currentUser.currentWorkspace, + defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null, + }); } if (isDefined(queryData.currentUser.currentUserWorkspace)) { diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index cf4e31f13..6ce9779eb 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -1,7 +1,9 @@ +import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment'; import { gql } from '@apollo/client'; export const USER_QUERY_FRAGMENT = gql` + ${ROLE_FRAGMENT} fragment UserQueryFragment on User { id firstName @@ -64,6 +66,9 @@ export const USER_QUERY_FRAGMENT = gql` status } workspaceMembersCount + defaultRole { + ...RoleFragment + } } workspaces { workspace { diff --git a/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts b/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts index 4150f36d0..fc32736f6 100644 --- a/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts +++ b/packages/twenty-front/src/modules/workspace/graphql/mutations/updateWorkspace.ts @@ -1,6 +1,8 @@ +import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; import { gql } from '@apollo/client'; export const UPDATE_WORKSPACE = gql` + ${ROLE_FRAGMENT} mutation UpdateWorkspace($input: UpdateWorkspaceInput!) { updateWorkspace(data: $input) { id @@ -13,6 +15,9 @@ export const UPDATE_WORKSPACE = gql` isGoogleAuthEnabled isMicrosoftAuthEnabled isPasswordAuthEnabled + defaultRole { + ...RoleFragment + } } } `; diff --git a/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx b/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx index 0736c83b4..a31ec63b5 100644 --- a/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx +++ b/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx @@ -1,106 +1,19 @@ -import styled from '@emotion/styled'; import { Trans, useLingui } from '@lingui/react/macro'; -import { - AppTooltip, - Avatar, - Button, - H2Title, - IconChevronRight, - IconLock, - IconPlus, - IconUser, - Section, - TooltipDelay, -} from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; -import { Table } from '@/ui/layout/table/components/Table'; -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 { useTheme } from '@emotion/react'; -import React from 'react'; import { useGetRolesQuery } from '~/generated/graphql'; -import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { Roles } from '~/pages/settings/roles/components/Roles'; +import { RolesDefaultRole } from '~/pages/settings/roles/components/RolesDefaultRole'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -const StyledTable = styled(Table)` - margin-top: ${({ theme }) => theme.spacing(0.5)}; -`; - -const StyledTableRow = styled(TableRow)` - &:hover { - background: ${({ theme }) => theme.background.transparent.light}; - cursor: pointer; - } -`; - -const StyledNameCell = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledAssignedCell = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledAvatarGroup = styled.div` - align-items: center; - display: flex; - margin-right: ${({ theme }) => theme.spacing(1)}; - - > * { - margin-left: -5px; - - &:first-of-type { - margin-left: 0; - } - } -`; - -const StyledTableHeaderRow = styled(Table)` - margin-bottom: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledBottomSection = styled(Section)` - border-top: 1px solid ${({ theme }) => theme.border.color.light}; - margin-top: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(4)}; - display: flex; - justify-content: flex-end; -`; - -const StyledIconChevronRight = styled(IconChevronRight)` - color: ${({ theme }) => theme.font.color.tertiary}; -`; - -const StyledAvatarContainer = styled.div` - border: 0px; -`; - -const StyledAssignedText = styled.div` - color: ${({ theme }) => theme.font.color.primary}; - font-size: ${({ theme }) => theme.font.size.md}; -`; - export const SettingsRoles = () => { const { t } = useLingui(); - - const theme = useTheme(); - const navigateSettings = useNavigateSettings(); const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({ fetchPolicy: 'network-only', }); - const handleRoleClick = (roleId: string) => { - navigateSettings(SettingsPath.RoleDetail, { roleId }); - }; - return ( { ]} > -
- - - - - - Name - - - Assigned to - - - - - {!rolesLoading && - rolesData?.getRoles.map((role) => ( - handleRoleClick(role.id)} - > - - - - {role.label} - {!role.isEditable && ( - - )} - - - - - - {role.workspaceMembers - .slice(0, 5) - .map((workspaceMember) => ( - - - - - - - ))} - - - {role.workspaceMembers.length} - - - - - - - - ))} - - -
+ {!rolesLoading && ( + <> + + + + )}
); diff --git a/packages/twenty-front/src/pages/settings/roles/components/RolePermissionsObjectsTableRow.tsx b/packages/twenty-front/src/pages/settings/roles/components/RolePermissionsObjectsTableRow.tsx index 725c5fcb6..d81974021 100644 --- a/packages/twenty-front/src/pages/settings/roles/components/RolePermissionsObjectsTableRow.tsx +++ b/packages/twenty-front/src/pages/settings/roles/components/RolePermissionsObjectsTableRow.tsx @@ -6,8 +6,8 @@ import { RolePermissionsObjectPermission } from '~/pages/settings/roles/types/Ro const StyledIconWrapper = styled.div` align-items: center; - background: ${({ theme }) => theme.color.blue10}; - border: 1px solid ${({ theme }) => theme.color.blue30}; + background: ${({ theme }) => theme.adaptiveColors.blue1}; + border: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; border-radius: ${({ theme }) => theme.border.radius.sm}; display: flex; height: ${({ theme }) => theme.spacing(4)}; diff --git a/packages/twenty-front/src/pages/settings/roles/components/Roles.tsx b/packages/twenty-front/src/pages/settings/roles/components/Roles.tsx new file mode 100644 index 000000000..cb8def5c9 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/Roles.tsx @@ -0,0 +1,46 @@ +import { Table } from '@/ui/layout/table/components/Table'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; + +import { Button, H2Title, IconPlus, Section } from 'twenty-ui'; +import { Role } from '~/generated-metadata/graphql'; +import { RolesTableHeader } from '~/pages/settings/roles/components/RolesTableHeader'; +import { RolesTableRow } from '~/pages/settings/roles/components/RolesTableRow'; + +const StyledTable = styled(Table)` + margin-top: ${({ theme }) => theme.spacing(0.5)}; +`; + +const StyledBottomSection = styled(Section)` + border-top: 1px solid ${({ theme }) => theme.border.color.light}; + margin-top: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(4)}; + display: flex; + justify-content: flex-end; +`; + +export const Roles = ({ roles }: { roles: Role[] }) => { + return ( +
+ + + + {roles.map((role) => ( + + ))} + + +
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RolesDefaultRole.tsx b/packages/twenty-front/src/pages/settings/roles/components/RolesDefaultRole.tsx new file mode 100644 index 000000000..cbf5a1b9c --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/RolesDefaultRole.tsx @@ -0,0 +1,80 @@ +import { + CurrentWorkspace, + currentWorkspaceState, +} from '@/auth/states/currentWorkspaceState'; +import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; +import { Select } from '@/ui/input/components/Select'; +import { t } from '@lingui/core/macro'; +import { useRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared'; +import { Card, H2Title, IconUserPin, Section } from 'twenty-ui'; +import { + Role, + UpdateWorkspaceMutation, + useUpdateWorkspaceMutation, +} from '~/generated/graphql'; + +export const RolesDefaultRole = ({ roles }: { roles: Role[] }) => { + const [updateWorkspace] = useUpdateWorkspaceMutation(); + + const [currentWorkspace, setCurrentWorkspace] = useRecoilState( + currentWorkspaceState, + ); + + const defaultRole = currentWorkspace?.defaultRole; + + const updateDefaultRole = ( + defaultRoleId: string | null, + currentWorkspace: CurrentWorkspace, + ) => { + updateWorkspace({ + variables: { + input: { + defaultRoleId: isDefined(defaultRoleId) ? defaultRoleId : null, + }, + }, + onCompleted: (data: UpdateWorkspaceMutation) => { + const defaultRole = data.updateWorkspace.defaultRole; + + setCurrentWorkspace({ + ...currentWorkspace, + defaultRole: isDefined(defaultRole) ? defaultRole : null, + }); + }, + }); + }; + + if (!currentWorkspace) { + return null; + } + + return ( +
+ + + +