From 02ced028e50e7e9f0ccf4eabd45b6df021e0a155 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 11 Feb 2025 14:51:31 +0100 Subject: [PATCH] add role assignment page (#10115) ## Context This PR introduces the "assignment" tab in the Role edit page, currently allowing admin users to assign workspace members to specific roles. Note: For now, a user can only have one role and a modal will warn you if you try to re-assign a user to a new role. ## Test Screenshot 2025-02-10 at 17 59 21 Screenshot 2025-02-10 at 17 59 33 Screenshot 2025-02-10 at 17 59 42 --- .../src/generated-metadata/graphql.ts | 40 +-- .../twenty-front/src/generated/graphql.tsx | 80 +++++- .../src/modules/app/components/AppRouter.tsx | 7 + .../modules/app/components/SettingsRoutes.tsx | 13 +- .../modules/app/hooks/useCreateAppRouter.tsx | 2 + .../hooks/__mocks__/useFieldMetadataItem.ts | 1 + .../roles/graphql/fragments/roleFragment.ts | 11 + .../updateWorkspaceMemberRoleMutation.ts | 22 ++ .../roles/graphql/queries/getRolesQuery.ts | 8 +- .../fragments/workspaceMemberQueryFragment.ts | 1 + .../pages/settings/roles/SettingsRoleEdit.tsx | 22 +- .../pages/settings/roles/SettingsRoles.tsx | 21 +- .../roles/components/RoleAssignment.tsx | 228 +++++++++++++++++- .../RoleAssignmentConfirmationModal.tsx | 50 ++++ ...oleAssignmentConfirmationModalSubtitle.tsx | 49 ++++ .../components/RoleAssignmentTableHeader.tsx | 25 ++ .../components/RoleAssignmentTableRow.tsx | 79 ++++++ .../roles/components/RoleSettings.tsx | 46 ++++ .../RoleWorkspaceMemberPickerDropdown.tsx | 71 ++++++ ...leWorkspaceMemberPickerDropdownContent.tsx | 83 +++++++ .../RoleAssignmentConfirmationModalMode.ts | 1 + ...onfirmationModalSelectedWorkspaceMember.ts | 5 + .../testing/mock-data/timeline-activities.ts | 5 + .../user/dtos/workspace-member.dto.ts | 3 + ...ermissions-graphql-api-exception.filter.ts | 4 + .../src/input/button/components/Button.tsx | 6 - 26 files changed, 813 insertions(+), 70 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts create mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModal.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableHeader.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleSettings.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleWorkspaceMemberPickerDropdown.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/components/RoleWorkspaceMemberPickerDropdownContent.tsx create mode 100644 packages/twenty-front/src/pages/settings/roles/types/RoleAssignmentConfirmationModalMode.ts create mode 100644 packages/twenty-front/src/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index fb10c6572..117f963ef 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -468,29 +468,6 @@ export type EnvironmentVariable = { }; export enum EnvironmentVariablesGroup { - Authentication = 'Authentication', - Email = 'Email', - Logging = 'Logging', - Other = 'Other', - ServerConfig = 'ServerConfig', - Workspace = 'Workspace' -} - -export type EnvironmentVariablesGroupData = { - __typename?: 'EnvironmentVariablesGroupData'; - description: Scalars['String']['output']; - isHiddenOnLoad: Scalars['Boolean']['output']; - name: EnvironmentVariablesGroup; - subgroups: Array; - variables: Array; -}; - -export type EnvironmentVariablesOutput = { - __typename?: 'EnvironmentVariablesOutput'; - groups: Array; -}; - -export enum EnvironmentVariablesSubGroup { BillingConfig = 'BillingConfig', CaptchaConfig = 'CaptchaConfig', CloudflareConfig = 'CloudflareConfig', @@ -498,10 +475,12 @@ export enum EnvironmentVariablesSubGroup { ExceptionHandler = 'ExceptionHandler', GoogleAuth = 'GoogleAuth', LLM = 'LLM', + Logging = 'Logging', MicrosoftAuth = 'MicrosoftAuth', - PasswordAuth = 'PasswordAuth', + Other = 'Other', RateLimiting = 'RateLimiting', SSL = 'SSL', + ServerConfig = 'ServerConfig', ServerlessConfig = 'ServerlessConfig', StorageConfig = 'StorageConfig', SupportChatConfig = 'SupportChatConfig', @@ -509,13 +488,19 @@ export enum EnvironmentVariablesSubGroup { TokensDuration = 'TokensDuration' } -export type EnvironmentVariablesSubgroupData = { - __typename?: 'EnvironmentVariablesSubgroupData'; +export type EnvironmentVariablesGroupData = { + __typename?: 'EnvironmentVariablesGroupData'; description: Scalars['String']['output']; - name: EnvironmentVariablesSubGroup; + isHiddenOnLoad: Scalars['Boolean']['output']; + name: EnvironmentVariablesGroup; variables: Array; }; +export type EnvironmentVariablesOutput = { + __typename?: 'EnvironmentVariablesOutput'; + groups: Array; +}; + export type ExecuteServerlessFunctionInput = { /** Id of the serverless function to execute */ id: Scalars['UUID']['input']; @@ -2175,6 +2160,7 @@ export type WorkspaceMember = { roles?: Maybe>; timeFormat?: Maybe; timeZone?: Maybe; + userEmail: Scalars['String']['output']; userWorkspaceId?: Maybe; }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index df85448bc..7fe350adf 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -400,7 +400,6 @@ export type EnvironmentVariable = { }; export enum EnvironmentVariablesGroup { - AuthenticationTokensDuration = 'AuthenticationTokensDuration', BillingConfig = 'BillingConfig', CaptchaConfig = 'CaptchaConfig', CloudflareConfig = 'CloudflareConfig', @@ -1939,6 +1938,7 @@ export type WorkspaceMember = { roles?: Maybe>; timeFormat?: Maybe; timeZone?: Maybe; + userEmail: Scalars['String']; userWorkspaceId?: Maybe; }; @@ -2243,10 +2243,20 @@ export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{ export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: { __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean } }; +export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean }; + +export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ + workspaceMemberId: Scalars['String']; + roleId?: InputMaybe; +}>; + + +export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } }; + export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>; -export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> }> }; +export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, 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 } }> }> }; export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; @@ -2281,7 +2291,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: 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, 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, 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, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: 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, 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 } | 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 DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2298,7 +2308,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, 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, 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, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: 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, 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 } | 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 ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2382,7 +2392,7 @@ export type GetWorkspaceInvitationsQueryVariables = Exact<{ [key: string]: never export type GetWorkspaceInvitationsQuery = { __typename?: 'Query', findWorkspaceInvitations: Array<{ __typename?: 'WorkspaceInvitation', id: any, email: string, expiresAt: string }> }; -export type WorkspaceMemberQueryFragmentFragment = { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }; +export type WorkspaceMemberQueryFragmentFragment = { __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 } }; export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; @@ -2521,6 +2531,15 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql` } } `; +export const RoleFragmentFragmentDoc = gql` + fragment RoleFragment on Role { + id + label + description + canUpdateAllSettings + isEditable +} + `; export const WorkspaceMemberQueryFragmentFragmentDoc = gql` fragment WorkspaceMemberQueryFragment on WorkspaceMember { id @@ -2531,6 +2550,7 @@ export const WorkspaceMemberQueryFragmentFragmentDoc = gql` colorScheme avatarUrl locale + userEmail timeZone dateFormat timeFormat @@ -3959,20 +3979,58 @@ export function useUpdateLabPublicFeatureFlagMutation(baseOptions?: Apollo.Mutat export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType; export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult; export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions; +export const UpdateWorkspaceMemberRoleDocument = gql` + mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String) { + updateWorkspaceMemberRole( + workspaceMemberId: $workspaceMemberId + roleId: $roleId + ) { + ...WorkspaceMemberQueryFragment + roles { + ...RoleFragment + } + } +} + ${WorkspaceMemberQueryFragmentFragmentDoc} +${RoleFragmentFragmentDoc}`; +export type UpdateWorkspaceMemberRoleMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateWorkspaceMemberRoleMutation__ + * + * To run a mutation, you first call `useUpdateWorkspaceMemberRoleMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateWorkspaceMemberRoleMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateWorkspaceMemberRoleMutation, { data, loading, error }] = useUpdateWorkspaceMemberRoleMutation({ + * variables: { + * workspaceMemberId: // value for 'workspaceMemberId' + * roleId: // value for 'roleId' + * }, + * }); + */ +export function useUpdateWorkspaceMemberRoleMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateWorkspaceMemberRoleDocument, options); + } +export type UpdateWorkspaceMemberRoleMutationHookResult = ReturnType; +export type UpdateWorkspaceMemberRoleMutationResult = Apollo.MutationResult; +export type UpdateWorkspaceMemberRoleMutationOptions = Apollo.BaseMutationOptions; export const GetRolesDocument = gql` query GetRoles { getRoles { - id - label - description - canUpdateAllSettings - isEditable + ...RoleFragment workspaceMembers { ...WorkspaceMemberQueryFragment } } } - ${WorkspaceMemberQueryFragmentFragmentDoc}`; + ${RoleFragmentFragmentDoc} +${WorkspaceMemberQueryFragmentFragmentDoc}`; /** * __useGetRolesQuery__ diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index 0ad07c1e4..c68ab8673 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -1,8 +1,10 @@ import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; import { currentUserState } from '@/auth/states/currentUserState'; import { billingState } from '@/client-config/states/billingState'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { RouterProvider } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; export const AppRouter = () => { const billing = useRecoilValue(billingState); @@ -16,12 +18,17 @@ export const AppRouter = () => { const isAdminPageEnabled = currentUser?.canImpersonate; + const isPermissionsEnabled = useIsFeatureEnabled( + FeatureFlagKey.IsPermissionsEnabled, + ); + return ( ); diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 61beb1c9b..f2514b5ed 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -267,12 +267,14 @@ type SettingsRoutesProps = { isBillingEnabled?: boolean; isFunctionSettingsEnabled?: boolean; isAdminPageEnabled?: boolean; + isPermissionsEnabled?: boolean; }; export const SettingsRoutes = ({ isBillingEnabled, isFunctionSettingsEnabled, isAdminPageEnabled, + isPermissionsEnabled, }: SettingsRoutesProps) => ( }> @@ -308,8 +310,15 @@ export const SettingsRoutes = ({ element={} /> } /> - } /> - } /> + {isPermissionsEnabled && ( + <> + } /> + } + /> + + )} } /> createBrowserRouter( createRoutesFromElements( @@ -63,6 +64,7 @@ export const useCreateAppRouter = ( isBillingEnabled={isBillingEnabled} isFunctionSettingsEnabled={isFunctionSettingsEnabled} isAdminPageEnabled={isAdminPageEnabled} + isPermissionsEnabled={isPermissionsEnabled} /> } /> 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 e076acb9e..d3f360324 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 @@ -209,6 +209,7 @@ export const queries = { colorScheme avatarUrl locale + userEmail timeZone dateFormat timeFormat diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts new file mode 100644 index 000000000..17c90f40d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const ROLE_FRAGMENT = gql` + fragment RoleFragment on Role { + id + label + description + canUpdateAllSettings + isEditable + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts new file mode 100644 index 000000000..eaa0ec675 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts @@ -0,0 +1,22 @@ +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 UPDATE_WORKSPACE_MEMBER_ROLE = gql` + ${WORKSPACE_MEMBER_QUERY_FRAGMENT} + ${ROLE_FRAGMENT} + mutation UpdateWorkspaceMemberRole( + $workspaceMemberId: String! + $roleId: String + ) { + updateWorkspaceMemberRole( + workspaceMemberId: $workspaceMemberId + roleId: $roleId + ) { + ...WorkspaceMemberQueryFragment + roles { + ...RoleFragment + } + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts b/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts index 9799fc81e..6434b48ed 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts @@ -1,15 +1,13 @@ +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 GET_ROLES = gql` ${WORKSPACE_MEMBER_QUERY_FRAGMENT} + ${ROLE_FRAGMENT} query GetRoles { getRoles { - id - label - description - canUpdateAllSettings - isEditable + ...RoleFragment workspaceMembers { ...WorkspaceMemberQueryFragment } diff --git a/packages/twenty-front/src/modules/workspace-member/graphql/fragments/workspaceMemberQueryFragment.ts b/packages/twenty-front/src/modules/workspace-member/graphql/fragments/workspaceMemberQueryFragment.ts index 394253eb4..62f0ce0fd 100644 --- a/packages/twenty-front/src/modules/workspace-member/graphql/fragments/workspaceMemberQueryFragment.ts +++ b/packages/twenty-front/src/modules/workspace-member/graphql/fragments/workspaceMemberQueryFragment.ts @@ -10,6 +10,7 @@ export const WORKSPACE_MEMBER_QUERY_FRAGMENT = gql` colorScheme avatarUrl locale + userEmail timeZone dateFormat timeFormat diff --git a/packages/twenty-front/src/pages/settings/roles/SettingsRoleEdit.tsx b/packages/twenty-front/src/pages/settings/roles/SettingsRoleEdit.tsx index de75427ab..25eb7dbcc 100644 --- a/packages/twenty-front/src/pages/settings/roles/SettingsRoleEdit.tsx +++ b/packages/twenty-front/src/pages/settings/roles/SettingsRoleEdit.tsx @@ -2,7 +2,13 @@ import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { H3Title, IconLockOpen, IconUser, IconUserPlus } from 'twenty-ui'; +import { + H3Title, + IconLockOpen, + IconSettings, + IconUser, + IconUserPlus, +} from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; @@ -12,6 +18,7 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useGetRolesQuery } from '~/generated/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { RolePermissions } from '~/pages/settings/roles/components/RolePermissions'; +import { RoleSettings } from '~/pages/settings/roles/components/RoleSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { RoleAssignment } from './components/RoleAssignment'; @@ -36,13 +43,16 @@ export const SETTINGS_ROLE_DETAIL_TABS = { TABS_IDS: { ASSIGNMENT: 'assignment', PERMISSIONS: 'permissions', + SETTINGS: 'settings', }, } as const; export const SettingsRoleEdit = () => { const { roleId = '' } = useParams(); - const { data: rolesData, loading: rolesLoading } = useGetRolesQuery(); const navigateSettings = useNavigateSettings(); + const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({ + fetchPolicy: 'network-only', + }); const role = rolesData?.getRoles.find((r) => r.id === roleId); @@ -71,6 +81,12 @@ export const SettingsRoleEdit = () => { Icon: IconLockOpen, hide: false, }, + { + id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS, + title: t`Settings`, + Icon: IconSettings, + hide: false, + }, ]; const renderActiveTabContent = () => { @@ -79,6 +95,8 @@ export const SettingsRoleEdit = () => { return ; case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS: return ; + case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS: + return ; default: return null; } diff --git a/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx b/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx index 7e17ffb7b..86aad9830 100644 --- a/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx +++ b/packages/twenty-front/src/pages/settings/roles/SettingsRoles.tsx @@ -55,8 +55,7 @@ const StyledAvatarGroup = styled.div` margin-right: ${({ theme }) => theme.spacing(1)}; > * { - border: 2px solid ${({ theme }) => theme.background.primary}; - margin-left: -8px; + margin-left: -5px; &:first-of-type { margin-left: 0; @@ -84,6 +83,11 @@ 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 isPermissionsEnabled = useIsFeatureEnabled( @@ -91,8 +95,9 @@ export const SettingsRoles = () => { ); const theme = useTheme(); const navigateSettings = useNavigateSettings(); - - const { data: rolesData, loading: isRolesLoading } = useGetRolesQuery(); + const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({ + fetchPolicy: 'network-only', + }); if (!isPermissionsEnabled) { return null; @@ -131,7 +136,7 @@ export const SettingsRoles = () => { - {!isRolesLoading && + {!rolesLoading && rolesData?.getRoles.map((role) => ( { workspaceMember.name.firstName ?? '' } type="rounded" - size="sm" + size="md" /> { ))} - {role.workspaceMembers.length} + + {role.workspaceMembers.length} + diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx index 5e625565c..b7cc51579 100644 --- a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx @@ -1,19 +1,227 @@ +import { SettingsPath } from '@/types/SettingsPath'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { Table } from '@/ui/layout/table/components/Table'; +import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; -import { H2Title, Section } from 'twenty-ui'; -import { Role } from '~/generated-metadata/graphql'; +import { useState } from 'react'; +import { Button, H2Title, IconPlus, IconSearch, Section } from 'twenty-ui'; +import { Role, WorkspaceMember } from '~/generated-metadata/graphql'; +import { + GetRolesDocument, + useGetRolesQuery, + useUpdateWorkspaceMemberRoleMutation, +} from '~/generated/graphql'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode'; +import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; +import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal'; +import { RoleAssignmentTableHeader } from './RoleAssignmentTableHeader'; +import { RoleAssignmentTableRow } from './RoleAssignmentTableRow'; +import { RoleWorkspaceMemberPickerDropdown } from './RoleWorkspaceMemberPickerDropdown'; + +const StyledBottomSection = styled(Section)<{ hasRows: boolean }>` + ${({ hasRows, theme }) => + hasRows + ? ` + border-top: 1px solid ${theme.border.color.light}; + margin-top: ${theme.spacing(2)}; + padding-top: ${theme.spacing(4)}; + ` + : ` + margin-top: ${theme.spacing(8)}; + `} + display: flex; + justify-content: flex-end; +`; + +const StyledEmptyText = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.light}; + display: flex; + justify-content: center; + margin-top: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledSearchContainer = styled.div` + margin: ${({ theme }) => theme.spacing(2)} 0; +`; + +const StyledSearchInput = styled(TextInput)` + input { + background: ${({ theme }) => theme.background.transparent.lighter}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + + &:hover { + border: 1px solid ${({ theme }) => theme.border.color.medium}; + } + } +`; type RoleAssignmentProps = { - role: Pick; + role: Pick & { + workspaceMembers: Array; + }; }; -// eslint-disable-next-line unused-imports/no-unused-vars export const RoleAssignment = ({ role }: RoleAssignmentProps) => { + const navigateSettings = useNavigateSettings(); + const [updateWorkspaceMemberRole] = useUpdateWorkspaceMemberRoleMutation({ + refetchQueries: [GetRolesDocument], + }); + + const [modalMode, setModalMode] = + useState(null); + const [selectedWorkspaceMember, setSelectedWorkspaceMember] = + useState( + null, + ); + const { data: rolesData } = useGetRolesQuery(); + const { closeDropdown } = useDropdown('role-member-select'); + const [searchFilter, setSearchFilter] = useState(''); + + const workspaceMemberRoleMap = new Map< + string, + { id: string; label: string } + >(); + rolesData?.getRoles?.forEach((role) => { + role.workspaceMembers.forEach((member) => { + workspaceMemberRoleMap.set(member.id, { id: role.id, label: role.label }); + }); + }); + + const filteredWorkspaceMembers = !searchFilter + ? role.workspaceMembers + : role.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 handleModalClose = () => { + setModalMode(null); + setSelectedWorkspaceMember(null); + }; + + const handleSelectWorkspaceMember = (workspaceMember: WorkspaceMember) => { + const existingRole = workspaceMemberRoleMap.get(workspaceMember.id); + + setSelectedWorkspaceMember({ + id: workspaceMember.id, + name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, + role: existingRole, + }); + setModalMode('assign'); + closeDropdown(); + }; + + const handleRemoveClick = (workspaceMember: WorkspaceMember) => { + setSelectedWorkspaceMember({ + id: workspaceMember.id, + name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, + role: workspaceMemberRoleMap.get(workspaceMember.id), + }); + setModalMode('remove'); + }; + + const handleConfirm = async () => { + if (!selectedWorkspaceMember || !modalMode) return; + + await updateWorkspaceMemberRole({ + variables: { + workspaceMemberId: selectedWorkspaceMember.id, + roleId: modalMode === 'assign' ? role.id : null, + }, + }); + + handleModalClose(); + }; + + const handleRoleClick = (roleId: string) => { + navigateSettings(SettingsPath.RoleDetail, { roleId }); + handleModalClose(); + }; + + const handleSearchChange = (text: string) => { + setSearchFilter(text); + }; + return ( -
- -
+ <> +
+ + + + + + + {filteredWorkspaceMembers.map((workspaceMember) => ( + handleRemoveClick(workspaceMember)} + /> + ))} + {filteredWorkspaceMembers.length === 0 && ( + + {searchFilter + ? t`No members matching your search` + : t`No members assigned to this role yet`} + + )} +
+
+ 0}> + + } + dropdownComponents={ + workspaceMember.id, + )} + onSelect={handleSelectWorkspaceMember} + /> + } + /> + + + {modalMode && selectedWorkspaceMember && ( + + )} + ); }; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModal.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModal.tsx new file mode 100644 index 000000000..1ae183823 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModal.tsx @@ -0,0 +1,50 @@ +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { t } from '@lingui/core/macro'; +import { RoleAssignmentConfirmationModalSubtitle } from '~/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle'; +import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode'; +import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; + +type RoleAssignmentConfirmationModalProps = { + mode: RoleAssignmentConfirmationModalMode; + selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember; + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + onRoleClick: (roleId: string) => void; +}; + +export const RoleAssignmentConfirmationModal = ({ + mode, + selectedWorkspaceMember, + isOpen, + onClose, + onConfirm, + onRoleClick, +}: RoleAssignmentConfirmationModalProps) => { + const isAssignMode = mode === 'assign'; + const hasExistingRole = !!selectedWorkspaceMember.role; + + const workspaceMemberName = selectedWorkspaceMember.name; + + const title = isAssignMode + ? t`Assign ${workspaceMemberName}?` + : t`Remove ${workspaceMemberName}?`; + + return ( + + } + onConfirmClick={onConfirm} + deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`} + confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'} + /> + ); +}; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx new file mode 100644 index 000000000..a7afa0d60 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx @@ -0,0 +1,49 @@ +import { SettingsCard } from '@/settings/components/SettingsCard'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { IconUser } from 'twenty-ui'; +import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode'; +import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; + +const StyledSettingsCardContainer = styled.div` + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +type RoleAssignmentConfirmationModalSubtitleProps = { + mode: RoleAssignmentConfirmationModalMode; + selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember; + onRoleClick: (roleId: string) => void; +}; + +export const RoleAssignmentConfirmationModalSubtitle = ({ + mode, + selectedWorkspaceMember, + onRoleClick, +}: RoleAssignmentConfirmationModalSubtitleProps) => { + const isAssignMode = mode === 'assign'; + const hasExistingRole = !!selectedWorkspaceMember.role; + + const workspaceMemberName = selectedWorkspaceMember.name; + + if (isAssignMode && hasExistingRole) { + return ( + <> + {t`${workspaceMemberName} will be unassigned from the following role:`} + + } + onClick={() => + selectedWorkspaceMember.role && + onRoleClick(selectedWorkspaceMember.role.id) + } + /> + + + ); + } + + return isAssignMode + ? t`Are you sure you want to assign this role?` + : t`This member will be unassigned from this role.`; +}; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableHeader.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableHeader.tsx new file mode 100644 index 000000000..dd6e229ca --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableHeader.tsx @@ -0,0 +1,25 @@ +import { Table } from '@/ui/layout/table/components/Table'; +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'; + +const StyledTableHeaderRow = styled(Table)` + margin-bottom: ${({ theme }) => theme.spacing(1.5)}; +`; + +type RoleAssignmentTableHeaderProps = { + className?: string; +}; + +export const RoleAssignmentTableHeader = ({ + className, +}: RoleAssignmentTableHeaderProps) => ( + + + {t`Name`} + {t`Email`} + + + +); diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx new file mode 100644 index 000000000..fc2f20b42 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx @@ -0,0 +1,79 @@ +import { Table } from '@/ui/layout/table/components/Table'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { + Avatar, + IconButton, + IconTrash, + OverflowingTextWithTooltip, +} from 'twenty-ui'; +import { WorkspaceMember } from '~/generated-metadata/graphql'; + +const StyledTable = styled(Table)` + margin-top: ${({ theme }) => theme.spacing(0.5)}; +`; + +const StyledIconWrapper = styled.div` + display: flex; + align-items: center; + margin-right: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledButtonContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + margin-left: ${({ theme }) => theme.spacing(3)}; +`; + +type RoleAssignmentTableRowProps = { + workspaceMember: WorkspaceMember; + onRemove: (workspaceMemberId: string) => void; +}; + +export const RoleAssignmentTableRow = ({ + workspaceMember, + onRemove, +}: RoleAssignmentTableRowProps) => { + const handleRemoveClick = (event: React.MouseEvent) => { + event.stopPropagation(); + onRemove(workspaceMember.id); + }; + + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleSettings.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleSettings.tsx new file mode 100644 index 000000000..dabee39a4 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleSettings.tsx @@ -0,0 +1,46 @@ +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; + +import { IconPicker } from '@/ui/input/components/IconPicker'; +import { TextArea } from '@/ui/input/components/TextArea'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { Role } from '~/generated-metadata/graphql'; + +const StyledInputsContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledInputContainer = styled.div` + display: flex; + flex-direction: column; +`; + +type RoleSettingsProps = { + role: Pick; +}; + +export const RoleSettings = ({ role }: RoleSettingsProps) => { + return ( + <> + + + {}} + /> + + + +