diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 6ff693f8a..8eae609b2 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -380,6 +380,7 @@ export type CreateRoleInput = { canUpdateAllSettings?: InputMaybe; description?: InputMaybe; icon?: InputMaybe; + id?: InputMaybe; label: Scalars['String']['input']; }; @@ -515,6 +516,7 @@ export enum EnvironmentVariablesGroup { GoogleAuth = 'GoogleAuth', LLM = 'LLM', Logging = 'Logging', + Metering = 'Metering', MicrosoftAuth = 'MicrosoftAuth', Other = 'Other', RateLimiting = 'RateLimiting', @@ -557,7 +559,6 @@ export type FeatureFlag = { }; export enum FeatureFlagKey { - IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled', diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 772d6384b..7da66f05a 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -316,6 +316,7 @@ export type CreateRoleInput = { canUpdateAllSettings?: InputMaybe; description?: InputMaybe; icon?: InputMaybe; + id?: InputMaybe; label: Scalars['String']; }; @@ -446,6 +447,7 @@ export enum EnvironmentVariablesGroup { GoogleAuth = 'GoogleAuth', LLM = 'LLM', Logging = 'Logging', + Metering = 'Metering', MicrosoftAuth = 'MicrosoftAuth', Other = 'Other', RateLimiting = 'RateLimiting', @@ -488,7 +490,6 @@ export type FeatureFlag = { }; export enum FeatureFlagKey { - IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled', @@ -2594,6 +2595,20 @@ export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', upda export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }; +export type CreateOneRoleMutationVariables = Exact<{ + createRoleInput: CreateRoleInput; +}>; + + +export type CreateOneRoleMutation = { __typename?: 'Mutation', createOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } }; + +export type UpdateOneRoleMutationVariables = Exact<{ + updateRoleInput: UpdateRoleInput; +}>; + + +export type UpdateOneRoleMutation = { __typename?: 'Mutation', updateOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } }; + export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ workspaceMemberId: Scalars['String']; roleId: Scalars['String']; @@ -4605,6 +4620,72 @@ export function useUpdateLabPublicFeatureFlagMutation(baseOptions?: Apollo.Mutat export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType; export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult; export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions; +export const CreateOneRoleDocument = gql` + mutation CreateOneRole($createRoleInput: CreateRoleInput!) { + createOneRole(createRoleInput: $createRoleInput) { + ...RoleFragment + } +} + ${RoleFragmentFragmentDoc}`; +export type CreateOneRoleMutationFn = Apollo.MutationFunction; + +/** + * __useCreateOneRoleMutation__ + * + * To run a mutation, you first call `useCreateOneRoleMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateOneRoleMutation` 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 [createOneRoleMutation, { data, loading, error }] = useCreateOneRoleMutation({ + * variables: { + * createRoleInput: // value for 'createRoleInput' + * }, + * }); + */ +export function useCreateOneRoleMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateOneRoleDocument, options); + } +export type CreateOneRoleMutationHookResult = ReturnType; +export type CreateOneRoleMutationResult = Apollo.MutationResult; +export type CreateOneRoleMutationOptions = Apollo.BaseMutationOptions; +export const UpdateOneRoleDocument = gql` + mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { + updateOneRole(updateRoleInput: $updateRoleInput) { + ...RoleFragment + } +} + ${RoleFragmentFragmentDoc}`; +export type UpdateOneRoleMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateOneRoleMutation__ + * + * To run a mutation, you first call `useUpdateOneRoleMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateOneRoleMutation` 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 [updateOneRoleMutation, { data, loading, error }] = useUpdateOneRoleMutation({ + * variables: { + * updateRoleInput: // value for 'updateRoleInput' + * }, + * }); + */ +export function useUpdateOneRoleMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateOneRoleDocument, options); + } +export type UpdateOneRoleMutationHookResult = ReturnType; +export type UpdateOneRoleMutationResult = Apollo.MutationResult; +export type UpdateOneRoleMutationOptions = Apollo.BaseMutationOptions; export const UpdateWorkspaceMemberRoleDocument = gql` mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String!) { updateWorkspaceMemberRole( diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5cf8aa4a6..ce2751a69 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -301,6 +301,12 @@ const SettingsRoles = lazy(() => })), ); +const SettingsRoleCreate = lazy(() => + import('~/pages/settings/roles/SettingsRoleCreate').then((module) => ({ + default: module.SettingsRoleCreate, + })), +); + const SettingsRoleEdit = lazy(() => import('~/pages/settings/roles/SettingsRoleEdit').then((module) => ({ default: module.SettingsRoleEdit, @@ -392,6 +398,10 @@ export const SettingsRoutes = ({ > } /> } /> + } + /> { const setTokenPair = useSetRecoilState(tokenPairState); diff --git a/packages/twenty-front/src/modules/settings/roles/components/Roles.tsx b/packages/twenty-front/src/modules/settings/roles/components/Roles.tsx deleted file mode 100644 index b8e591257..000000000 --- a/packages/twenty-front/src/modules/settings/roles/components/Roles.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Table } from '@/ui/layout/table/components/Table'; -import styled from '@emotion/styled'; -import { t } from '@lingui/core/macro'; - -import { RolesTableHeader } from '@/settings/roles/components/RolesTableHeader'; -import { RolesTableRow } from '@/settings/roles/components/RolesTableRow'; -import { Button, H2Title, IconPlus, Section } from 'twenty-ui'; -import { Role } from '~/generated-metadata/graphql'; - -const StyledCreateRoleSection = styled(Section)` - border-top: 1px solid ${({ theme }) => theme.border.color.light}; - display: flex; - justify-content: flex-end; - padding-top: ${({ theme }) => theme.spacing(2)}; - padding-bottom: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledTableRows = styled.div` - padding-bottom: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(2)}; -`; - -export const Roles = ({ roles }: { roles: Role[] }) => { - return ( -
- - - - - {roles.map((role) => ( - - ))} - -
- -
- ); -}; diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx new file mode 100644 index 000000000..64b3c9778 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesContainer.tsx @@ -0,0 +1,44 @@ +import { H3Title } from 'twenty-ui'; + +import { SettingsPath } from '@/types/SettingsPath'; + +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; + +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SettingsRoleDefaultRole } from '@/settings/roles/components/SettingsRolesDefaultRole'; + +import { SettingsRolesList } from '@/settings/roles/components/SettingsRolesList'; +import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector'; +import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useRecoilValue } from 'recoil'; + +export const SettingsRolesContainer = () => { + const { t } = useLingui(); + + const settingsAllRoles = useRecoilValue(settingsAllRolesSelector); + const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState); + + if (settingsRolesIsLoading) { + return null; + } + + return ( + } + links={[ + { + children: Workspace, + href: getSettingsPath(SettingsPath.Workspace), + }, + { children: Roles }, + ]} + > + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/components/RolesDefaultRole.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx similarity index 96% rename from packages/twenty-front/src/modules/settings/roles/components/RolesDefaultRole.tsx rename to packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx index c22974a5e..6c76bb57b 100644 --- a/packages/twenty-front/src/modules/settings/roles/components/RolesDefaultRole.tsx +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesDefaultRole.tsx @@ -6,15 +6,15 @@ import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsO import { Select } from '@/ui/input/components/Select'; import { t } from '@lingui/core/macro'; import { useRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { Card, H2Title, IconUserPin, Section } from 'twenty-ui'; import { Role, UpdateWorkspaceMutation, useUpdateWorkspaceMutation, } from '~/generated/graphql'; -import { isDefined } from 'twenty-shared/utils'; -export const RolesDefaultRole = ({ roles }: { roles: Role[] }) => { +export const SettingsRoleDefaultRole = ({ roles }: { roles: Role[] }) => { const [updateWorkspace] = useUpdateWorkspaceMutation(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx new file mode 100644 index 000000000..e6b7fb461 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesList.tsx @@ -0,0 +1,72 @@ +import { Table } from '@/ui/layout/table/components/Table'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; + +import { SettingsRolesTableHeader } from '@/settings/roles/components/SettingsRolesTableHeader'; +import { SettingsRolesTableRow } from '@/settings/roles/components/SettingsRolesTableRow'; +import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector'; +import { SettingsPath } from '@/types/SettingsPath'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useRecoilValue } from 'recoil'; +import { Button, H2Title, IconPlus, Section } from 'twenty-ui'; +import { FeatureFlagKey } from '~/generated/graphql'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; + +const StyledCreateRoleSection = styled(Section)` + border-top: 1px solid ${({ theme }) => theme.border.color.light}; + display: flex; + justify-content: flex-end; + padding-top: ${({ theme }) => theme.spacing(2)}; + padding-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTableRows = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledNoRoles = styled(TableCell)` + color: ${({ theme }) => theme.font.color.tertiary}; +`; + +export const SettingsRolesList = () => { + const navigateSettings = useNavigateSettings(); + const isPermissionsV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsPermissionsV2Enabled, + ); + + const settingsAllRoles = useRecoilValue(settingsAllRolesSelector); + + return ( +
+ + + + + {settingsAllRoles.length === 0 ? ( + {t`No roles found`} + ) : ( + settingsAllRoles.map((role) => ( + + )) + )} + +
+ +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx new file mode 100644 index 000000000..9d3a1fb16 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx @@ -0,0 +1,43 @@ +import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; +import { settingsRoleIdsState } from '@/settings/roles/states/settingsRoleIdsState'; +import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState'; +import { useEffect } from 'react'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { Role, useGetRolesQuery } from '~/generated/graphql'; + +export const SettingsRolesQueryEffect = () => { + const { data, loading } = useGetRolesQuery({ + fetchPolicy: 'network-only', + }); + + const setSettingsRolesIsLoading = useSetRecoilState( + settingsRolesIsLoadingState, + ); + + const populateRoles = useRecoilCallback( + ({ set }) => + (roles: Role[]) => { + const roleIds = roles.map((role) => role.id); + set(settingsRoleIdsState, roleIds); + roles.forEach((role) => { + set(settingsPersistedRoleFamilyState(role.id), role); + }); + }, + [], + ); + + useEffect(() => { + setSettingsRolesIsLoading(loading); + if (!loading) { + const roles = data?.getRoles; + if (!isDefined(roles)) { + return; + } + + populateRoles(roles); + } + }, [data, loading, populateRoles, setSettingsRolesIsLoading]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/settings/roles/components/RolesTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesTableHeader.tsx similarity index 92% rename from packages/twenty-front/src/modules/settings/roles/components/RolesTableHeader.tsx rename to packages/twenty-front/src/modules/settings/roles/components/SettingsRolesTableHeader.tsx index 1a56cc4d7..b54d3fb4e 100644 --- a/packages/twenty-front/src/modules/settings/roles/components/RolesTableHeader.tsx +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesTableHeader.tsx @@ -3,7 +3,7 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { Trans } from '@lingui/react/macro'; -export const RolesTableHeader = () => { +export const SettingsRolesTableHeader = () => { return ( diff --git a/packages/twenty-front/src/modules/settings/roles/components/RolesTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesTableRow.tsx similarity index 98% rename from packages/twenty-front/src/modules/settings/roles/components/RolesTableRow.tsx rename to packages/twenty-front/src/modules/settings/roles/components/SettingsRolesTableRow.tsx index 7519d36b4..655067956 100644 --- a/packages/twenty-front/src/modules/settings/roles/components/RolesTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesTableRow.tsx @@ -52,7 +52,7 @@ const StyledTableRow = styled(TableRow)` } `; -export const RolesTableRow = ({ role }: { role: Role }) => { +export const SettingsRolesTableRow = ({ role }: { role: Role }) => { const theme = useTheme(); const navigateSettings = useNavigateSettings(); diff --git a/packages/twenty-front/src/modules/settings/roles/components/__stories__/Roles.stories.tsx b/packages/twenty-front/src/modules/settings/roles/components/__stories__/Roles.stories.tsx deleted file mode 100644 index ec49731c4..000000000 --- a/packages/twenty-front/src/modules/settings/roles/components/__stories__/Roles.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator, RouterDecorator } from 'twenty-ui'; - -import { Roles } from '@/settings/roles/components/Roles'; -import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; -import { getRolesMock } from '~/testing/mock-data/roles'; - -const meta: Meta = { - title: 'Modules/Settings/Roles/Roles', - component: Roles, - decorators: [ComponentDecorator, I18nFrontDecorator, RouterDecorator], - parameters: { - maxWidth: 800, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - roles: getRolesMock(), - }, -}; diff --git a/packages/twenty-front/src/modules/settings/roles/components/__stories__/RolesDefaultRole.stories.tsx b/packages/twenty-front/src/modules/settings/roles/components/__stories__/RolesDefaultRole.stories.tsx deleted file mode 100644 index 5afc1b9ca..000000000 --- a/packages/twenty-front/src/modules/settings/roles/components/__stories__/RolesDefaultRole.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { RolesDefaultRole } from '@/settings/roles/components/RolesDefaultRole'; -import { Meta, StoryObj } from '@storybook/react'; -import { RecoilRoot } from 'recoil'; -import { ComponentDecorator } from 'twenty-ui'; -import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; -import { getRolesMock } from '~/testing/mock-data/roles'; -import { mockCurrentWorkspace } from '~/testing/mock-data/users'; - -const rolesMock = getRolesMock(); - -const RolesDefaultRoleWrapper = () => { - return ( - { - snapshot.set(currentWorkspaceState, { - ...mockCurrentWorkspace, - defaultRole: rolesMock[1], - }); - }} - > - - - ); -}; - -const meta: Meta = { - title: 'Modules/Settings/Roles/RolesDefaultRole', - component: RolesDefaultRoleWrapper, - decorators: [ComponentDecorator, I18nFrontDecorator], - parameters: { - maxWidth: 800, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - roles: rolesMock, - }, -}; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/createRoleMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/createRoleMutation.ts new file mode 100644 index 000000000..cdc8d51e3 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/createRoleMutation.ts @@ -0,0 +1,11 @@ +import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; +import { gql } from '@apollo/client'; + +export const CREATE_ROLE = gql` + ${ROLE_FRAGMENT} + mutation CreateOneRole($createRoleInput: CreateRoleInput!) { + createOneRole(createRoleInput: $createRoleInput) { + ...RoleFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateRoleMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateRoleMutation.ts new file mode 100644 index 000000000..50c6d33f0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateRoleMutation.ts @@ -0,0 +1,11 @@ +import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; +import { gql } from '@apollo/client'; + +export const UPDATE_ROLE = gql` + ${ROLE_FRAGMENT} + mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) { + updateOneRole(updateRoleInput: $updateRoleInput) { + ...RoleFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/hooks/useUpdateWorkspaceMemberRole.ts b/packages/twenty-front/src/modules/settings/roles/hooks/useUpdateWorkspaceMemberRole.ts new file mode 100644 index 000000000..7e07f8426 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/hooks/useUpdateWorkspaceMemberRole.ts @@ -0,0 +1,105 @@ +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { + useUpdateWorkspaceMemberRoleMutation, + WorkspaceMember, +} from '~/generated/graphql'; + +type AddWorkspaceMemberToRoleAndUpdateStateParams = { + workspaceMemberId: string; +}; + +type UpdateWorkspaceMemberRoleDraftStateParams = { + workspaceMember: WorkspaceMember; +}; + +type AddWorkspaceMembersToRoleParams = { + roleId: string; + workspaceMemberIds: string[]; +}; + +export const useUpdateWorkspaceMemberRole = (roleId: string) => { + const setSettingsPersistedRole = useSetRecoilState( + settingsPersistedRoleFamilyState(roleId), + ); + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( + settingsDraftRoleFamilyState(roleId), + ); + + const [updateWorkspaceMemberRoleMutation] = + useUpdateWorkspaceMemberRoleMutation(); + + const updateWorkspaceMemberRoleDraftState = ({ + workspaceMember, + }: UpdateWorkspaceMemberRoleDraftStateParams) => { + setSettingsDraftRole({ + ...settingsDraftRole, + workspaceMembers: [ + ...settingsDraftRole.workspaceMembers, + { + id: workspaceMember.id, + name: workspaceMember.name, + colorScheme: workspaceMember.colorScheme, + userEmail: workspaceMember.userEmail, + }, + ], + }); + }; + + const addWorkspaceMemberToRoleAndUpdateState = async ({ + workspaceMemberId, + }: AddWorkspaceMemberToRoleAndUpdateStateParams) => { + const { data } = await updateWorkspaceMemberRoleMutation({ + variables: { + workspaceMemberId, + roleId, + }, + }); + + if (data?.updateWorkspaceMemberRole !== undefined) { + const updatedWorkspaceMember = data.updateWorkspaceMemberRole; + const updatedWorkspaceMembers = [ + ...settingsDraftRole.workspaceMembers, + { + id: updatedWorkspaceMember.id, + name: updatedWorkspaceMember.name, + colorScheme: updatedWorkspaceMember.colorScheme, + userEmail: updatedWorkspaceMember.userEmail, + }, + ]; + + const updatedRole = { + ...settingsDraftRole, + workspaceMembers: updatedWorkspaceMembers, + }; + + setSettingsPersistedRole(updatedRole); + setSettingsDraftRole(updatedRole); + } + + return data?.updateWorkspaceMemberRole; + }; + + const addWorkspaceMembersToRole = async ({ + roleId, + workspaceMemberIds, + }: AddWorkspaceMembersToRoleParams) => { + await Promise.all( + workspaceMemberIds.map((workspaceMemberId) => + updateWorkspaceMemberRoleMutation({ + variables: { + roleId, + workspaceMemberId, + }, + }), + ), + ); + }; + + return { + addWorkspaceMemberToRoleAndUpdateState, + updateWorkspaceMemberRoleDraftState, + addWorkspaceMembersToRole, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignment.tsx similarity index 72% rename from packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx rename to packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignment.tsx index 514967714..e30636ad7 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignment.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignment.tsx @@ -1,8 +1,12 @@ import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { RoleAssignmentTableHeader } from '@/settings/roles/role-assignment/components/RoleAssignmentTableHeader'; -import { RoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown'; -import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; +import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole'; +import { SettingsRoleAssignmentConfirmationModal } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModal'; +import { SettingsRoleAssignmentTableHeader } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader'; +import { SettingsRoleAssignmentWorkspaceMemberPickerDropdown } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown'; +import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember'; +import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { SettingsPath } from '@/types/SettingsPath'; import { TextInput } from '@/ui/input/components/TextInput'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -26,14 +30,8 @@ import { SearchRecord, WorkspaceMember, } from '~/generated-metadata/graphql'; -import { - GetRolesDocument, - useGetRolesQuery, - useUpdateWorkspaceMemberRoleMutation, -} from '~/generated/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal'; -import { RoleAssignmentTableRow } from './RoleAssignmentTableRow'; +import { SettingsRoleAssignmentTableRow } from './SettingsRoleAssignmentTableRow'; const StyledAssignToMemberContainer = styled.div` display: flex; @@ -67,43 +65,51 @@ const StyledNoMembers = styled(TableCell)` color: ${({ theme }) => theme.font.color.tertiary}; `; -type RoleAssignmentProps = { - role: Pick & { - workspaceMembers: Array; - }; +type SettingsRoleAssignmentProps = { + roleId: string; + isCreateMode?: boolean; }; -export const RoleAssignment = ({ role }: RoleAssignmentProps) => { +export const SettingsRoleAssignment = ({ + roleId, + isCreateMode, +}: SettingsRoleAssignmentProps) => { + const settingsDraftRole = useRecoilValue( + settingsDraftRoleFamilyState(roleId), + ); + const navigateSettings = useNavigateSettings(); - const [updateWorkspaceMemberRole] = useUpdateWorkspaceMemberRoleMutation({ - refetchQueries: [GetRolesDocument], - }); + const { + addWorkspaceMemberToRoleAndUpdateState, + updateWorkspaceMemberRoleDraftState, + } = useUpdateWorkspaceMemberRole(roleId); const [confirmationModalIsOpen, setConfirmationModalIsOpen] = useState(false); const [selectedWorkspaceMember, setSelectedWorkspaceMember] = - useState( + useState( null, ); - const { data: rolesData } = useGetRolesQuery(); const { closeDropdown } = useDropdown('role-member-select'); const [searchFilter, setSearchFilter] = useState(''); const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const settingsAllRoles = useRecoilValue(settingsAllRolesSelector); + const workspaceMemberRoleMap = new Map< string, { id: string; label: string } >(); - rolesData?.getRoles?.forEach((role) => { - role.workspaceMembers.forEach((member) => { + settingsAllRoles.forEach((role: Role) => { + role.workspaceMembers.forEach((member: WorkspaceMember) => { workspaceMemberRoleMap.set(member.id, { id: role.id, label: role.label }); }); }); const filteredWorkspaceMembers = !searchFilter - ? role.workspaceMembers - : role.workspaceMembers.filter((member) => { + ? settingsDraftRole.workspaceMembers + : settingsDraftRole.workspaceMembers.filter((member) => { const searchTerm = searchFilter.toLowerCase(); const firstName = member.name.firstName?.toLowerCase() || ''; const lastName = member.name.lastName?.toLowerCase() || ''; @@ -116,7 +122,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { ); }); - const assignedWorkspaceMemberIds = role.workspaceMembers.map( + const assignedWorkspaceMemberIds = settingsDraftRole.workspaceMembers.map( (workspaceMember) => workspaceMember.id, ); @@ -153,12 +159,28 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { const handleConfirm = async () => { if (!selectedWorkspaceMember || !confirmationModalIsOpen) return; - await updateWorkspaceMemberRole({ - variables: { + if (!isCreateMode) { + await addWorkspaceMemberToRoleAndUpdateState({ workspaceMemberId: selectedWorkspaceMember.id, - roleId: role.id, - }, - }); + }); + } else { + const workspaceMember = currentWorkspaceMembers.find( + (member) => member.id === selectedWorkspaceMember.id, + ); + + if (!workspaceMember) { + throw new Error('Workspace member not found'); + } + + updateWorkspaceMemberRoleDraftState({ + workspaceMember: { + id: workspaceMember.id, + name: workspaceMember.name, + colorScheme: '', + userEmail: '', + }, + }); + } handleModalClose(); }; @@ -190,11 +212,11 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { /> - + {filteredWorkspaceMembers.length > 0 ? ( filteredWorkspaceMembers.map((workspaceMember) => ( - @@ -233,7 +255,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { } dropdownComponents={ - { {confirmationModalIsOpen && selectedWorkspaceMember && ( - void; onConfirm: () => void; onRoleClick: (roleId: string) => void; }; -export const RoleAssignmentConfirmationModal = ({ +export const SettingsRoleAssignmentConfirmationModal = ({ selectedWorkspaceMember, isOpen, onClose, onConfirm, onRoleClick, -}: RoleAssignmentConfirmationModalProps) => { +}: SettingsRoleAssignmentConfirmationModalProps) => { const workspaceMemberName = selectedWorkspaceMember.name; const title = t`Assign ${workspaceMemberName}?`; @@ -28,7 +28,7 @@ export const RoleAssignmentConfirmationModal = ({ setIsOpen={onClose} title={title} subtitle={ - diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentConfirmationModalSubtitle.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModalSubtitle.tsx similarity index 71% rename from packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentConfirmationModalSubtitle.tsx rename to packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModalSubtitle.tsx index cb20ec19b..e78c439b6 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentConfirmationModalSubtitle.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModalSubtitle.tsx @@ -1,5 +1,5 @@ import { SettingsCard } from '@/settings/components/SettingsCard'; -import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; +import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { Avatar } from 'twenty-ui'; @@ -8,15 +8,15 @@ const StyledSettingsCardContainer = styled.div` margin-top: ${({ theme }) => theme.spacing(6)}; `; -type RoleAssignmentConfirmationModalSubtitleProps = { - selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember; +type SettingsRoleAssignmentConfirmationModalSubtitleProps = { + selectedWorkspaceMember: SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember; onRoleClick: (roleId: string) => void; }; -export const RoleAssignmentConfirmationModalSubtitle = ({ +export const SettingsRoleAssignmentConfirmationModalSubtitle = ({ selectedWorkspaceMember, onRoleClick, -}: RoleAssignmentConfirmationModalSubtitleProps) => { +}: SettingsRoleAssignmentConfirmationModalSubtitleProps) => { const workspaceMemberName = selectedWorkspaceMember.name; return ( diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader.tsx similarity index 84% rename from packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentTableHeader.tsx rename to packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader.tsx index 21c2b7129..04983dc01 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentTableHeader.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader.tsx @@ -2,7 +2,7 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { t } from '@lingui/core/macro'; -export const RoleAssignmentTableHeader = () => ( +export const SettingsRoleAssignmentTableHeader = () => ( {t`Name`} {t`Email`} diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentTableRow.tsx similarity index 92% rename from packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentTableRow.tsx rename to packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentTableRow.tsx index 51393b83a..295486294 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentTableRow.tsx @@ -28,13 +28,13 @@ const StyledTableCell = styled(TableCell)` overflow: hidden; `; -type RoleAssignmentTableRowProps = { +type SettingsRoleAssignmentTableRowProps = { workspaceMember: WorkspaceMember; }; -export const RoleAssignmentTableRow = ({ +export const SettingsRoleAssignmentTableRow = ({ workspaceMember, -}: RoleAssignmentTableRowProps) => { +}: SettingsRoleAssignmentTableRowProps) => { return ( diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown.tsx similarity index 81% rename from packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx rename to packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown.tsx index e6df5cb8b..a74e05ef2 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdown.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdown.tsx @@ -1,6 +1,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; -import { RoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/RoleAssignmentWorkspaceMemberPickerDropdownContent'; +import { SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; @@ -9,15 +9,15 @@ import { useLingui } from '@lingui/react/macro'; import { ChangeEvent, useState } from 'react'; import { SearchRecord } from '~/generated-metadata/graphql'; -type RoleAssignmentWorkspaceMemberPickerDropdownProps = { +type SettingsRoleAssignmentWorkspaceMemberPickerDropdownProps = { excludedWorkspaceMemberIds: string[]; onSelect: (workspaceMemberSearchRecord: SearchRecord) => void; }; -export const RoleAssignmentWorkspaceMemberPickerDropdown = ({ +export const SettingsRoleAssignmentWorkspaceMemberPickerDropdown = ({ excludedWorkspaceMemberIds, onSelect, -}: RoleAssignmentWorkspaceMemberPickerDropdownProps) => { +}: SettingsRoleAssignmentWorkspaceMemberPickerDropdownProps) => { const [searchFilter, setSearchFilter] = useState(''); const { loading, searchRecords: workspaceMembers } = @@ -46,7 +46,7 @@ export const RoleAssignmentWorkspaceMemberPickerDropdown = ({ /> - void; }; -export const RoleAssignmentWorkspaceMemberPickerDropdownContent = ({ +export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({ loading, searchFilter, filteredWorkspaceMembers, onSelect, -}: RoleAssignmentWorkspaceMemberPickerDropdownContentProps) => { +}: SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps) => { if (loading) { return null; } diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/components/stories/SettingsRoleAssignment.stories.tsx b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/stories/SettingsRoleAssignment.stories.tsx new file mode 100644 index 000000000..6f9e40e9c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/components/stories/SettingsRoleAssignment.stories.tsx @@ -0,0 +1,46 @@ +import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/components/SettingsRoleAssignment'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { Meta, StoryObj } from '@storybook/react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { ComponentDecorator, RouterDecorator } from 'twenty-ui'; +import { PENDING_ROLE_ID } from '~/pages/settings/roles/SettingsRoleCreate'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; +import { getRolesMock } from '~/testing/mock-data/roles'; + +const SettingsRoleAssignmentWrapper = ( + args: React.ComponentProps, +) => { + const setDraftRole = useSetRecoilState( + settingsDraftRoleFamilyState(args.roleId), + ); + + const role = getRolesMock().find((role) => role.id === args.roleId); + + if (isDefined(role)) { + setDraftRole(role); + } + + return ; +}; + +const meta: Meta = { + title: 'Modules/Settings/Roles/RoleAssignment/SettingsRoleAssignment', + component: SettingsRoleAssignmentWrapper, + decorators: [RouterDecorator, ComponentDecorator, I18nFrontDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + roleId: '1', + }, +}; + +export const PendingRole: Story = { + args: { + roleId: PENDING_ROLE_ID, + }, +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember.ts b/packages/twenty-front/src/modules/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember.ts deleted file mode 100644 index 29cfdbdde..000000000 --- a/packages/twenty-front/src/modules/settings/roles/role-assignment/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type RoleAssignmentConfirmationModalSelectedWorkspaceMember = { - id: string; - name: string; - role?: { id: string; label: string }; - avatarUrl?: string | null; -}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember.ts b/packages/twenty-front/src/modules/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember.ts new file mode 100644 index 000000000..8e4e4dda7 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember.ts @@ -0,0 +1,8 @@ +export type SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember = { + id: string; + name: string; + role?: { id: string; label: string }; + avatarUrl?: string | null; + colorScheme?: string; + userEmail?: string; +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx deleted file mode 100644 index b5cdead9e..000000000 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { RolePermissionsObjectsTableHeader } from '@/settings/roles/role-permissions/components/RolePermissionsObjectsTableHeader'; -import { RolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/components/RolePermissionsSettingsTableHeader'; -import { RolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/components/RolePermissionsSettingsTableRow'; -import { RolePermissionsObjectPermission } from '@/settings/roles/types/RolePermissionsObjectPermission'; -import { RolePermissionsSettingPermission } from '@/settings/roles/types/RolePermissionsSettingPermission'; -import styled from '@emotion/styled'; -import { t } from '@lingui/core/macro'; -import { - H2Title, - IconCode, - IconEye, - IconHierarchy, - IconKey, - IconLockOpen, - IconPencil, - IconServer, - IconSettings, - IconTrash, - IconTrashX, - IconUsers, - Section, -} from 'twenty-ui'; -import { Role } from '~/generated-metadata/graphql'; -import { SettingPermissionType } from '~/generated/graphql'; -import { RolePermissionsObjectsTableRow } from './RolePermissionsObjectsTableRow'; - -const StyledRolePermissionsContainer = styled.div` - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(8)}; -`; - -const StyledTable = styled.div` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; -`; - -const StyledTableRows = styled.div` - padding-bottom: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(2)}; -`; - -type RolePermissionsProps = { - role: Pick< - Role, - | 'id' - | 'canUpdateAllSettings' - | 'canReadAllObjectRecords' - | 'canUpdateAllObjectRecords' - | 'canSoftDeleteAllObjectRecords' - | 'canDestroyAllObjectRecords' - >; -}; - -export const RolePermissions = ({ role }: RolePermissionsProps) => { - const objectPermissionsConfig: RolePermissionsObjectPermission[] = [ - { - key: 'seeRecords', - label: 'See Records on All Objects', - Icon: IconEye, - value: role.canReadAllObjectRecords, - }, - { - key: 'editRecords', - label: 'Edit Records on All Objects', - Icon: IconPencil, - value: role.canUpdateAllObjectRecords, - }, - { - key: 'deleteRecords', - label: 'Delete Records on All Objects', - Icon: IconTrash, - value: role.canSoftDeleteAllObjectRecords, - }, - { - key: 'destroyRecords', - label: 'Destroy Records on All Objects', - Icon: IconTrashX, - value: role.canDestroyAllObjectRecords, - }, - ]; - - const settingsPermissionsConfig: RolePermissionsSettingPermission[] = [ - { - key: SettingPermissionType.API_KEYS_AND_WEBHOOKS, - name: 'API Keys & Webhooks', - description: 'Manage API keys and webhooks', - value: role.canUpdateAllSettings, - Icon: IconCode, - }, - { - key: SettingPermissionType.WORKSPACE, - name: 'Workspace', - description: 'Set global workspace preferences', - value: role.canUpdateAllSettings, - Icon: IconSettings, - }, - { - key: SettingPermissionType.WORKSPACE_MEMBERS, - name: 'Users', - description: 'Add or remove users', - value: role.canUpdateAllSettings, - Icon: IconUsers, - }, - { - key: SettingPermissionType.ROLES, - name: 'Roles', - description: 'Define user roles and access levels', - value: role.canUpdateAllSettings, - Icon: IconLockOpen, - }, - { - key: SettingPermissionType.DATA_MODEL, - name: 'Data Model', - description: 'Edit CRM data structure and fields', - value: role.canUpdateAllSettings, - Icon: IconHierarchy, - }, - { - key: SettingPermissionType.ADMIN_PANEL, - name: 'Admin Panel', - description: 'Admin settings and system tools', - value: role.canUpdateAllSettings, - Icon: IconServer, - }, - { - key: SettingPermissionType.SECURITY, - name: 'Security', - description: 'Manage security policies', - value: role.canUpdateAllSettings, - Icon: IconKey, - }, - ]; - - return ( - -
- - - permission.value, - )} - /> - - {objectPermissionsConfig.map((permission) => ( - - ))} - - -
-
- - - - - {settingsPermissionsConfig.map((permission) => ( - - ))} - - -
-
- ); -}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsObjectsTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsObjectsTableHeader.tsx deleted file mode 100644 index 97e713d73..000000000 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsObjectsTableHeader.tsx +++ /dev/null @@ -1,35 +0,0 @@ -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'; -import { Checkbox } from 'twenty-ui'; - -const StyledNameHeader = styled(TableHeader)` - flex: 1; -`; - -const StyledActionsHeader = styled(TableHeader)` - align-items: center; - display: flex; - justify-content: flex-end; - padding-right: ${({ theme }) => theme.spacing(4)}; -`; - -type RolePermissionsObjectsTableHeaderProps = { - allPermissions: boolean; -}; - -export const RolePermissionsObjectsTableHeader = ({ - allPermissions, -}: RolePermissionsObjectsTableHeaderProps) => ( - - {t`Name`} - - - - -); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx new file mode 100644 index 000000000..17fcae610 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx @@ -0,0 +1,202 @@ +import { SettingsRolePermissionsObjectsTableHeader } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableHeader'; +import { SettingsRolePermissionsObjectsTableRow } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableRow'; +import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableHeader'; +import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableRow'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission'; +import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/types/SettingsRolePermissionsSettingPermission'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useRecoilState } from 'recoil'; +import { + H2Title, + IconCode, + IconEye, + IconHierarchy, + IconKey, + IconLockOpen, + IconPencil, + IconServer, + IconSettings, + IconTrash, + IconTrashX, + IconUsers, + Section, +} from 'twenty-ui'; +import { SettingPermissionType } from '~/generated-metadata/graphql'; + +const StyledRolePermissionsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledTable = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +`; + +const StyledTableRows = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +type SettingsRolePermissionsProps = { + roleId: string; + isEditable: boolean; +}; + +export const SettingsRolePermissions = ({ + roleId, + isEditable, +}: SettingsRolePermissionsProps) => { + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( + settingsDraftRoleFamilyState(roleId), + ); + + const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [ + { + key: 'seeRecords', + label: t`See Records on All Objects`, + Icon: IconEye, + value: settingsDraftRole.canReadAllObjectRecords, + setValue: (value: boolean) => { + setSettingsDraftRole({ + ...settingsDraftRole, + canReadAllObjectRecords: value, + }); + }, + }, + { + key: 'editRecords', + label: t`Edit Records on All Objects`, + Icon: IconPencil, + value: settingsDraftRole.canUpdateAllObjectRecords, + setValue: (value: boolean) => { + setSettingsDraftRole({ + ...settingsDraftRole, + canUpdateAllObjectRecords: value, + }); + }, + }, + { + key: 'deleteRecords', + label: t`Delete Records on All Objects`, + Icon: IconTrash, + value: settingsDraftRole.canSoftDeleteAllObjectRecords, + setValue: (value: boolean) => { + setSettingsDraftRole({ + ...settingsDraftRole, + canSoftDeleteAllObjectRecords: value, + }); + }, + }, + { + key: 'destroyRecords', + label: t`Destroy Records on All Objects`, + Icon: IconTrashX, + value: settingsDraftRole.canDestroyAllObjectRecords, + setValue: (value: boolean) => { + setSettingsDraftRole({ + ...settingsDraftRole, + canDestroyAllObjectRecords: value, + }); + }, + }, + ]; + + const settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[] = + [ + { + key: SettingPermissionType.API_KEYS_AND_WEBHOOKS, + name: t`API Keys & Webhooks`, + description: t`Manage API keys and webhooks`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconCode, + }, + { + key: SettingPermissionType.WORKSPACE, + name: t`Workspace`, + description: t`Set global workspace preferences`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconSettings, + }, + { + key: SettingPermissionType.WORKSPACE_MEMBERS, + name: t`Users`, + description: t`Add or remove users`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconUsers, + }, + { + key: SettingPermissionType.ROLES, + name: t`Roles`, + description: t`Define user roles and access levels`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconLockOpen, + }, + { + key: SettingPermissionType.DATA_MODEL, + name: t`Data Model`, + description: t`Edit CRM data structure and fields`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconHierarchy, + }, + { + key: SettingPermissionType.ADMIN_PANEL, + name: t`Admin Panel`, + description: t`Admin settings and system tools`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconServer, + }, + { + key: SettingPermissionType.SECURITY, + name: t`Security`, + description: t`Manage security policies`, + value: settingsDraftRole.canUpdateAllSettings, + Icon: IconKey, + }, + ]; + + return ( + +
+ + + + + {objectPermissionsConfig.map((permission) => ( + + ))} + + +
+
+ + + + + {settingsPermissionsConfig.map((permission) => ( + + ))} + + +
+
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableHeader.tsx new file mode 100644 index 000000000..d778b023c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableHeader.tsx @@ -0,0 +1,68 @@ +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission'; +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'; +import { useRecoilState } from 'recoil'; +import { Checkbox } from 'twenty-ui'; + +const StyledNameHeader = styled(TableHeader)` + flex: 1; +`; + +const StyledActionsHeader = styled(TableHeader)` + align-items: center; + display: flex; + justify-content: flex-end; + padding-right: ${({ theme }) => theme.spacing(4)}; +`; + +type SettingsRolePermissionsObjectsTableHeaderProps = { + roleId: string; + objectPermissionsConfig: SettingsRolePermissionsObjectPermission[]; + isEditable: boolean; +}; + +export const SettingsRolePermissionsObjectsTableHeader = ({ + roleId, + objectPermissionsConfig, + isEditable, +}: SettingsRolePermissionsObjectsTableHeaderProps) => { + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( + settingsDraftRoleFamilyState(roleId), + ); + + const allPermissionsEnabled = objectPermissionsConfig.every( + (permission) => permission.value, + ); + + const somePermissionsEnabled = objectPermissionsConfig.some( + (permission) => permission.value, + ); + + return ( + + {t`Name`} + + { + const newValue = !allPermissionsEnabled; + + setSettingsDraftRole({ + ...settingsDraftRole, + canReadAllObjectRecords: newValue, + canUpdateAllObjectRecords: newValue, + canSoftDeleteAllObjectRecords: newValue, + canDestroyAllObjectRecords: newValue, + }); + }} + /> + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsObjectsTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableRow.tsx similarity index 70% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsObjectsTableRow.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableRow.tsx index 68d604b94..d4cbe49d3 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsObjectsTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableRow.tsx @@ -1,6 +1,7 @@ -import { RolePermissionsObjectPermission } from '@/settings/roles/types/RolePermissionsObjectPermission'; +import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission'; 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 { Checkbox } from 'twenty-ui'; @@ -46,25 +47,33 @@ const StyledTableRow = styled(TableRow)` display: flex; `; -type RolePermissionsObjectsTableRowProps = { - permission: RolePermissionsObjectPermission; +type SettingsRolePermissionsObjectsTableRowProps = { + permission: SettingsRolePermissionsObjectPermission; + isEditable: boolean; }; -export const RolePermissionsObjectsTableRow = ({ +export const SettingsRolePermissionsObjectsTableRow = ({ permission, -}: RolePermissionsObjectsTableRowProps) => { + isEditable, +}: SettingsRolePermissionsObjectsTableRowProps) => { + const theme = useTheme(); + return ( - + - + {permission.label} - + permission.setValue(!permission.value)} + disabled={!isEditable} + /> ); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsSettingsTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableHeader.tsx similarity index 71% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsSettingsTableHeader.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableHeader.tsx index d8ead638f..6a7702041 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsSettingsTableHeader.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableHeader.tsx @@ -6,7 +6,10 @@ import { Checkbox } from 'twenty-ui'; const StyledNameHeader = styled(TableHeader)` flex: 1; - padding-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTypeHeader = styled(TableHeader)` + flex: 1; `; const StyledActionsHeader = styled(TableHeader)` @@ -16,22 +19,24 @@ const StyledActionsHeader = styled(TableHeader)` padding-right: ${({ theme }) => theme.spacing(4)}; `; -const StyledTypeHeader = styled(TableHeader)` - flex: 1; -`; - -type RolePermissionsSettingsTableHeaderProps = { +type SettingsRolePermissionsSettingsTableHeaderProps = { allPermissions: boolean; + onToggleAll?: () => void; }; -export const RolePermissionsSettingsTableHeader = ({ +export const SettingsRolePermissionsSettingsTableHeader = ({ allPermissions, -}: RolePermissionsSettingsTableHeaderProps) => ( + onToggleAll, +}: SettingsRolePermissionsSettingsTableHeaderProps) => ( {t`Name`} {t`Description`} - + ); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsSettingsTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableRow.tsx similarity index 80% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsSettingsTableRow.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableRow.tsx index 9c11cf106..a1b5098f3 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissionsSettingsTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableRow.tsx @@ -1,4 +1,4 @@ -import { RolePermissionsSettingPermission } from '@/settings/roles/types/RolePermissionsSettingPermission'; +import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/types/SettingsRolePermissionsSettingPermission'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { useTheme } from '@emotion/react'; @@ -33,13 +33,13 @@ const StyledIconContainer = styled.div` justify-content: center; `; -type RolePermissionsSettingsTableRowProps = { - permission: RolePermissionsSettingPermission; +type SettingsRolePermissionsSettingsTableRowProps = { + permission: SettingsRolePermissionsSettingPermission; }; -export const RolePermissionsSettingsTableRow = ({ +export const SettingsRolePermissionsSettingsTableRow = ({ permission, -}: RolePermissionsSettingsTableRowProps) => { +}: SettingsRolePermissionsSettingsTableRowProps) => { const theme = useTheme(); return ( @@ -47,7 +47,7 @@ export const RolePermissionsSettingsTableRow = ({ diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/stories/SettingsRolePermissions.stories.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/stories/SettingsRolePermissions.stories.tsx new file mode 100644 index 000000000..85abfa6dc --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/stories/SettingsRolePermissions.stories.tsx @@ -0,0 +1,59 @@ +import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { Meta, StoryObj } from '@storybook/react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { ComponentDecorator, RouterDecorator } from 'twenty-ui'; +import { PENDING_ROLE_ID } from '~/pages/settings/roles/SettingsRoleCreate'; +import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; +import { getRolesMock } from '~/testing/mock-data/roles'; + +const SettingsRolePermissionsWrapper = ( + args: React.ComponentProps, +) => { + const setDraftRole = useSetRecoilState( + settingsDraftRoleFamilyState(args.roleId), + ); + + const role = getRolesMock().find((role) => role.id === args.roleId); + + if (isDefined(role)) { + setDraftRole(role); + } + + return ( + + ); +}; + +const meta: Meta = { + title: 'Modules/Settings/Roles/RolePermissions/SettingsRolePermissions', + component: SettingsRolePermissionsWrapper, + decorators: [RouterDecorator, ComponentDecorator, I18nFrontDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + roleId: '1', + isEditable: true, + }, +}; + +export const ReadOnly: Story = { + args: { + roleId: '1', + isEditable: false, + }, +}; + +export const PendingRole: Story = { + args: { + roleId: PENDING_ROLE_ID, + }, +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-settings/components/RoleSettings.tsx b/packages/twenty-front/src/modules/settings/roles/role-settings/components/RoleSettings.tsx deleted file mode 100644 index b5a070366..000000000 --- a/packages/twenty-front/src/modules/settings/roles/role-settings/components/RoleSettings.tsx +++ /dev/null @@ -1,47 +0,0 @@ -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 { Section } from 'twenty-ui'; -import { Role } from '~/generated-metadata/graphql'; - -const StyledInputsContainer = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - width: 100%; - margin-bottom: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledInputContainer = styled.div` - display: flex; - flex-direction: column; -`; - -type RoleSettingsProps = { - role: Pick; -}; - -export const RoleSettings = ({ role }: RoleSettingsProps) => { - return ( -
- - - {}} - /> - - - -