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 <img width="648" alt="Screenshot 2025-02-10 at 17 59 21" src="https://github.com/user-attachments/assets/dabd7a17-6aca-4d2b-95d8-46182f53e1e8" /> <img width="668" alt="Screenshot 2025-02-10 at 17 59 33" src="https://github.com/user-attachments/assets/802aab7a-db67-4f83-9a44-35773df100f7" /> <img width="629" alt="Screenshot 2025-02-10 at 17 59 42" src="https://github.com/user-attachments/assets/277db061-3f05-4ccd-8a83-7a96d6c1673e" />
This commit is contained in:
@ -468,29 +468,6 @@ export type EnvironmentVariable = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum EnvironmentVariablesGroup {
|
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<EnvironmentVariablesSubgroupData>;
|
|
||||||
variables: Array<EnvironmentVariable>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EnvironmentVariablesOutput = {
|
|
||||||
__typename?: 'EnvironmentVariablesOutput';
|
|
||||||
groups: Array<EnvironmentVariablesGroupData>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum EnvironmentVariablesSubGroup {
|
|
||||||
BillingConfig = 'BillingConfig',
|
BillingConfig = 'BillingConfig',
|
||||||
CaptchaConfig = 'CaptchaConfig',
|
CaptchaConfig = 'CaptchaConfig',
|
||||||
CloudflareConfig = 'CloudflareConfig',
|
CloudflareConfig = 'CloudflareConfig',
|
||||||
@ -498,10 +475,12 @@ export enum EnvironmentVariablesSubGroup {
|
|||||||
ExceptionHandler = 'ExceptionHandler',
|
ExceptionHandler = 'ExceptionHandler',
|
||||||
GoogleAuth = 'GoogleAuth',
|
GoogleAuth = 'GoogleAuth',
|
||||||
LLM = 'LLM',
|
LLM = 'LLM',
|
||||||
|
Logging = 'Logging',
|
||||||
MicrosoftAuth = 'MicrosoftAuth',
|
MicrosoftAuth = 'MicrosoftAuth',
|
||||||
PasswordAuth = 'PasswordAuth',
|
Other = 'Other',
|
||||||
RateLimiting = 'RateLimiting',
|
RateLimiting = 'RateLimiting',
|
||||||
SSL = 'SSL',
|
SSL = 'SSL',
|
||||||
|
ServerConfig = 'ServerConfig',
|
||||||
ServerlessConfig = 'ServerlessConfig',
|
ServerlessConfig = 'ServerlessConfig',
|
||||||
StorageConfig = 'StorageConfig',
|
StorageConfig = 'StorageConfig',
|
||||||
SupportChatConfig = 'SupportChatConfig',
|
SupportChatConfig = 'SupportChatConfig',
|
||||||
@ -509,13 +488,19 @@ export enum EnvironmentVariablesSubGroup {
|
|||||||
TokensDuration = 'TokensDuration'
|
TokensDuration = 'TokensDuration'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EnvironmentVariablesSubgroupData = {
|
export type EnvironmentVariablesGroupData = {
|
||||||
__typename?: 'EnvironmentVariablesSubgroupData';
|
__typename?: 'EnvironmentVariablesGroupData';
|
||||||
description: Scalars['String']['output'];
|
description: Scalars['String']['output'];
|
||||||
name: EnvironmentVariablesSubGroup;
|
isHiddenOnLoad: Scalars['Boolean']['output'];
|
||||||
|
name: EnvironmentVariablesGroup;
|
||||||
variables: Array<EnvironmentVariable>;
|
variables: Array<EnvironmentVariable>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EnvironmentVariablesOutput = {
|
||||||
|
__typename?: 'EnvironmentVariablesOutput';
|
||||||
|
groups: Array<EnvironmentVariablesGroupData>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExecuteServerlessFunctionInput = {
|
export type ExecuteServerlessFunctionInput = {
|
||||||
/** Id of the serverless function to execute */
|
/** Id of the serverless function to execute */
|
||||||
id: Scalars['UUID']['input'];
|
id: Scalars['UUID']['input'];
|
||||||
@ -2175,6 +2160,7 @@ export type WorkspaceMember = {
|
|||||||
roles?: Maybe<Array<Role>>;
|
roles?: Maybe<Array<Role>>;
|
||||||
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
||||||
timeZone?: Maybe<Scalars['String']['output']>;
|
timeZone?: Maybe<Scalars['String']['output']>;
|
||||||
|
userEmail: Scalars['String']['output'];
|
||||||
userWorkspaceId?: Maybe<Scalars['String']['output']>;
|
userWorkspaceId?: Maybe<Scalars['String']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -400,7 +400,6 @@ export type EnvironmentVariable = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum EnvironmentVariablesGroup {
|
export enum EnvironmentVariablesGroup {
|
||||||
AuthenticationTokensDuration = 'AuthenticationTokensDuration',
|
|
||||||
BillingConfig = 'BillingConfig',
|
BillingConfig = 'BillingConfig',
|
||||||
CaptchaConfig = 'CaptchaConfig',
|
CaptchaConfig = 'CaptchaConfig',
|
||||||
CloudflareConfig = 'CloudflareConfig',
|
CloudflareConfig = 'CloudflareConfig',
|
||||||
@ -1939,6 +1938,7 @@ export type WorkspaceMember = {
|
|||||||
roles?: Maybe<Array<Role>>;
|
roles?: Maybe<Array<Role>>;
|
||||||
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
||||||
timeZone?: Maybe<Scalars['String']>;
|
timeZone?: Maybe<Scalars['String']>;
|
||||||
|
userEmail: Scalars['String'];
|
||||||
userWorkspaceId?: Maybe<Scalars['String']>;
|
userWorkspaceId?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2243,10 +2243,20 @@ export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: { __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean } };
|
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<Scalars['String']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
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 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<{
|
export type CreateOidcIdentityProviderMutationVariables = Exact<{
|
||||||
input: SetupOidcSsoInput;
|
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 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<SettingsFeatures> | 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<SettingsFeatures> | 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; }>;
|
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 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<SettingsFeatures> | 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<SettingsFeatures> | 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<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
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 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<{
|
export type ActivateWorkspaceMutationVariables = Exact<{
|
||||||
input: ActivateWorkspaceInput;
|
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`
|
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
|
||||||
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
||||||
id
|
id
|
||||||
@ -2531,6 +2550,7 @@ export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
|
|||||||
colorScheme
|
colorScheme
|
||||||
avatarUrl
|
avatarUrl
|
||||||
locale
|
locale
|
||||||
|
userEmail
|
||||||
timeZone
|
timeZone
|
||||||
dateFormat
|
dateFormat
|
||||||
timeFormat
|
timeFormat
|
||||||
@ -3959,20 +3979,58 @@ export function useUpdateLabPublicFeatureFlagMutation(baseOptions?: Apollo.Mutat
|
|||||||
export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof useUpdateLabPublicFeatureFlagMutation>;
|
export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof useUpdateLabPublicFeatureFlagMutation>;
|
||||||
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
|
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
|
||||||
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
||||||
|
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<UpdateWorkspaceMemberRoleMutation, UpdateWorkspaceMemberRoleMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __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<UpdateWorkspaceMemberRoleMutation, UpdateWorkspaceMemberRoleMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<UpdateWorkspaceMemberRoleMutation, UpdateWorkspaceMemberRoleMutationVariables>(UpdateWorkspaceMemberRoleDocument, options);
|
||||||
|
}
|
||||||
|
export type UpdateWorkspaceMemberRoleMutationHookResult = ReturnType<typeof useUpdateWorkspaceMemberRoleMutation>;
|
||||||
|
export type UpdateWorkspaceMemberRoleMutationResult = Apollo.MutationResult<UpdateWorkspaceMemberRoleMutation>;
|
||||||
|
export type UpdateWorkspaceMemberRoleMutationOptions = Apollo.BaseMutationOptions<UpdateWorkspaceMemberRoleMutation, UpdateWorkspaceMemberRoleMutationVariables>;
|
||||||
export const GetRolesDocument = gql`
|
export const GetRolesDocument = gql`
|
||||||
query GetRoles {
|
query GetRoles {
|
||||||
getRoles {
|
getRoles {
|
||||||
id
|
...RoleFragment
|
||||||
label
|
|
||||||
description
|
|
||||||
canUpdateAllSettings
|
|
||||||
isEditable
|
|
||||||
workspaceMembers {
|
workspaceMembers {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${WorkspaceMemberQueryFragmentFragmentDoc}`;
|
${RoleFragmentFragmentDoc}
|
||||||
|
${WorkspaceMemberQueryFragmentFragmentDoc}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useGetRolesQuery__
|
* __useGetRolesQuery__
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
|
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const AppRouter = () => {
|
export const AppRouter = () => {
|
||||||
const billing = useRecoilValue(billingState);
|
const billing = useRecoilValue(billingState);
|
||||||
@ -16,12 +18,17 @@ export const AppRouter = () => {
|
|||||||
|
|
||||||
const isAdminPageEnabled = currentUser?.canImpersonate;
|
const isAdminPageEnabled = currentUser?.canImpersonate;
|
||||||
|
|
||||||
|
const isPermissionsEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsPermissionsEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
router={useCreateAppRouter(
|
router={useCreateAppRouter(
|
||||||
isBillingPageEnabled,
|
isBillingPageEnabled,
|
||||||
isFunctionSettingsEnabled,
|
isFunctionSettingsEnabled,
|
||||||
isAdminPageEnabled,
|
isAdminPageEnabled,
|
||||||
|
isPermissionsEnabled,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -267,12 +267,14 @@ type SettingsRoutesProps = {
|
|||||||
isBillingEnabled?: boolean;
|
isBillingEnabled?: boolean;
|
||||||
isFunctionSettingsEnabled?: boolean;
|
isFunctionSettingsEnabled?: boolean;
|
||||||
isAdminPageEnabled?: boolean;
|
isAdminPageEnabled?: boolean;
|
||||||
|
isPermissionsEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsRoutes = ({
|
export const SettingsRoutes = ({
|
||||||
isBillingEnabled,
|
isBillingEnabled,
|
||||||
isFunctionSettingsEnabled,
|
isFunctionSettingsEnabled,
|
||||||
isAdminPageEnabled,
|
isAdminPageEnabled,
|
||||||
|
isPermissionsEnabled,
|
||||||
}: SettingsRoutesProps) => (
|
}: SettingsRoutesProps) => (
|
||||||
<Suspense fallback={<SettingsSkeletonLoader />}>
|
<Suspense fallback={<SettingsSkeletonLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@ -308,8 +310,15 @@ export const SettingsRoutes = ({
|
|||||||
element={<SettingsObjectDetailPage />}
|
element={<SettingsObjectDetailPage />}
|
||||||
/>
|
/>
|
||||||
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
|
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
|
||||||
<Route path={SettingsPath.Roles} element={<SettingsRoles />} />
|
{isPermissionsEnabled && (
|
||||||
<Route path={SettingsPath.RoleDetail} element={<SettingsRoleEdit />} />
|
<>
|
||||||
|
<Route path={SettingsPath.Roles} element={<SettingsRoles />} />
|
||||||
|
<Route
|
||||||
|
path={SettingsPath.RoleDetail}
|
||||||
|
element={<SettingsRoleEdit />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Route path={SettingsPath.Developers} element={<SettingsDevelopers />} />
|
<Route path={SettingsPath.Developers} element={<SettingsDevelopers />} />
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.DevelopersNewApiKey}
|
path={SettingsPath.DevelopersNewApiKey}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const useCreateAppRouter = (
|
|||||||
isBillingEnabled?: boolean,
|
isBillingEnabled?: boolean,
|
||||||
isFunctionSettingsEnabled?: boolean,
|
isFunctionSettingsEnabled?: boolean,
|
||||||
isAdminPageEnabled?: boolean,
|
isAdminPageEnabled?: boolean,
|
||||||
|
isPermissionsEnabled?: boolean,
|
||||||
) =>
|
) =>
|
||||||
createBrowserRouter(
|
createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
@ -63,6 +64,7 @@ export const useCreateAppRouter = (
|
|||||||
isBillingEnabled={isBillingEnabled}
|
isBillingEnabled={isBillingEnabled}
|
||||||
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
|
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
|
||||||
isAdminPageEnabled={isAdminPageEnabled}
|
isAdminPageEnabled={isAdminPageEnabled}
|
||||||
|
isPermissionsEnabled={isPermissionsEnabled}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -209,6 +209,7 @@ export const queries = {
|
|||||||
colorScheme
|
colorScheme
|
||||||
avatarUrl
|
avatarUrl
|
||||||
locale
|
locale
|
||||||
|
userEmail
|
||||||
timeZone
|
timeZone
|
||||||
dateFormat
|
dateFormat
|
||||||
timeFormat
|
timeFormat
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const ROLE_FRAGMENT = gql`
|
||||||
|
fragment RoleFragment on Role {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
description
|
||||||
|
canUpdateAllSettings
|
||||||
|
isEditable
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -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 { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const GET_ROLES = gql`
|
export const GET_ROLES = gql`
|
||||||
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||||
|
${ROLE_FRAGMENT}
|
||||||
query GetRoles {
|
query GetRoles {
|
||||||
getRoles {
|
getRoles {
|
||||||
id
|
...RoleFragment
|
||||||
label
|
|
||||||
description
|
|
||||||
canUpdateAllSettings
|
|
||||||
isEditable
|
|
||||||
workspaceMembers {
|
workspaceMembers {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const WORKSPACE_MEMBER_QUERY_FRAGMENT = gql`
|
|||||||
colorScheme
|
colorScheme
|
||||||
avatarUrl
|
avatarUrl
|
||||||
locale
|
locale
|
||||||
|
userEmail
|
||||||
timeZone
|
timeZone
|
||||||
dateFormat
|
dateFormat
|
||||||
timeFormat
|
timeFormat
|
||||||
|
|||||||
@ -2,7 +2,13 @@ import styled from '@emotion/styled';
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
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 { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
@ -12,6 +18,7 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
|||||||
import { useGetRolesQuery } from '~/generated/graphql';
|
import { useGetRolesQuery } from '~/generated/graphql';
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { RolePermissions } from '~/pages/settings/roles/components/RolePermissions';
|
import { RolePermissions } from '~/pages/settings/roles/components/RolePermissions';
|
||||||
|
import { RoleSettings } from '~/pages/settings/roles/components/RoleSettings';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
import { RoleAssignment } from './components/RoleAssignment';
|
import { RoleAssignment } from './components/RoleAssignment';
|
||||||
|
|
||||||
@ -36,13 +43,16 @@ export const SETTINGS_ROLE_DETAIL_TABS = {
|
|||||||
TABS_IDS: {
|
TABS_IDS: {
|
||||||
ASSIGNMENT: 'assignment',
|
ASSIGNMENT: 'assignment',
|
||||||
PERMISSIONS: 'permissions',
|
PERMISSIONS: 'permissions',
|
||||||
|
SETTINGS: 'settings',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const SettingsRoleEdit = () => {
|
export const SettingsRoleEdit = () => {
|
||||||
const { roleId = '' } = useParams();
|
const { roleId = '' } = useParams();
|
||||||
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery();
|
|
||||||
const navigateSettings = useNavigateSettings();
|
const navigateSettings = useNavigateSettings();
|
||||||
|
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
const role = rolesData?.getRoles.find((r) => r.id === roleId);
|
const role = rolesData?.getRoles.find((r) => r.id === roleId);
|
||||||
|
|
||||||
@ -71,6 +81,12 @@ export const SettingsRoleEdit = () => {
|
|||||||
Icon: IconLockOpen,
|
Icon: IconLockOpen,
|
||||||
hide: false,
|
hide: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS,
|
||||||
|
title: t`Settings`,
|
||||||
|
Icon: IconSettings,
|
||||||
|
hide: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderActiveTabContent = () => {
|
const renderActiveTabContent = () => {
|
||||||
@ -79,6 +95,8 @@ export const SettingsRoleEdit = () => {
|
|||||||
return <RoleAssignment role={role} />;
|
return <RoleAssignment role={role} />;
|
||||||
case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS:
|
case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS:
|
||||||
return <RolePermissions role={role} />;
|
return <RolePermissions role={role} />;
|
||||||
|
case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS:
|
||||||
|
return <RoleSettings role={role} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,8 +55,7 @@ const StyledAvatarGroup = styled.div`
|
|||||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
border: 2px solid ${({ theme }) => theme.background.primary};
|
margin-left: -5px;
|
||||||
margin-left: -8px;
|
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@ -84,6 +83,11 @@ const StyledAvatarContainer = styled.div`
|
|||||||
border: 0px;
|
border: 0px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledAssignedText = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsRoles = () => {
|
export const SettingsRoles = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const isPermissionsEnabled = useIsFeatureEnabled(
|
const isPermissionsEnabled = useIsFeatureEnabled(
|
||||||
@ -91,8 +95,9 @@ export const SettingsRoles = () => {
|
|||||||
);
|
);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigateSettings = useNavigateSettings();
|
const navigateSettings = useNavigateSettings();
|
||||||
|
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
|
||||||
const { data: rolesData, loading: isRolesLoading } = useGetRolesQuery();
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
if (!isPermissionsEnabled) {
|
if (!isPermissionsEnabled) {
|
||||||
return null;
|
return null;
|
||||||
@ -131,7 +136,7 @@ export const SettingsRoles = () => {
|
|||||||
<TableHeader align={'right'}></TableHeader>
|
<TableHeader align={'right'}></TableHeader>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</StyledTableHeaderRow>
|
</StyledTableHeaderRow>
|
||||||
{!isRolesLoading &&
|
{!rolesLoading &&
|
||||||
rolesData?.getRoles.map((role) => (
|
rolesData?.getRoles.map((role) => (
|
||||||
<StyledTableRow
|
<StyledTableRow
|
||||||
key={role.id}
|
key={role.id}
|
||||||
@ -164,7 +169,7 @@ export const SettingsRoles = () => {
|
|||||||
workspaceMember.name.firstName ?? ''
|
workspaceMember.name.firstName ?? ''
|
||||||
}
|
}
|
||||||
type="rounded"
|
type="rounded"
|
||||||
size="sm"
|
size="md"
|
||||||
/>
|
/>
|
||||||
</StyledAvatarContainer>
|
</StyledAvatarContainer>
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
@ -178,7 +183,9 @@ export const SettingsRoles = () => {
|
|||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</StyledAvatarGroup>
|
</StyledAvatarGroup>
|
||||||
{role.workspaceMembers.length}
|
<StyledAssignedText>
|
||||||
|
{role.workspaceMembers.length}
|
||||||
|
</StyledAssignedText>
|
||||||
</StyledAssignedCell>
|
</StyledAssignedCell>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align={'right'}>
|
<TableCell align={'right'}>
|
||||||
|
|||||||
@ -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 { t } from '@lingui/core/macro';
|
||||||
import { H2Title, Section } from 'twenty-ui';
|
import { useState } from 'react';
|
||||||
import { Role } from '~/generated-metadata/graphql';
|
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 = {
|
type RoleAssignmentProps = {
|
||||||
role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'>;
|
role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'> & {
|
||||||
|
workspaceMembers: Array<WorkspaceMember>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
||||||
export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
||||||
|
const navigateSettings = useNavigateSettings();
|
||||||
|
const [updateWorkspaceMemberRole] = useUpdateWorkspaceMemberRoleMutation({
|
||||||
|
refetchQueries: [GetRolesDocument],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [modalMode, setModalMode] =
|
||||||
|
useState<RoleAssignmentConfirmationModalMode | null>(null);
|
||||||
|
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
|
||||||
|
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
|
||||||
|
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 (
|
return (
|
||||||
<Section>
|
<>
|
||||||
<H2Title
|
<Section>
|
||||||
title={t`Assigned members`}
|
<H2Title
|
||||||
description={t`This Role is assigned to these workspace member.`}
|
title={t`Assigned members`}
|
||||||
/>
|
description={t`This role is assigned to these workspace members.`}
|
||||||
</Section>
|
/>
|
||||||
|
<StyledSearchContainer>
|
||||||
|
<StyledSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
placeholder={t`Search a member`}
|
||||||
|
fullWidth
|
||||||
|
LeftIcon={IconSearch}
|
||||||
|
sizeVariant="lg"
|
||||||
|
/>
|
||||||
|
</StyledSearchContainer>
|
||||||
|
<Table>
|
||||||
|
<RoleAssignmentTableHeader />
|
||||||
|
{filteredWorkspaceMembers.map((workspaceMember) => (
|
||||||
|
<RoleAssignmentTableRow
|
||||||
|
key={workspaceMember.id}
|
||||||
|
workspaceMember={workspaceMember}
|
||||||
|
onRemove={() => handleRemoveClick(workspaceMember)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredWorkspaceMembers.length === 0 && (
|
||||||
|
<StyledEmptyText>
|
||||||
|
{searchFilter
|
||||||
|
? t`No members matching your search`
|
||||||
|
: t`No members assigned to this role yet`}
|
||||||
|
</StyledEmptyText>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</Section>
|
||||||
|
<StyledBottomSection hasRows={filteredWorkspaceMembers.length > 0}>
|
||||||
|
<Dropdown
|
||||||
|
dropdownId="role-member-select"
|
||||||
|
dropdownHotkeyScope={{ scope: 'roleAssignment' }}
|
||||||
|
clickableComponent={
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title={t`Assign to member`}
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<RoleWorkspaceMemberPickerDropdown
|
||||||
|
excludedWorkspaceMemberIds={role.workspaceMembers.map(
|
||||||
|
(workspaceMember) => workspaceMember.id,
|
||||||
|
)}
|
||||||
|
onSelect={handleSelectWorkspaceMember}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledBottomSection>
|
||||||
|
|
||||||
|
{modalMode && selectedWorkspaceMember && (
|
||||||
|
<RoleAssignmentConfirmationModal
|
||||||
|
mode={modalMode}
|
||||||
|
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||||
|
isOpen={true}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onRoleClick={handleRoleClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
setIsOpen={onClose}
|
||||||
|
title={title}
|
||||||
|
subtitle={
|
||||||
|
<RoleAssignmentConfirmationModalSubtitle
|
||||||
|
mode={mode}
|
||||||
|
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||||
|
onRoleClick={onRoleClick}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onConfirmClick={onConfirm}
|
||||||
|
deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`}
|
||||||
|
confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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:`}
|
||||||
|
<StyledSettingsCardContainer>
|
||||||
|
<SettingsCard
|
||||||
|
title={selectedWorkspaceMember.role?.label || ''}
|
||||||
|
Icon={<IconUser />}
|
||||||
|
onClick={() =>
|
||||||
|
selectedWorkspaceMember.role &&
|
||||||
|
onRoleClick(selectedWorkspaceMember.role.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledSettingsCardContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAssignMode
|
||||||
|
? t`Are you sure you want to assign this role?`
|
||||||
|
: t`This member will be unassigned from this role.`;
|
||||||
|
};
|
||||||
@ -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) => (
|
||||||
|
<StyledTableHeaderRow className={className}>
|
||||||
|
<TableRow gridAutoColumns="150px 1fr 1fr">
|
||||||
|
<TableHeader>{t`Name`}</TableHeader>
|
||||||
|
<TableHeader>{t`Email`}</TableHeader>
|
||||||
|
<TableHeader align={'right'} aria-label={t`Actions`}></TableHeader>
|
||||||
|
</TableRow>
|
||||||
|
</StyledTableHeaderRow>
|
||||||
|
);
|
||||||
@ -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 (
|
||||||
|
<StyledTable>
|
||||||
|
<TableRow gridAutoColumns="150px 1fr 1fr">
|
||||||
|
<TableCell>
|
||||||
|
<StyledIconWrapper>
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={workspaceMember.avatarUrl}
|
||||||
|
placeholderColorSeed={workspaceMember.id}
|
||||||
|
placeholder={workspaceMember.name.firstName ?? ''}
|
||||||
|
type="rounded"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</StyledIconWrapper>
|
||||||
|
<OverflowingTextWithTooltip
|
||||||
|
text={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<OverflowingTextWithTooltip text={workspaceMember.userEmail} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align={'right'}>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleRemoveClick}
|
||||||
|
variant="tertiary"
|
||||||
|
size="medium"
|
||||||
|
Icon={IconTrash}
|
||||||
|
aria-label={t`Remove`}
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</StyledTable>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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<Role, 'id' | 'label' | 'description'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoleSettings = ({ role }: RoleSettingsProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<StyledInputContainer>
|
||||||
|
<IconPicker
|
||||||
|
disabled={true}
|
||||||
|
selectedIconKey={'IconUser'}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
|
<TextInput value={role.label} disabled fullWidth />
|
||||||
|
</StyledInputsContainer>
|
||||||
|
<TextArea
|
||||||
|
minRows={4}
|
||||||
|
placeholder={t`Write a description`}
|
||||||
|
value={role.description || ''}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ChangeEvent, useState } from 'react';
|
||||||
|
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||||
|
import { RoleWorkspaceMemberPickerDropdownContent } from './RoleWorkspaceMemberPickerDropdownContent';
|
||||||
|
|
||||||
|
const StyledWorkspaceMemberSelectContainer = styled.div`
|
||||||
|
max-height: ${({ theme }) => theme.spacing(50)};
|
||||||
|
overflow-y: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RoleWorkspaceMemberPickerDropdownProps = {
|
||||||
|
excludedWorkspaceMemberIds: string[];
|
||||||
|
onSelect: (workspaceMember: WorkspaceMember) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoleWorkspaceMemberPickerDropdown = ({
|
||||||
|
excludedWorkspaceMemberIds,
|
||||||
|
onSelect,
|
||||||
|
}: RoleWorkspaceMemberPickerDropdownProps) => {
|
||||||
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
|
const { records: workspaceMembers, loading } = useFindManyRecords({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||||
|
filter: searchFilter
|
||||||
|
? {
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
name: { firstName: { ilike: `%${searchFilter}%` } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: { lastName: { ilike: `%${searchFilter}%` } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userEmail: { ilike: `%${searchFilter}%` },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredWorkspaceMembers = (workspaceMembers?.filter(
|
||||||
|
(workspaceMember) =>
|
||||||
|
!excludedWorkspaceMemberIds.includes(workspaceMember.id),
|
||||||
|
) ?? []) as WorkspaceMember[];
|
||||||
|
|
||||||
|
const handleSearchFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchFilter(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleSearchFilterChange}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
<StyledWorkspaceMemberSelectContainer>
|
||||||
|
<RoleWorkspaceMemberPickerDropdownContent
|
||||||
|
loading={loading}
|
||||||
|
searchFilter={searchFilter}
|
||||||
|
filteredWorkspaceMembers={filteredWorkspaceMembers}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
</StyledWorkspaceMemberSelectContainer>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { Avatar } from 'twenty-ui';
|
||||||
|
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
const StyledEmptyState = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
display: flex;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledWorkspaceMemberItem = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
min-width: ${({ theme }) => theme.spacing(45)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme }) => theme.background.tertiary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledWorkspaceMemberName = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RoleWorkspaceMemberPickerDropdownContentProps = {
|
||||||
|
loading: boolean;
|
||||||
|
searchFilter: string;
|
||||||
|
filteredWorkspaceMembers: WorkspaceMember[];
|
||||||
|
onSelect: (workspaceMember: WorkspaceMember) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoleWorkspaceMemberPickerDropdownContent = ({
|
||||||
|
loading,
|
||||||
|
searchFilter,
|
||||||
|
filteredWorkspaceMembers,
|
||||||
|
onSelect,
|
||||||
|
}: RoleWorkspaceMemberPickerDropdownContentProps) => {
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filteredWorkspaceMembers?.length) {
|
||||||
|
return (
|
||||||
|
<StyledEmptyState>
|
||||||
|
{searchFilter
|
||||||
|
? t`No members matching this search`
|
||||||
|
: t`No more members to add`}
|
||||||
|
</StyledEmptyState>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{filteredWorkspaceMembers.map((workspaceMember) => (
|
||||||
|
<StyledWorkspaceMemberItem
|
||||||
|
key={workspaceMember.id}
|
||||||
|
onClick={() => onSelect(workspaceMember)}
|
||||||
|
aria-label={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
type="rounded"
|
||||||
|
size="md"
|
||||||
|
placeholderColorSeed={workspaceMember.id}
|
||||||
|
placeholder={workspaceMember.name.firstName ?? ''}
|
||||||
|
/>
|
||||||
|
<StyledWorkspaceMemberName>
|
||||||
|
{workspaceMember.name.firstName} {workspaceMember.name.lastName}
|
||||||
|
</StyledWorkspaceMemberName>
|
||||||
|
</StyledWorkspaceMemberItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove';
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export type RoleAssignmentConfirmationModalSelectedWorkspaceMember = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role?: { id: string; label: string };
|
||||||
|
};
|
||||||
@ -20,6 +20,7 @@ export const mockedTimelineActivities: Array<TimelineActivity> = [
|
|||||||
firstName: 'Tim',
|
firstName: 'Tim',
|
||||||
lastName: 'Apple',
|
lastName: 'Apple',
|
||||||
},
|
},
|
||||||
|
userEmail: 'tim@apple.com',
|
||||||
colorScheme: 'Light',
|
colorScheme: 'Light',
|
||||||
},
|
},
|
||||||
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
@ -45,6 +46,7 @@ export const mockedTimelineActivities: Array<TimelineActivity> = [
|
|||||||
firstName: 'Tim',
|
firstName: 'Tim',
|
||||||
lastName: 'Apple',
|
lastName: 'Apple',
|
||||||
},
|
},
|
||||||
|
userEmail: 'tim@apple.com',
|
||||||
colorScheme: 'Light',
|
colorScheme: 'Light',
|
||||||
},
|
},
|
||||||
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
@ -70,6 +72,7 @@ export const mockedTimelineActivities: Array<TimelineActivity> = [
|
|||||||
firstName: 'Tim',
|
firstName: 'Tim',
|
||||||
lastName: 'Apple',
|
lastName: 'Apple',
|
||||||
},
|
},
|
||||||
|
userEmail: 'tim@apple.com',
|
||||||
colorScheme: 'Light',
|
colorScheme: 'Light',
|
||||||
},
|
},
|
||||||
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
@ -102,6 +105,7 @@ export const mockedTimelineActivities: Array<TimelineActivity> = [
|
|||||||
firstName: 'Jane',
|
firstName: 'Jane',
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
},
|
},
|
||||||
|
userEmail: 'jane@doe.com',
|
||||||
colorScheme: 'Light',
|
colorScheme: 'Light',
|
||||||
},
|
},
|
||||||
workspaceMemberId: '20202020-1553-45c6-a028-5a9064cce07f',
|
workspaceMemberId: '20202020-1553-45c6-a028-5a9064cce07f',
|
||||||
@ -128,6 +132,7 @@ export const mockedTimelineActivities: Array<TimelineActivity> = [
|
|||||||
firstName: 'Tim',
|
firstName: 'Tim',
|
||||||
lastName: 'Apple',
|
lastName: 'Apple',
|
||||||
},
|
},
|
||||||
|
userEmail: 'tim@apple.com',
|
||||||
colorScheme: 'Light',
|
colorScheme: 'Light',
|
||||||
},
|
},
|
||||||
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
|
|||||||
@ -26,6 +26,9 @@ export class WorkspaceMember {
|
|||||||
@Field(() => FullName)
|
@Field(() => FullName)
|
||||||
name: FullName;
|
name: FullName;
|
||||||
|
|
||||||
|
@Field({ nullable: false })
|
||||||
|
userEmail: string;
|
||||||
|
|
||||||
@Field({ nullable: false })
|
@Field({ nullable: false })
|
||||||
colorScheme: string;
|
colorScheme: string;
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Catch, ExceptionFilter } from '@nestjs/common';
|
|||||||
import {
|
import {
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
|
NotFoundError,
|
||||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import {
|
import {
|
||||||
PermissionsException,
|
PermissionsException,
|
||||||
@ -16,6 +17,9 @@ export class PermissionsGraphqlApiExceptionFilter implements ExceptionFilter {
|
|||||||
case PermissionsExceptionCode.PERMISSION_DENIED:
|
case PermissionsExceptionCode.PERMISSION_DENIED:
|
||||||
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
||||||
throw new ForbiddenError(exception.message);
|
throw new ForbiddenError(exception.message);
|
||||||
|
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||||
|
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||||
|
throw new NotFoundError(exception.message);
|
||||||
default:
|
default:
|
||||||
throw new InternalServerError(exception.message);
|
throw new InternalServerError(exception.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,6 @@ const StyledButton = styled('button', {
|
|||||||
: theme.background.transparent.light
|
: theme.background.transparent.light
|
||||||
: theme.background.transparent.light};
|
: theme.background.transparent.light};
|
||||||
border-width: 1px 1px 1px 1px !important;
|
border-width: 1px 1px 1px 1px !important;
|
||||||
opacity: ${disabled ? 0.24 : 1};
|
|
||||||
box-shadow: ${!disabled && focus
|
box-shadow: ${!disabled && focus
|
||||||
? `0 0 0 3px ${
|
? `0 0 0 3px ${
|
||||||
!inverted
|
!inverted
|
||||||
@ -112,7 +111,6 @@ const StyledButton = styled('button', {
|
|||||||
}`
|
}`
|
||||||
: 'none'};
|
: 'none'};
|
||||||
color: ${!inverted ? theme.grayScale.gray0 : theme.color.blue};
|
color: ${!inverted ? theme.grayScale.gray0 : theme.color.blue};
|
||||||
opacity: ${disabled ? 0.24 : 1};
|
|
||||||
${disabled
|
${disabled
|
||||||
? ''
|
? ''
|
||||||
: css`
|
: css`
|
||||||
@ -147,7 +145,6 @@ const StyledButton = styled('button', {
|
|||||||
}`
|
}`
|
||||||
: 'none'};
|
: 'none'};
|
||||||
color: ${!inverted ? theme.background.primary : theme.color.red};
|
color: ${!inverted ? theme.background.primary : theme.color.red};
|
||||||
opacity: ${disabled ? 0.24 : 1};
|
|
||||||
${disabled
|
${disabled
|
||||||
? ''
|
? ''
|
||||||
: css`
|
: css`
|
||||||
@ -194,7 +191,6 @@ const StyledButton = styled('button', {
|
|||||||
: theme.background.transparent.medium
|
: theme.background.transparent.medium
|
||||||
}`
|
}`
|
||||||
: 'none'};
|
: 'none'};
|
||||||
opacity: ${disabled ? 0.24 : 1};
|
|
||||||
color: ${!inverted
|
color: ${!inverted
|
||||||
? !disabled
|
? !disabled
|
||||||
? theme.font.color.secondary
|
? theme.font.color.secondary
|
||||||
@ -241,7 +237,6 @@ const StyledButton = styled('button', {
|
|||||||
: theme.background.transparent.medium
|
: theme.background.transparent.medium
|
||||||
}`
|
}`
|
||||||
: 'none'};
|
: 'none'};
|
||||||
opacity: ${disabled ? 0.24 : 1};
|
|
||||||
color: ${!inverted
|
color: ${!inverted
|
||||||
? !disabled
|
? !disabled
|
||||||
? theme.color.blue
|
? theme.color.blue
|
||||||
@ -288,7 +283,6 @@ const StyledButton = styled('button', {
|
|||||||
: theme.background.transparent.medium
|
: theme.background.transparent.medium
|
||||||
}`
|
}`
|
||||||
: 'none'};
|
: 'none'};
|
||||||
opacity: ${disabled ? 0.24 : 1};
|
|
||||||
color: ${!inverted
|
color: ${!inverted
|
||||||
? theme.font.color.danger
|
? theme.font.color.danger
|
||||||
: theme.font.color.inverted};
|
: theme.font.color.inverted};
|
||||||
|
|||||||
Reference in New Issue
Block a user