Add default role to workspace (#10444)

## Context
Adding a defaultRole to each workspace, this role will be automatically
added when a member joins a workspace via invite link or public link
(seeds work differently though).
Took the occasion to refactor a bit the frontend components, splitting
them in smaller components for more readability.

## Test
<img width="948" alt="Screenshot 2025-02-24 at 14 54 02"
src="https://github.com/user-attachments/assets/13ef1452-d3c9-4385-940c-2ced0f0b05ef"
/>
This commit is contained in:
Weiko
2025-02-25 11:26:35 +01:00
committed by GitHub
parent a1eea40cf7
commit 0220672fa9
29 changed files with 538 additions and 273 deletions

View File

@ -2045,6 +2045,7 @@ export type UpdateWorkflowVersionStepInput = {
export type UpdateWorkspaceInput = {
allowImpersonation?: InputMaybe<Scalars['Boolean']['input']>;
customDomain?: InputMaybe<Scalars['String']['input']>;
defaultRoleId?: InputMaybe<Scalars['String']['input']>;
displayName?: InputMaybe<Scalars['String']['input']>;
inviteHash?: InputMaybe<Scalars['String']['input']>;
isGoogleAuthEnabled?: InputMaybe<Scalars['Boolean']['input']>;
@ -2197,6 +2198,7 @@ export type Workspace = {
customDomain?: Maybe<Scalars['String']['output']>;
databaseSchema: Scalars['String']['output'];
databaseUrl: Scalars['String']['output'];
defaultRole?: Maybe<Role>;
deletedAt?: Maybe<Scalars['DateTime']['output']>;
displayName?: Maybe<Scalars['String']['output']>;
featureFlags?: Maybe<Array<FeatureFlag>>;

View File

@ -1833,6 +1833,7 @@ export type UpdateWorkflowVersionStepInput = {
export type UpdateWorkspaceInput = {
allowImpersonation?: InputMaybe<Scalars['Boolean']>;
customDomain?: InputMaybe<Scalars['String']>;
defaultRoleId?: InputMaybe<Scalars['String']>;
displayName?: InputMaybe<Scalars['String']>;
inviteHash?: InputMaybe<Scalars['String']>;
isGoogleAuthEnabled?: InputMaybe<Scalars['Boolean']>;
@ -1975,6 +1976,7 @@ export type Workspace = {
customDomain?: Maybe<Scalars['String']>;
databaseSchema: Scalars['String'];
databaseUrl: Scalars['String'];
defaultRole?: Maybe<Role>;
deletedAt?: Maybe<Scalars['DateTime']>;
displayName?: Maybe<Scalars['String']>;
featureFlags?: Maybe<Array<FeatureFlag>>;
@ -2436,7 +2438,7 @@ export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never
export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -2453,7 +2455,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@ -2556,7 +2558,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{
}>;
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, customDomain?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean } };
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, customDomain?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } };
export type UploadWorkspaceLogoMutationVariables = Exact<{
file: Scalars['Upload'];
@ -2676,19 +2678,6 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
}
}
`;
export const RoleFragmentFragmentDoc = gql`
fragment RoleFragment on Role {
id
label
description
canUpdateAllSettings
isEditable
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
}
`;
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
id
@ -2705,6 +2694,19 @@ export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
timeFormat
}
`;
export const RoleFragmentFragmentDoc = gql`
fragment RoleFragment on Role {
id
label
description
canUpdateAllSettings
isEditable
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
}
`;
export const UserQueryFragmentFragmentDoc = gql`
fragment UserQueryFragment on User {
id
@ -2768,6 +2770,9 @@ export const UserQueryFragmentFragmentDoc = gql`
status
}
workspaceMembersCount
defaultRole {
...RoleFragment
}
}
workspaces {
workspace {
@ -2784,7 +2789,8 @@ export const UserQueryFragmentFragmentDoc = gql`
}
userVars
}
${WorkspaceMemberQueryFragmentFragmentDoc}`;
${WorkspaceMemberQueryFragmentFragmentDoc}
${RoleFragmentFragmentDoc}`;
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {
getTimelineCalendarEventsFromCompanyId(
@ -5217,9 +5223,12 @@ export const UpdateWorkspaceDocument = gql`
isGoogleAuthEnabled
isMicrosoftAuthEnabled
isPasswordAuthEnabled
defaultRole {
...RoleFragment
}
}
}
`;
${RoleFragmentFragmentDoc}`;
export type UpdateWorkspaceMutationFn = Apollo.MutationFunction<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
/**

View File

@ -1,6 +1,6 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { Workspace } from '~/generated/graphql';
import { Role, Workspace } from '~/generated/graphql';
export type CurrentWorkspace = Pick<
Workspace,
@ -23,7 +23,9 @@ export type CurrentWorkspace = Pick<
| 'customDomain'
| 'workspaceUrls'
| 'metadataVersion'
>;
> & {
defaultRole?: Omit<Role, 'workspaceMembers'> | null;
};
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
key: 'currentWorkspaceState',

View File

@ -342,6 +342,17 @@ export const responseData = {
metadataVersion: 1,
currentBillingSubscription: null,
workspaceMembersCount: 1,
defaultRole: {
id: 'default-role-id',
label: 'Default Role',
description: 'Default Role Description',
canUpdateAllSettings: true,
isEditable: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
}
},
currentBillingSubscription: null,
billingSubscriptions: [],

View File

@ -21,6 +21,16 @@ import {
responseData as findManyObjectMetadataItemsResponseData,
} from '../__mocks__/useFindManyObjectMetadataItems';
jest.mock('@/object-metadata/hooks/useUpdateOneFieldMetadataItem', () => ({
useUpdateOneFieldMetadataItem: () => ({
updateOneFieldMetadataItem: jest.fn().mockResolvedValue({
data: {
updateOneField: responseData.default,
},
}),
}),
}));
const fieldMetadataItem: FieldMetadataItem = {
id: FIELD_METADATA_ID,
createdAt: '',
@ -111,17 +121,6 @@ const mocks = [
},
})),
},
{
request: {
query: queries.activateMetadataField,
variables: variables.activateMetadataField,
},
result: jest.fn(() => ({
data: {
updateOneField: responseData.default,
},
})),
},
{
request: {
query: queries.createMetadataField,
@ -133,26 +132,6 @@ const mocks = [
},
})),
},
{
request: {
query: queries.activateMetadataField,
variables: variables.deactivateMetadataField,
},
result: jest.fn(() => ({
data: {
updateOneField: responseData.default,
},
})),
},
{
request: {
query: queries.getCurrentUser,
variables: {},
},
result: jest.fn(() => ({
data: responseData.getCurrentUser,
})),
},
{
request: {
query: queries.getCurrentUser,

View File

@ -57,7 +57,10 @@ export const UserProviderEffect = () => {
setCurrentUser(queryData.currentUser);
if (isDefined(queryData.currentUser.currentWorkspace)) {
setCurrentWorkspace(queryData.currentUser.currentWorkspace);
setCurrentWorkspace({
...queryData.currentUser.currentWorkspace,
defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null,
});
}
if (isDefined(queryData.currentUser.currentUserWorkspace)) {

View File

@ -1,7 +1,9 @@
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
import { gql } from '@apollo/client';
export const USER_QUERY_FRAGMENT = gql`
${ROLE_FRAGMENT}
fragment UserQueryFragment on User {
id
firstName
@ -64,6 +66,9 @@ export const USER_QUERY_FRAGMENT = gql`
status
}
workspaceMembersCount
defaultRole {
...RoleFragment
}
}
workspaces {
workspace {

View File

@ -1,6 +1,8 @@
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { gql } from '@apollo/client';
export const UPDATE_WORKSPACE = gql`
${ROLE_FRAGMENT}
mutation UpdateWorkspace($input: UpdateWorkspaceInput!) {
updateWorkspace(data: $input) {
id
@ -13,6 +15,9 @@ export const UPDATE_WORKSPACE = gql`
isGoogleAuthEnabled
isMicrosoftAuthEnabled
isPasswordAuthEnabled
defaultRole {
...RoleFragment
}
}
}
`;

View File

@ -1,106 +1,19 @@
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import {
AppTooltip,
Avatar,
Button,
H2Title,
IconChevronRight,
IconLock,
IconPlus,
IconUser,
Section,
TooltipDelay,
} from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import React from 'react';
import { useGetRolesQuery } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { Roles } from '~/pages/settings/roles/components/Roles';
import { RolesDefaultRole } from '~/pages/settings/roles/components/RolesDefaultRole';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledTableRow = styled(TableRow)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
cursor: pointer;
}
`;
const StyledNameCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAssignedCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledAvatarGroup = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
> * {
margin-left: -5px;
&:first-of-type {
margin-left: 0;
}
}
`;
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledBottomSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
margin-top: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(4)};
display: flex;
justify-content: flex-end;
`;
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledAvatarContainer = styled.div`
border: 0px;
`;
const StyledAssignedText = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
`;
export const SettingsRoles = () => {
const { t } = useLingui();
const theme = useTheme();
const navigateSettings = useNavigateSettings();
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
fetchPolicy: 'network-only',
});
const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId });
};
return (
<SubMenuTopBarContainer
title={t`Roles`}
@ -113,90 +26,12 @@ export const SettingsRoles = () => {
]}
>
<SettingsPageContainer>
<Section>
<H2Title
title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`}
/>
<StyledTable>
<StyledTableHeaderRow>
<TableRow>
<TableHeader>
<Trans>Name</Trans>
</TableHeader>
<TableHeader align={'right'}>
<Trans>Assigned to</Trans>
</TableHeader>
<TableHeader align={'right'}></TableHeader>
</TableRow>
</StyledTableHeaderRow>
{!rolesLoading &&
rolesData?.getRoles.map((role) => (
<StyledTableRow
key={role.id}
onClick={() => handleRoleClick(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) => (
<React.Fragment key={workspaceMember.id}>
<StyledAvatarContainer
id={`avatar-${workspaceMember.id}`}
>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
placeholderColorSeed={workspaceMember.id}
placeholder={
workspaceMember.name.firstName ?? ''
}
type="rounded"
size="md"
/>
</StyledAvatarContainer>
<AppTooltip
anchorSelect={`#avatar-${workspaceMember.id}`}
content={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
noArrow
place="top"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</React.Fragment>
))}
</StyledAvatarGroup>
<StyledAssignedText>
{role.workspaceMembers.length}
</StyledAssignedText>
</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>
{!rolesLoading && (
<>
<Roles roles={rolesData?.getRoles ?? []} />
<RolesDefaultRole roles={rolesData?.getRoles ?? []} />
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);

View File

@ -6,8 +6,8 @@ import { RolePermissionsObjectPermission } from '~/pages/settings/roles/types/Ro
const StyledIconWrapper = styled.div`
align-items: center;
background: ${({ theme }) => theme.color.blue10};
border: 1px solid ${({ theme }) => theme.color.blue30};
background: ${({ theme }) => theme.adaptiveColors.blue1};
border: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
height: ${({ theme }) => theme.spacing(4)};

View File

@ -0,0 +1,46 @@
import { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { Button, H2Title, IconPlus, Section } from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql';
import { RolesTableHeader } from '~/pages/settings/roles/components/RolesTableHeader';
import { RolesTableRow } from '~/pages/settings/roles/components/RolesTableRow';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledBottomSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
margin-top: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(4)};
display: flex;
justify-content: flex-end;
`;
export const Roles = ({ roles }: { roles: Role[] }) => {
return (
<Section>
<H2Title
title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`}
/>
<StyledTable>
<RolesTableHeader />
{roles.map((role) => (
<RolesTableRow key={role.id} role={role} />
))}
</StyledTable>
<StyledBottomSection>
<Button
Icon={IconPlus}
title={t`Create Role`}
variant="secondary"
size="small"
soon
/>
</StyledBottomSection>
</Section>
);
};

View File

@ -0,0 +1,80 @@
import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { Card, H2Title, IconUserPin, Section } from 'twenty-ui';
import {
Role,
UpdateWorkspaceMutation,
useUpdateWorkspaceMutation,
} from '~/generated/graphql';
export const RolesDefaultRole = ({ roles }: { roles: Role[] }) => {
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const defaultRole = currentWorkspace?.defaultRole;
const updateDefaultRole = (
defaultRoleId: string | null,
currentWorkspace: CurrentWorkspace,
) => {
updateWorkspace({
variables: {
input: {
defaultRoleId: isDefined(defaultRoleId) ? defaultRoleId : null,
},
},
onCompleted: (data: UpdateWorkspaceMutation) => {
const defaultRole = data.updateWorkspace.defaultRole;
setCurrentWorkspace({
...currentWorkspace,
defaultRole: isDefined(defaultRole) ? defaultRole : null,
});
},
});
};
if (!currentWorkspace) {
return null;
}
return (
<Section>
<H2Title
title={t`Options`}
description={t`Adjust the role-related settings`}
/>
<Card rounded>
<SettingsOptionCardContentSelect
Icon={IconUserPin}
title="Default Role"
description={t`Set a default role for this workspace`}
>
<Select
selectSizeVariant="small"
withSearchInput
dropdownId="default-role-select"
options={roles.map((role) => ({
label: role.label,
value: role.id,
}))}
value={defaultRole?.id ?? ''}
onChange={(value) =>
updateDefaultRole(value as string, currentWorkspace)
}
/>
</SettingsOptionCardContentSelect>
</Card>
</Section>
);
};

View File

@ -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 { Trans } from '@lingui/react/macro';
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const RolesTableHeader = () => {
return (
<StyledTableHeaderRow>
<TableRow>
<TableHeader>
<Trans>Name</Trans>
</TableHeader>
<TableHeader align={'right'}>
<Trans>Assigned to</Trans>
</TableHeader>
<TableHeader align={'right'}></TableHeader>
</TableRow>
</StyledTableHeaderRow>
);
};

View File

@ -0,0 +1,117 @@
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React from 'react';
import {
AppTooltip,
Avatar,
IconChevronRight,
IconLock,
IconUser,
TooltipDelay,
} from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledAvatarContainer = styled.div`
border: 0px;
`;
const StyledAssignedText = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
`;
const StyledNameCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAssignedCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledAvatarGroup = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
> * {
margin-left: -5px;
&:first-of-type {
margin-left: 0;
}
}
`;
const StyledTableRow = styled(TableRow)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
cursor: pointer;
}
`;
export const RolesTableRow = ({ role }: { role: Role }) => {
const theme = useTheme();
const navigateSettings = useNavigateSettings();
const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId });
};
return (
<StyledTableRow key={role.id} onClick={() => handleRoleClick(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) => (
<React.Fragment key={workspaceMember.id}>
<StyledAvatarContainer id={`avatar-${workspaceMember.id}`}>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
placeholderColorSeed={workspaceMember.id}
placeholder={workspaceMember.name.firstName ?? ''}
type="rounded"
size="md"
/>
</StyledAvatarContainer>
<AppTooltip
anchorSelect={`#avatar-${workspaceMember.id}`}
content={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
noArrow
place="top"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</React.Fragment>
))}
</StyledAvatarGroup>
<StyledAssignedText>
{role.workspaceMembers.length}
</StyledAssignedText>
</StyledAssignedCell>
</TableCell>
<TableCell align={'right'}>
<StyledIconChevronRight size={theme.icon.size.md} />
</TableCell>
</StyledTableRow>
);
};

View File

@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddDefaultRoleToWorkspace1740390801418
implements MigrationInterface
{
name = 'AddDefaultRoleToWorkspace1740390801418';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "defaultRoleId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "defaultRoleId"`,
);
}
}

View File

@ -15,9 +15,9 @@ import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.ser
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
@ -46,6 +46,7 @@ import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.mod
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
@ -91,6 +92,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
GuardRedirectModule,
HealthModule,
PermissionsModule,
UserRoleModule,
],
controllers: [
GoogleAuthController,

View File

@ -6,6 +6,10 @@ import { WorkspaceActivationStatus } from 'twenty-shared';
import { Repository } from 'typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import {
AuthProviderWithPasswordType,
@ -14,18 +18,16 @@ import {
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
jest.mock('src/utils/image', () => {
return {
@ -42,6 +44,8 @@ describe('SignInUpService', () => {
let userWorkspaceService: UserWorkspaceService;
let environmentService: EnvironmentService;
let domainManagerService: DomainManagerService;
let userRoleService: UserRoleService;
let featureFlagService: FeatureFlagService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -117,6 +121,18 @@ describe('SignInUpService', () => {
generateSubdomain: jest.fn(),
},
},
{
provide: UserRoleService,
useValue: {
assignRoleToUserWorkspace: jest.fn(),
},
},
{
provide: FeatureFlagService,
useValue: {
isFeatureEnabled: jest.fn(),
},
},
],
}).compile();
@ -132,6 +148,8 @@ describe('SignInUpService', () => {
environmentService = module.get<EnvironmentService>(EnvironmentService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
userRoleService = module.get<UserRoleService>(UserRoleService);
featureFlagService = module.get<FeatureFlagService>(FeatureFlagService);
});
it('should handle signInUp with valid personal invitation', async () => {
@ -161,9 +179,10 @@ describe('SignInUpService', () => {
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
.mockResolvedValue(undefined);
jest
.spyOn(userWorkspaceService, 'addUserToWorkspace')
.mockResolvedValue({} as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
const result = await service.signInUp(params);
@ -198,9 +217,10 @@ describe('SignInUpService', () => {
},
};
jest
.spyOn(userWorkspaceService, 'addUserToWorkspace')
.mockResolvedValue({} as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
const result = await service.signInUp(params);
@ -271,9 +291,10 @@ describe('SignInUpService', () => {
};
jest.spyOn(environmentService, 'get').mockReturnValue(false);
jest
.spyOn(userWorkspaceService, 'addUserToWorkspace')
.mockResolvedValue({} as User);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: {} as UserWorkspace,
});
jest
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
.mockResolvedValue({} as UserWorkspace);
@ -312,4 +333,38 @@ describe('SignInUpService', () => {
),
);
});
it('should assign default role when permissions are enabled', async () => {
const params: SignInUpBaseParams &
ExistingUserOrPartialUserWithPicture &
AuthProviderWithPasswordType = {
workspace: {
id: 'workspaceId',
defaultRoleId: 'defaultRoleId',
activationStatus: WorkspaceActivationStatus.ACTIVE,
} as Workspace,
authParams: { provider: 'password', password: 'validPassword' },
userData: {
type: 'existingUser',
existingUser: { email: 'test@example.com' } as User,
},
};
const mockUserWorkspace = { id: 'userWorkspaceId' };
jest.spyOn(featureFlagService, 'isFeatureEnabled').mockResolvedValue(true);
jest.spyOn(userWorkspaceService, 'addUserToWorkspace').mockResolvedValue({
user: {} as User,
userWorkspace: mockUserWorkspace as UserWorkspace,
});
await service.signInUp(params);
expect(params.workspace).toBeDefined();
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({
workspaceId: params.workspace!.id,
userWorkspaceId: mockUserWorkspace.id,
roleId: params.workspace!.defaultRoleId,
});
});
});

View File

@ -31,6 +31,8 @@ import {
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -38,6 +40,7 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
import { getImageBufferFromUrl } from 'src/utils/image';
import { isWorkEmail } from 'src/utils/is-work-email';
@ -58,6 +61,8 @@ export class SignInUpService {
private readonly environmentService: EnvironmentService,
private readonly domainManagerService: DomainManagerService,
private readonly userService: UserService,
private readonly userRoleService: UserRoleService,
private readonly featureFlagService: FeatureFlagService,
) {}
async computeParamsForNewUser(
@ -256,10 +261,11 @@ export class SignInUpService {
)
: params.userData.existingUser;
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
currentUser,
params.workspace,
);
const { user: updatedUser, userWorkspace } =
await this.userWorkspaceService.addUserToWorkspace(
currentUser,
params.workspace,
);
const user = Object.assign(currentUser, updatedUser);
@ -267,6 +273,19 @@ export class SignInUpService {
await this.activateOnboardingForUser(user, params.workspace);
}
const isPermissionsEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
params.workspace.id,
);
if (isPermissionsEnabled && params.workspace.defaultRoleId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId: params.workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: params.workspace.defaultRoleId,
});
}
return user;
}

View File

@ -113,13 +113,13 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
}
async addUserToWorkspace(user: User, workspace: Workspace) {
const userWorkspaceExists = await this.checkUserWorkspaceExists(
let userWorkspace = await this.checkUserWorkspaceExists(
user.id,
workspace.id,
);
if (!userWorkspaceExists) {
await this.create(user.id, workspace.id);
if (!userWorkspace) {
userWorkspace = await this.create(user.id, workspace.id);
await this.createWorkspaceMember(workspace.id, user);
}
@ -129,7 +129,10 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
user.email,
);
return user;
return {
user,
userWorkspace,
};
}
public async getUserCount(workspaceId: string): Promise<number | undefined> {

View File

@ -5,6 +5,7 @@ import {
IsNotIn,
IsOptional,
IsString,
IsUUID,
Matches,
} from 'class-validator';
@ -184,4 +185,9 @@ export class UpdateWorkspaceInput {
@IsBoolean()
@IsOptional()
isPasswordAuthEnabled?: boolean;
@Field({ nullable: true })
@IsUUID()
@IsOptional()
defaultRoleId?: string;
}

View File

@ -21,6 +21,7 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
registerEnumType(WorkspaceActivationStatus, {
name: 'WorkspaceActivationStatus',
@ -151,6 +152,12 @@ export class Workspace {
@Column({ default: false })
isCustomDomainEnabled: boolean;
@Column({ nullable: true, type: 'uuid' })
defaultRoleId: string | null;
@Field(() => RoleDTO, { nullable: true })
defaultRole: RoleDTO | null;
@Field(() => String, { nullable: true })
@Column({ type: 'varchar', nullable: true })
version: string | null;

View File

@ -20,6 +20,7 @@ import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/worksp
import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
@ -53,6 +54,7 @@ import { WorkspaceService } from './services/workspace.service';
TypeORMModule,
PermissionsModule,
WorkspaceCacheStorageModule,
RoleModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,

View File

@ -48,6 +48,8 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { GraphqlValidationExceptionFilter } from 'src/filters/graphql-validation-exception.filter';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@ -70,6 +72,7 @@ export class WorkspaceResolver {
private readonly fileService: FileService,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly featureFlagService: FeatureFlagService,
private readonly roleService: RoleService,
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
) {}
@ -191,6 +194,20 @@ export class WorkspaceResolver {
}
}
@ResolveField(() => RoleDTO, { nullable: true })
async defaultRole(@Parent() workspace: Workspace): Promise<RoleDTO | null> {
if (!workspace.defaultRoleId) {
return null;
}
const role = await this.roleService.getRoleById(
workspace.defaultRoleId,
workspace.id,
);
return role;
}
@ResolveField(() => BillingSubscription, { nullable: true })
async currentBillingSubscription(
@Parent() workspace: Workspace,

View File

@ -34,23 +34,7 @@ export class RoleResolver {
@Query(() => [RoleDTO])
async getRoles(@AuthWorkspace() workspace: Workspace): Promise<RoleDTO[]> {
const roles = await this.roleService.getWorkspaceRoles(workspace.id);
return roles.map((role) => ({
id: role.id,
label: role.label,
description: role.description,
workspaceId: role.workspaceId,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isEditable: role.isEditable,
userWorkspaceRoles: role.userWorkspaceRoles,
canUpdateAllSettings: role.canUpdateAllSettings,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
}));
return this.roleService.getWorkspaceRoles(workspace.id);
}
@Mutation(() => WorkspaceMember)

View File

@ -21,6 +21,19 @@ export class RoleService {
});
}
public async getRoleById(
id: string,
workspaceId: string,
): Promise<RoleEntity | null> {
return this.roleRepository.findOne({
where: {
id,
workspaceId,
},
relations: ['userWorkspaceRoles'],
});
}
public async createAdminRole({
workspaceId,
}: {

View File

@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
@ -29,7 +30,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
WorkspaceHealthModule,
FeatureFlagModule,
PermissionsModule,
TypeOrmModule.forFeature([UserWorkspace], 'core'),
TypeOrmModule.forFeature([UserWorkspace, Workspace], 'core'),
RoleModule,
UserRoleModule,
TypeOrmModule.forFeature(

View File

@ -11,6 +11,7 @@ import {
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -51,6 +52,8 @@ export class WorkspaceManagerService {
private readonly roleService: RoleService,
private readonly userRoleService: UserRoleService,
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
) {}
/**
@ -292,6 +295,14 @@ export class WorkspaceManagerService {
userWorkspaceId: userWorkspace.id,
roleId: adminRole.id,
});
const memberRole = await this.roleService.createMemberRole({
workspaceId,
});
await this.workspaceRepository.update(workspaceId, {
defaultRoleId: memberRole.id,
});
}
private async initPermissionsDev(workspaceId: string) {
@ -336,6 +347,10 @@ export class WorkspaceManagerService {
workspaceId,
});
await this.workspaceRepository.update(workspaceId, {
defaultRoleId: memberRole.id,
});
if (memberUserWorkspaceId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,

View File

@ -12,9 +12,9 @@ export {
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconArrowsVertical,
IconAt,
IconBaselineDensitySmall,
IconBell,
@ -41,8 +41,8 @@ export {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronUp,
IconChevronsRight,
IconChevronUp,
IconCircleDot,
IconCircleOff,
IconCirclePlus,
@ -261,6 +261,7 @@ export {
IconUpload,
IconUser,
IconUserCircle,
IconUserPin,
IconUserPlus,
IconUsers,
IconVariable,

View File

@ -102,7 +102,7 @@ const StyledInput = styled.input<InputProps>`
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
background: ${({ theme, indeterminate, isChecked, disabled }) =>
disabled && isChecked
? theme.color.blue30
? theme.color.blue
: indeterminate || isChecked
? theme.color.blue
: 'transparent'};
@ -114,8 +114,10 @@ const StyledInput = styled.input<InputProps>`
disabled,
}) => {
switch (true) {
case isChecked:
return theme.color.blue;
case disabled:
return isChecked ? theme.color.blue30 : theme.font.color.extraLight;
return theme.background.transparent.medium;
case indeterminate || isChecked:
return theme.color.blue;
case variant === CheckboxVariant.Primary:
@ -150,7 +152,7 @@ const StyledInput = styled.input<InputProps>`
height: var(--size);
left: var(--padding);
position: absolute;
stroke: ${({ theme }) => theme.grayScale.gray0};
stroke: ${({ theme }) => theme.font.color.inverted};
top: var(--padding);
width: var(--size);
}