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

View File

@ -1250,7 +1250,7 @@ export type Query = {
getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: BillingProductPricesOutput;
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
getRoles: Array<RoleDto>;
getRoles: Array<Role>;
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
@ -1449,8 +1449,8 @@ export type ResendEmailVerificationTokenOutput = {
success: Scalars['Boolean'];
};
export type RoleDto = {
__typename?: 'RoleDTO';
export type Role = {
__typename?: 'Role';
canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<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 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<{
input: SetupOidcSsoInput;
}>;
@ -3936,6 +3941,47 @@ export function useUpdateLabPublicFeatureFlagMutation(baseOptions?: Apollo.Mutat
export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof useUpdateLabPublicFeatureFlagMutation>;
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
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`
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
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 { 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 { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { 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';
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;
display: grid;
grid-template-columns: 1fr 1fr;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAssignedCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledAvatarGroup = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
> * {
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 = () => {
@ -22,22 +88,9 @@ export const SettingsRoles = () => {
const isPermissionsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
);
const theme = useTheme();
const GET_SETTINGS_ROLE_TABLE_METADATA = {
tableId: 'settingsRole',
fields: [
{
fieldName: 'name',
fieldLabel: t`Name`,
align: 'left' as const,
},
{
fieldName: 'assignedTo',
fieldLabel: t`Assigned to`,
align: 'left' as const,
},
],
};
const { data: rolesData, loading: isRolesLoading } = useGetRolesQuery();
if (!isPermissionsEnabled) {
return null;
@ -46,14 +99,6 @@ export const SettingsRoles = () => {
return (
<SubMenuTopBarContainer
title={t`Roles`}
actionButton={
<Button
Icon={IconPlus}
title={t`New Role`}
accent="blue"
size="small"
/>
}
links={[
{
children: <Trans>Workspace</Trans>,
@ -68,21 +113,80 @@ export const SettingsRoles = () => {
title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`}
/>
<Table>
<StyledRoleTableRow>
{GET_SETTINGS_ROLE_TABLE_METADATA.fields.map(
(settingsRoleTableMetadataField) => (
<TableHeader
key={settingsRoleTableMetadataField.fieldName}
align={settingsRoleTableMetadataField.align}
>
{settingsRoleTableMetadataField.fieldLabel}
</TableHeader>
),
)}
</StyledRoleTableRow>
</Table>
<StyledTable>
<StyledTableHeaderRow>
<TableRow>
<TableHeader>
<Trans>Name</Trans>
</TableHeader>
<TableHeader align={'right'}>
<Trans>Assigned to</Trans>
</TableHeader>
<TableHeader align={'right'}></TableHeader>
</TableRow>
</StyledTableHeaderRow>
{!isRolesLoading &&
rolesData?.getRoles.map((role) => (
<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>
</SettingsPageContainer>
</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 { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.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 { AnalyticsModule } from './analytics/analytics.module';
@ -74,6 +75,7 @@ import { FileModule } from './file/file.module';
TelemetryModule,
AdminPanelModule,
LabModule,
RoleModule,
EnvironmentModule.forRoot({}),
RedisClientModule,
FileStorageModule.forRootAsync({

View File

@ -54,7 +54,7 @@ export class PermissionsService {
}
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,
);
}

View File

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