From 8f07f681d2009551729b3e3cc54f31ee4e726036 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 17 Jun 2025 16:00:31 +0200 Subject: [PATCH] role settings various fixes + update role object level permission design (#12664) Screenshot 2025-06-17 at 12 10 07 Screenshot 2025-06-17 at 12 10 00 Screenshot 2025-06-17 at 12 09 49 Screenshot 2025-06-17 at 12 09 42 Screenshot 2025-06-17 at 12 09 36 --- .../modules/app/components/SettingsRoutes.tsx | 12 ++ .../components/SettingsRoleAssignment.tsx | 8 +- ...RolePermissionsObjectLevelObjectPicker.tsx | 198 ++++++++++++++++++ ...ObjectLevelObjectPickerDropdownContent.tsx | 59 ------ ...tingsRolePermissionsObjectLevelSection.tsx | 90 +++----- ...gsRolePermissionsObjectLevelObjectForm.tsx | 34 +-- .../utils/hasPermissionOverride.ts | 31 +++ .../roles/role/components/SettingsRole.tsx | 19 +- .../src/modules/types/SettingsPath.ts | 1 + .../roles/SettingsRoleAddObjectLevel.tsx | 45 ++++ .../pages/settings/roles/SettingsRoleEdit.tsx | 4 +- .../roles/SettingsRoleObjectLevel.tsx | 11 +- 12 files changed, 350 insertions(+), 162 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPicker.tsx delete mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/utils/hasPermissionOverride.ts create mode 100644 packages/twenty-front/src/pages/settings/roles/SettingsRoleAddObjectLevel.tsx diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5c4cb936a..83a9b601b 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -327,6 +327,14 @@ const SettingsRoleObjectLevel = lazy(() => })), ); +const SettingsRoleAddObjectLevel = lazy(() => + import('~/pages/settings/roles/SettingsRoleAddObjectLevel').then( + (module) => ({ + default: module.SettingsRoleAddObjectLevel, + }), + ), +); + type SettingsRoutesProps = { isFunctionSettingsEnabled?: boolean; isAdminPageEnabled?: boolean; @@ -420,6 +428,10 @@ export const SettingsRoutes = ({ path={SettingsPath.RoleObjectLevel} element={} /> + } + /> theme.spacing(2)}; `; -const StyledTable = styled.div` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; -`; - const StyledSearchInput = styled(TextInput)` input { background: ${({ theme }) => theme.background.transparent.lighter}; @@ -58,6 +54,10 @@ const StyledSearchInput = styled(TextInput)` } `; +const StyledTable = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +`; + const StyledTableRows = styled.div` gap: ${({ theme }) => theme.spacing(0.5)}; padding-bottom: ${({ theme }) => theme.spacing(2)}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPicker.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPicker.tsx new file mode 100644 index 000000000..f2cbfd495 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPicker.tsx @@ -0,0 +1,198 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata'; +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { hasPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/utils/hasPermissionOverride'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { SettingsPath } from '@/types/SettingsPath'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useMemo, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import { H2Title, IconSearch, useIcons } from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; + +const StyledTypeSelectContainer = styled.div` + display: flex; + flex-direction: column; + gap: inherit; + width: 100%; +`; + +const StyledContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + justify-content: flex-start; + flex-wrap: wrap; + width: 100%; +`; + +const StyledCardContainer = styled.div` + cursor: pointer; + display: flex; + position: relative; + width: calc(50% - ${({ theme }) => theme.spacing(1)}); +`; + +const StyledSearchContainer = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledSearchInput = styled(TextInput)` + input { + background: ${({ theme }) => theme.background.transparent.lighter}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + } +`; + +export const SettingsRolePermissionsObjectLevelObjectPicker = ({ + roleId, +}: { + roleId: string; +}) => { + const theme = useTheme(); + const navigate = useNavigateSettings(); + const [searchFilter, setSearchFilter] = useState(''); + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( + settingsDraftRoleFamilyState(roleId), + ); + + const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } = + useFilteredObjectMetadataItems(); + + const { getIcon } = useIcons(); + + const handleSearchChange = (text: string) => { + setSearchFilter(text); + }; + + const handleSelectObjectMetadata = (objectMetadataId: string) => { + setSettingsDraftRole((draftRole) => ({ + ...draftRole, + objectPermissions: [ + ...(draftRole.objectPermissions ?? []).filter( + (permission) => permission.objectMetadataId !== objectMetadataId, + ), + { + objectMetadataId, + canReadObjectRecords: null, + canUpdateObjectRecords: null, + canSoftDeleteObjectRecords: null, + canDestroyObjectRecords: null, + }, + ], + })); + navigate(SettingsPath.RoleObjectLevel, { + roleId, + objectMetadataId, + }); + }; + + const excludedObjectMetadataIds = useMemo( + () => + settingsDraftRole.objectPermissions + ?.filter((objectPermission) => + hasPermissionOverride(objectPermission, settingsDraftRole), + ) + .map((p) => p.objectMetadataId) ?? [], + [settingsDraftRole], + ); + + const filteredObjectMetadataItems = useMemo( + () => + objectMetadataItems.filter( + (objectMetadataItem) => + objectMetadataItem.labelPlural + .toLowerCase() + .includes(searchFilter.toLowerCase()) && + !excludedObjectMetadataIds.includes(objectMetadataItem.id) && + !isWorkflowRelatedObjectMetadata(objectMetadataItem.nameSingular), + ), + [objectMetadataItems, searchFilter, excludedObjectMetadataIds], + ); + + const basicObjects = filteredObjectMetadataItems.filter( + (item) => !item.isCustom, + ); + const customObjects = filteredObjectMetadataItems.filter( + (item) => item.isCustom, + ); + + return ( + +
+ + + +
+ + {basicObjects.length > 0 && ( +
+ + + {basicObjects.map((objectMetadataItem) => { + const Icon = getIcon(objectMetadataItem.icon); + return ( + + handleSelectObjectMetadata(objectMetadataItem.id) + } + > + + } + title={objectMetadataItem.labelPlural} + /> + + ); + })} + +
+ )} + + {customObjects.length > 0 && ( +
+ + + {customObjects.map((objectMetadataItem) => { + const Icon = getIcon(objectMetadataItem.icon); + return ( + + handleSelectObjectMetadata(objectMetadataItem.id) + } + > + + } + title={objectMetadataItem.labelPlural} + /> + + ); + })} + +
+ )} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx deleted file mode 100644 index 27d992992..000000000 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; -import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; -import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { t } from '@lingui/core/macro'; -import { ChangeEvent, useState } from 'react'; -import { useIcons } from 'twenty-ui/display'; -import { MenuItem } from 'twenty-ui/navigation'; - -type SettingsRolePermissionsObjectLevelObjectPickerDropdownContentProps = { - excludedObjectMetadataIds: string[]; - onSelect: (objectMetadataId: string) => void; -}; - -export const SettingsRolePermissionsObjectLevelObjectPickerDropdownContent = ({ - excludedObjectMetadataIds, - onSelect, -}: SettingsRolePermissionsObjectLevelObjectPickerDropdownContentProps) => { - const [searchFilter, setSearchFilter] = useState(''); - - const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } = - useFilteredObjectMetadataItems(); - - const { getIcon } = useIcons(); - - const handleSearchFilterChange = (event: ChangeEvent) => { - setSearchFilter(event.target.value); - }; - - const filteredObjectMetadataItems = objectMetadataItems.filter( - (objectMetadataItem) => - objectMetadataItem.labelSingular - .toLowerCase() - .includes(searchFilter.toLowerCase()) && - !excludedObjectMetadataIds.includes(objectMetadataItem.id), - ); - - return ( - - - - - {filteredObjectMetadataItems.map((objectMetadataItem) => ( - onSelect(objectMetadataItem.id)} - /> - ))} - - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx index e25586446..877a541e6 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx @@ -1,16 +1,16 @@ -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { SettingsRolePermissionsObjectLevelObjectPickerDropdownContent } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent'; +import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata'; import { SettingsRolePermissionsObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader'; import { SettingsRolePermissionsObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow'; +import { hasPermissionOverride } from '@/settings/roles/role-permissions/object-level-permissions/utils/hasPermissionOverride'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { SettingsPath } from '@/types/SettingsPath'; -import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Table } from '@/ui/layout/table/components/Table'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; -import { useRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { H2Title, IconPlus } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; @@ -43,15 +43,20 @@ export const SettingsRolePermissionsObjectLevelSection = ({ roleId, isEditable, }: SettingsRolePermissionsObjectLevelSectionProps) => { - const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( + const settingsDraftRole = useRecoilValue( settingsDraftRoleFamilyState(roleId), ); - const navigate = useNavigateSettings(); + const navigateSettings = useNavigateSettings(); - const objectMetadataItems = useObjectMetadataItems(); + const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } = + useFilteredObjectMetadataItems(); - const objectMetadataMap = objectMetadataItems.objectMetadataItems.reduce( + const filteredObjectMetadataItems = objectMetadataItems.filter( + (item) => !isWorkflowRelatedObjectMetadata(item.nameSingular), + ); + + const objectMetadataMap = filteredObjectMetadataItems.reduce( (acc, item) => { acc[item.id] = item; return acc; @@ -61,39 +66,18 @@ export const SettingsRolePermissionsObjectLevelSection = ({ const filteredObjectPermissions = settingsDraftRole.objectPermissions?.filter( (objectPermission) => - (isDefined(objectPermission.canReadObjectRecords) && - objectPermission.canReadObjectRecords !== - settingsDraftRole.canReadAllObjectRecords) || - (isDefined(objectPermission.canUpdateObjectRecords) && - objectPermission.canUpdateObjectRecords !== - settingsDraftRole.canUpdateAllObjectRecords) || - (isDefined(objectPermission.canSoftDeleteObjectRecords) && - objectPermission.canSoftDeleteObjectRecords !== - settingsDraftRole.canSoftDeleteAllObjectRecords) || - (isDefined(objectPermission.canDestroyObjectRecords) && - objectPermission.canDestroyObjectRecords !== - settingsDraftRole.canDestroyAllObjectRecords), + hasPermissionOverride(objectPermission, settingsDraftRole) && + !isWorkflowRelatedObjectMetadata( + objectMetadataMap[objectPermission.objectMetadataId]?.nameSingular, + ), ); - const handleSelectObjectMetadata = (objectMetadataId: string) => { - setSettingsDraftRole((draftRole) => ({ - ...draftRole, - objectPermissions: [ - ...(draftRole.objectPermissions ?? []).filter( - (permission) => permission.objectMetadataId !== objectMetadataId, - ), - { - objectMetadataId, - canReadObjectRecords: null, - canUpdateObjectRecords: null, - canSoftDeleteObjectRecords: null, - canDestroyObjectRecords: null, - }, - ], - })); - navigate(SettingsPath.RoleObjectLevel, { + const allObjectsHaveSetPermission = + filteredObjectPermissions?.length === filteredObjectMetadataItems.length; + + const handleAddRule = () => { + navigateSettings(SettingsPath.RoleAddObjectLevel, { roleId, - objectMetadataId, }); }; @@ -124,29 +108,13 @@ export const SettingsRolePermissionsObjectLevelSection = ({ - - } - dropdownOffset={{ x: 0, y: 4 }} - dropdownComponents={ - objectPermission.objectMetadataId, - ) ?? [] - } - onSelect={handleSelectObjectMetadata} - /> - } +