Fetch roles in roles settings page (#10001)

## Context
Following the addition of the new Roles page, we are now fetching roles
from the DB thanks to this PR #9955

## Test
<img width="1136" alt="Screenshot 2025-02-04 at 14 46 21"
src="https://github.com/user-attachments/assets/2c55c4d0-ee51-47bb-8113-efce172a9365"
/>

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Weiko
2025-02-05 14:22:00 +01:00
committed by GitHub
parent 3e05c3743e
commit 36d148d5e5
7 changed files with 221 additions and 51 deletions

View File

@ -1386,7 +1386,7 @@ export type Query = {
getPostgresCredentials?: Maybe<PostgresCredentials>; getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: BillingProductPricesOutput; getProductPrices: BillingProductPricesOutput;
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput; getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
getRoles: Array<RoleDto>; getRoles: Array<Role>;
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>; getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
@ -1653,8 +1653,8 @@ export type ResendEmailVerificationTokenOutput = {
success: Scalars['Boolean']['output']; success: Scalars['Boolean']['output'];
}; };
export type RoleDto = { export type Role = {
__typename?: 'RoleDTO'; __typename?: 'Role';
canUpdateAllSettings: Scalars['Boolean']['output']; canUpdateAllSettings: Scalars['Boolean']['output'];
description?: Maybe<Scalars['String']['output']>; description?: Maybe<Scalars['String']['output']>;
id: Scalars['String']['output']; id: Scalars['String']['output'];

View File

@ -1250,7 +1250,7 @@ export type Query = {
getPostgresCredentials?: Maybe<PostgresCredentials>; getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: BillingProductPricesOutput; getProductPrices: BillingProductPricesOutput;
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput; getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
getRoles: Array<RoleDto>; getRoles: Array<Role>;
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>; getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
@ -1449,8 +1449,8 @@ export type ResendEmailVerificationTokenOutput = {
success: Scalars['Boolean']; success: Scalars['Boolean'];
}; };
export type RoleDto = { export type Role = {
__typename?: 'RoleDTO'; __typename?: 'Role';
canUpdateAllSettings: Scalars['Boolean']; canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<Scalars['String']>; description?: Maybe<Scalars['String']>;
id: Scalars['String']; id: Scalars['String'];
@ -2242,6 +2242,11 @@ 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 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 CreateOidcIdentityProviderMutationVariables = Exact<{ export type CreateOidcIdentityProviderMutationVariables = Exact<{
input: SetupOidcSsoInput; input: SetupOidcSsoInput;
}>; }>;
@ -3936,6 +3941,47 @@ 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 GetRolesDocument = gql`
query GetRoles {
getRoles {
id
label
description
canUpdateAllSettings
isEditable
workspaceMembers {
...WorkspaceMemberQueryFragment
}
}
}
${WorkspaceMemberQueryFragmentFragmentDoc}`;
/**
* __useGetRolesQuery__
*
* To run a query within a React component, call `useGetRolesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRolesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRolesQuery({
* variables: {
* },
* });
*/
export function useGetRolesQuery(baseOptions?: Apollo.QueryHookOptions<GetRolesQuery, GetRolesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRolesQuery, GetRolesQueryVariables>(GetRolesDocument, options);
}
export function useGetRolesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRolesQuery, GetRolesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRolesQuery, GetRolesQueryVariables>(GetRolesDocument, options);
}
export type GetRolesQueryHookResult = ReturnType<typeof useGetRolesQuery>;
export type GetRolesLazyQueryHookResult = ReturnType<typeof useGetRolesLazyQuery>;
export type GetRolesQueryResult = Apollo.QueryResult<GetRolesQuery, GetRolesQueryVariables>;
export const CreateOidcIdentityProviderDocument = gql` export const CreateOidcIdentityProviderDocument = gql`
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
createOIDCIdentityProvider(input: $input) { createOIDCIdentityProvider(input: $input) {

View File

@ -0,0 +1,18 @@
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}
query GetRoles {
getRoles {
id
label
description
canUpdateAllSettings
isEditable
workspaceMembers {
...WorkspaceMemberQueryFragment
}
}
}
`;

View File

@ -1,20 +1,86 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { Button, H2Title, IconPlus, Section } from 'twenty-ui'; import {
AppTooltip,
Avatar,
Button,
H2Title,
IconChevronRight,
IconLock,
IconPlus,
IconUser,
Section,
TooltipDelay,
} 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';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Table } from '@/ui/layout/table/components/Table'; import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql'; import { useTheme } from '@emotion/react';
import { FeatureFlagKey, useGetRolesQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledRoleTableRow = styled.div` const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledTableRow = styled(TableRow)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
cursor: pointer;
}
`;
const StyledNameCell = styled.div`
align-items: center; align-items: center;
display: grid; display: flex;
grid-template-columns: 1fr 1fr; gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAssignedCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledAvatarGroup = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
> * {
border: 2px solid ${({ theme }) => theme.background.primary};
margin-left: -8px;
&:first-of-type {
margin-left: 0;
}
}
`;
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
`;
const StyledBottomSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
margin-top: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(4)};
display: flex;
justify-content: flex-end;
`;
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledAvatarContainer = styled.div`
border: 0px;
`; `;
export const SettingsRoles = () => { export const SettingsRoles = () => {
@ -22,22 +88,9 @@ export const SettingsRoles = () => {
const isPermissionsEnabled = useIsFeatureEnabled( const isPermissionsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled, FeatureFlagKey.IsPermissionsEnabled,
); );
const theme = useTheme();
const GET_SETTINGS_ROLE_TABLE_METADATA = { const { data: rolesData, loading: isRolesLoading } = useGetRolesQuery();
tableId: 'settingsRole',
fields: [
{
fieldName: 'name',
fieldLabel: t`Name`,
align: 'left' as const,
},
{
fieldName: 'assignedTo',
fieldLabel: t`Assigned to`,
align: 'left' as const,
},
],
};
if (!isPermissionsEnabled) { if (!isPermissionsEnabled) {
return null; return null;
@ -46,14 +99,6 @@ export const SettingsRoles = () => {
return ( return (
<SubMenuTopBarContainer <SubMenuTopBarContainer
title={t`Roles`} title={t`Roles`}
actionButton={
<Button
Icon={IconPlus}
title={t`New Role`}
accent="blue"
size="small"
/>
}
links={[ links={[
{ {
children: <Trans>Workspace</Trans>, children: <Trans>Workspace</Trans>,
@ -68,21 +113,80 @@ export const SettingsRoles = () => {
title={t`All roles`} title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`} description={t`Assign roles to specify each member's access permissions`}
/> />
<StyledTable>
<Table> <StyledTableHeaderRow>
<StyledRoleTableRow> <TableRow>
{GET_SETTINGS_ROLE_TABLE_METADATA.fields.map( <TableHeader>
(settingsRoleTableMetadataField) => ( <Trans>Name</Trans>
<TableHeader </TableHeader>
key={settingsRoleTableMetadataField.fieldName} <TableHeader align={'right'}>
align={settingsRoleTableMetadataField.align} <Trans>Assigned to</Trans>
> </TableHeader>
{settingsRoleTableMetadataField.fieldLabel} <TableHeader align={'right'}></TableHeader>
</TableHeader> </TableRow>
), </StyledTableHeaderRow>
)} {!isRolesLoading &&
</StyledRoleTableRow> rolesData?.getRoles.map((role) => (
</Table> <StyledTableRow key={role.id}>
<TableCell>
<StyledNameCell>
<IconUser size={theme.icon.size.md} />
{role.label}
{!role.isEditable && (
<IconLock size={theme.icon.size.sm} />
)}
</StyledNameCell>
</TableCell>
<TableCell align={'right'}>
<StyledAssignedCell>
<StyledAvatarGroup>
{role.workspaceMembers
.slice(0, 5)
.map((workspaceMember) => (
<>
<StyledAvatarContainer
key={workspaceMember.id}
id={`avatar-${workspaceMember.id}`}
>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
placeholderColorSeed={workspaceMember.id}
placeholder={
workspaceMember.name.firstName ?? ''
}
type="rounded"
size="sm"
/>
</StyledAvatarContainer>
<AppTooltip
anchorSelect={`#avatar-${workspaceMember.id}`}
content={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
noArrow
place="top"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</>
))}
</StyledAvatarGroup>
{role.workspaceMembers.length}
</StyledAssignedCell>
</TableCell>
<TableCell align={'right'}>
<StyledIconChevronRight size={theme.icon.size.md} />
</TableCell>
</StyledTableRow>
))}
</StyledTable>
<StyledBottomSection>
<Button
Icon={IconPlus}
title={t`Create Role`}
variant="secondary"
size="small"
soon
/>
</StyledBottomSection>
</Section> </Section>
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>

View File

@ -44,6 +44,7 @@ import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkflowApiModule } from 'src/engine/core-modules/workflow/workflow-api.module'; import { WorkflowApiModule } from 'src/engine/core-modules/workflow/workflow-api.module';
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { AnalyticsModule } from './analytics/analytics.module'; import { AnalyticsModule } from './analytics/analytics.module';
@ -74,6 +75,7 @@ import { FileModule } from './file/file.module';
TelemetryModule, TelemetryModule,
AdminPanelModule, AdminPanelModule,
LabModule, LabModule,
RoleModule,
EnvironmentModule.forRoot({}), EnvironmentModule.forRoot({}),
RedisClientModule, RedisClientModule,
FileStorageModule.forRootAsync({ FileStorageModule.forRootAsync({

View File

@ -54,7 +54,7 @@ export class PermissionsService {
} }
throw new PermissionsException( throw new PermissionsException(
`User does not have permission to update this setting: ${setting}`, `User does not have permission to access this setting: ${setting}`,
PermissionsExceptionCode.PERMISSION_DENIED, PermissionsExceptionCode.PERMISSION_DENIED,
); );
} }

View File

@ -5,7 +5,7 @@ import { Relation } from 'typeorm';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
@ObjectType() @ObjectType('Role')
export class RoleDTO { export class RoleDTO {
@Field({ nullable: false }) @Field({ nullable: false })
id: string; id: string;