role settings various fixes + update role object level permission design (#12664)
<img width="944" alt="Screenshot 2025-06-17 at 12 10 07" src="https://github.com/user-attachments/assets/abfda0c2-3266-465c-b98e-7bf78660a057" /> <img width="943" alt="Screenshot 2025-06-17 at 12 10 00" src="https://github.com/user-attachments/assets/8fd28479-1f55-4f3a-815c-1195154d3305" /> <img width="667" alt="Screenshot 2025-06-17 at 12 09 49" src="https://github.com/user-attachments/assets/8d444523-4e43-4b59-95bb-45dc5fac5520" /> <img width="632" alt="Screenshot 2025-06-17 at 12 09 42" src="https://github.com/user-attachments/assets/8a1e45bb-7fde-42a6-9f2d-79cbec8121cd" /> <img width="643" alt="Screenshot 2025-06-17 at 12 09 36" src="https://github.com/user-attachments/assets/43f80a92-16e2-4a0e-8a07-2f3e7278ff4a" />
This commit is contained in:
@ -327,6 +327,14 @@ const SettingsRoleObjectLevel = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SettingsRoleAddObjectLevel = lazy(() =>
|
||||||
|
import('~/pages/settings/roles/SettingsRoleAddObjectLevel').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.SettingsRoleAddObjectLevel,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
type SettingsRoutesProps = {
|
type SettingsRoutesProps = {
|
||||||
isFunctionSettingsEnabled?: boolean;
|
isFunctionSettingsEnabled?: boolean;
|
||||||
isAdminPageEnabled?: boolean;
|
isAdminPageEnabled?: boolean;
|
||||||
@ -420,6 +428,10 @@ export const SettingsRoutes = ({
|
|||||||
path={SettingsPath.RoleObjectLevel}
|
path={SettingsPath.RoleObjectLevel}
|
||||||
element={<SettingsRoleObjectLevel />}
|
element={<SettingsRoleObjectLevel />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={SettingsPath.RoleAddObjectLevel}
|
||||||
|
element={<SettingsRoleAddObjectLevel />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -47,10 +47,6 @@ const StyledSearchContainer = styled.div`
|
|||||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTable = styled.div`
|
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledSearchInput = styled(TextInput)`
|
const StyledSearchInput = styled(TextInput)`
|
||||||
input {
|
input {
|
||||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
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`
|
const StyledTableRows = styled.div`
|
||||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<StyledTypeSelectContainer>
|
||||||
|
<Section>
|
||||||
|
<StyledSearchContainer>
|
||||||
|
<StyledSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
placeholder={t`Search an object`}
|
||||||
|
fullWidth
|
||||||
|
LeftIcon={IconSearch}
|
||||||
|
sizeVariant="lg"
|
||||||
|
/>
|
||||||
|
</StyledSearchContainer>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{basicObjects.length > 0 && (
|
||||||
|
<Section>
|
||||||
|
<H2Title title={t`Basics`} description={t`All the basic objects`} />
|
||||||
|
<StyledContainer>
|
||||||
|
{basicObjects.map((objectMetadataItem) => {
|
||||||
|
const Icon = getIcon(objectMetadataItem.icon);
|
||||||
|
return (
|
||||||
|
<StyledCardContainer
|
||||||
|
key={objectMetadataItem.id}
|
||||||
|
onClick={() =>
|
||||||
|
handleSelectObjectMetadata(objectMetadataItem.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsCard
|
||||||
|
Icon={
|
||||||
|
<Icon
|
||||||
|
size={theme.icon.size.xl}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={objectMetadataItem.labelPlural}
|
||||||
|
/>
|
||||||
|
</StyledCardContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledContainer>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{customObjects.length > 0 && (
|
||||||
|
<Section>
|
||||||
|
<H2Title title={t`Custom`} description={t`All your custom objects`} />
|
||||||
|
<StyledContainer>
|
||||||
|
{customObjects.map((objectMetadataItem) => {
|
||||||
|
const Icon = getIcon(objectMetadataItem.icon);
|
||||||
|
return (
|
||||||
|
<StyledCardContainer
|
||||||
|
key={objectMetadataItem.id}
|
||||||
|
onClick={() =>
|
||||||
|
handleSelectObjectMetadata(objectMetadataItem.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsCard
|
||||||
|
key={objectMetadataItem.id}
|
||||||
|
Icon={
|
||||||
|
<Icon
|
||||||
|
size={theme.icon.size.xl}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={objectMetadataItem.labelPlural}
|
||||||
|
/>
|
||||||
|
</StyledCardContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledContainer>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</StyledTypeSelectContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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<HTMLInputElement>) => {
|
|
||||||
setSearchFilter(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredObjectMetadataItems = objectMetadataItems.filter(
|
|
||||||
(objectMetadataItem) =>
|
|
||||||
objectMetadataItem.labelSingular
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchFilter.toLowerCase()) &&
|
|
||||||
!excludedObjectMetadataIds.includes(objectMetadataItem.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownContent>
|
|
||||||
<DropdownMenuSearchInput
|
|
||||||
value={searchFilter}
|
|
||||||
onChange={handleSearchFilterChange}
|
|
||||||
placeholder={t`Search`}
|
|
||||||
/>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
|
||||||
{filteredObjectMetadataItems.map((objectMetadataItem) => (
|
|
||||||
<MenuItem
|
|
||||||
key={objectMetadataItem.id}
|
|
||||||
text={objectMetadataItem.labelSingular}
|
|
||||||
LeftIcon={getIcon(objectMetadataItem.icon)}
|
|
||||||
onClick={() => onSelect(objectMetadataItem.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</DropdownContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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 { 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 { SettingsRolePermissionsObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader';
|
||||||
import { SettingsRolePermissionsObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow';
|
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 { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { H2Title, IconPlus } from 'twenty-ui/display';
|
import { H2Title, IconPlus } from 'twenty-ui/display';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
@ -43,15 +43,20 @@ export const SettingsRolePermissionsObjectLevelSection = ({
|
|||||||
roleId,
|
roleId,
|
||||||
isEditable,
|
isEditable,
|
||||||
}: SettingsRolePermissionsObjectLevelSectionProps) => {
|
}: SettingsRolePermissionsObjectLevelSectionProps) => {
|
||||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
const settingsDraftRole = useRecoilValue(
|
||||||
settingsDraftRoleFamilyState(roleId),
|
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) => {
|
||||||
acc[item.id] = item;
|
acc[item.id] = item;
|
||||||
return acc;
|
return acc;
|
||||||
@ -61,39 +66,18 @@ export const SettingsRolePermissionsObjectLevelSection = ({
|
|||||||
|
|
||||||
const filteredObjectPermissions = settingsDraftRole.objectPermissions?.filter(
|
const filteredObjectPermissions = settingsDraftRole.objectPermissions?.filter(
|
||||||
(objectPermission) =>
|
(objectPermission) =>
|
||||||
(isDefined(objectPermission.canReadObjectRecords) &&
|
hasPermissionOverride(objectPermission, settingsDraftRole) &&
|
||||||
objectPermission.canReadObjectRecords !==
|
!isWorkflowRelatedObjectMetadata(
|
||||||
settingsDraftRole.canReadAllObjectRecords) ||
|
objectMetadataMap[objectPermission.objectMetadataId]?.nameSingular,
|
||||||
(isDefined(objectPermission.canUpdateObjectRecords) &&
|
),
|
||||||
objectPermission.canUpdateObjectRecords !==
|
|
||||||
settingsDraftRole.canUpdateAllObjectRecords) ||
|
|
||||||
(isDefined(objectPermission.canSoftDeleteObjectRecords) &&
|
|
||||||
objectPermission.canSoftDeleteObjectRecords !==
|
|
||||||
settingsDraftRole.canSoftDeleteAllObjectRecords) ||
|
|
||||||
(isDefined(objectPermission.canDestroyObjectRecords) &&
|
|
||||||
objectPermission.canDestroyObjectRecords !==
|
|
||||||
settingsDraftRole.canDestroyAllObjectRecords),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectObjectMetadata = (objectMetadataId: string) => {
|
const allObjectsHaveSetPermission =
|
||||||
setSettingsDraftRole((draftRole) => ({
|
filteredObjectPermissions?.length === filteredObjectMetadataItems.length;
|
||||||
...draftRole,
|
|
||||||
objectPermissions: [
|
const handleAddRule = () => {
|
||||||
...(draftRole.objectPermissions ?? []).filter(
|
navigateSettings(SettingsPath.RoleAddObjectLevel, {
|
||||||
(permission) => permission.objectMetadataId !== objectMetadataId,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
objectMetadataId,
|
|
||||||
canReadObjectRecords: null,
|
|
||||||
canUpdateObjectRecords: null,
|
|
||||||
canSoftDeleteObjectRecords: null,
|
|
||||||
canDestroyObjectRecords: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
navigate(SettingsPath.RoleObjectLevel, {
|
|
||||||
roleId,
|
roleId,
|
||||||
objectMetadataId,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,29 +108,13 @@ export const SettingsRolePermissionsObjectLevelSection = ({
|
|||||||
</StyledTableRows>
|
</StyledTableRows>
|
||||||
</Table>
|
</Table>
|
||||||
<StyledCreateObjectOverrideSection>
|
<StyledCreateObjectOverrideSection>
|
||||||
<Dropdown
|
<Button
|
||||||
dropdownId="role-object-select"
|
Icon={IconPlus}
|
||||||
dropdownHotkeyScope={{ scope: 'roleObject' }}
|
title={t`Add rule`}
|
||||||
clickableComponent={
|
variant="secondary"
|
||||||
<Button
|
size="small"
|
||||||
Icon={IconPlus}
|
disabled={!isEditable || allObjectsHaveSetPermission}
|
||||||
title={t`Add rule`}
|
onClick={handleAddRule}
|
||||||
variant="secondary"
|
|
||||||
size="small"
|
|
||||||
disabled={!isEditable}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
dropdownOffset={{ x: 0, y: 4 }}
|
|
||||||
dropdownComponents={
|
|
||||||
<SettingsRolePermissionsObjectLevelObjectPickerDropdownContent
|
|
||||||
excludedObjectMetadataIds={
|
|
||||||
filteredObjectPermissions?.map(
|
|
||||||
(objectPermission) => objectPermission.objectMetadataId,
|
|
||||||
) ?? []
|
|
||||||
}
|
|
||||||
onSelect={handleSelectObjectMetadata}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</StyledCreateObjectOverrideSection>
|
</StyledCreateObjectOverrideSection>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,28 +1,14 @@
|
|||||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
|
|
||||||
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
|
||||||
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel';
|
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel';
|
||||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { H3Title } from 'twenty-ui/display';
|
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
const StyledObjectTypeTag = styled(SettingsDataModelObjectTypeTag)`
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: ${({ theme }) => theme.spacing(5)};
|
|
||||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTitleContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type SettingsRolePermissionsObjectLevelObjectFormProps = {
|
type SettingsRolePermissionsObjectLevelObjectFormProps = {
|
||||||
roleId: string;
|
roleId: string;
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
@ -42,23 +28,19 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
|||||||
|
|
||||||
const objectMetadataItem = objectMetadata.objectMetadataItem;
|
const objectMetadataItem = objectMetadata.objectMetadataItem;
|
||||||
|
|
||||||
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
|
const objectLabelSingular = objectMetadataItem.labelSingular;
|
||||||
|
const objectLabelPlural = objectMetadataItem.labelPlural;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title={
|
title={t`2. Set ${objectLabelPlural} permissions`}
|
||||||
<StyledTitleContainer>
|
|
||||||
<H3Title title={objectMetadataItem.labelPlural} />
|
|
||||||
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
|
||||||
</StyledTitleContainer>
|
|
||||||
}
|
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
children: 'Workspace',
|
children: t`Workspace`,
|
||||||
href: getSettingsPath(SettingsPath.Workspace),
|
href: getSettingsPath(SettingsPath.Workspace),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: 'Roles',
|
children: t`Roles`,
|
||||||
href: getSettingsPath(SettingsPath.Roles),
|
href: getSettingsPath(SettingsPath.Roles),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,13 +50,13 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: `Permissions · ${objectMetadataItem.labelSingular}`,
|
children: t`Permissions · ${objectLabelSingular}`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
actionButton={
|
actionButton={
|
||||||
<Button
|
<Button
|
||||||
title={t`Back`}
|
title={t`Finish`}
|
||||||
variant="primary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
accent="blue"
|
accent="blue"
|
||||||
to={getSettingsPath(SettingsPath.RoleDetail, {
|
to={getSettingsPath(SettingsPath.RoleDetail, {
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { ObjectPermission, Role } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const hasPermissionOverride = (
|
||||||
|
objectPermission: ObjectPermission,
|
||||||
|
settingsDraftRole: Role,
|
||||||
|
) => {
|
||||||
|
const permissionChecks = [
|
||||||
|
{
|
||||||
|
permission: objectPermission.canReadObjectRecords,
|
||||||
|
globalPermission: settingsDraftRole.canReadAllObjectRecords,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permission: objectPermission.canUpdateObjectRecords,
|
||||||
|
globalPermission: settingsDraftRole.canUpdateAllObjectRecords,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permission: objectPermission.canSoftDeleteObjectRecords,
|
||||||
|
globalPermission: settingsDraftRole.canSoftDeleteAllObjectRecords,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permission: objectPermission.canDestroyObjectRecords,
|
||||||
|
globalPermission: settingsDraftRole.canDestroyAllObjectRecords,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return permissionChecks.some(
|
||||||
|
({ permission, globalPermission }) =>
|
||||||
|
isDefined(permission) && permission !== globalPermission,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -20,7 +20,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
|
|||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui/display';
|
import { IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui/display';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
@ -74,7 +74,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
|||||||
|
|
||||||
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
|
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
|
||||||
|
|
||||||
const settingsDraftRole = useRecoilValue(
|
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||||
settingsDraftRoleFamilyState(roleId),
|
settingsDraftRoleFamilyState(roleId),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -110,6 +110,12 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
|||||||
|
|
||||||
const isDirty = !isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
|
const isDirty = !isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (isDefined(settingsPersistedRole)) {
|
||||||
|
setSettingsDraftRole(settingsPersistedRole);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const dirtyFields = getDirtyFields(
|
const dirtyFields = getDirtyFields(
|
||||||
settingsDraftRole,
|
settingsDraftRole,
|
||||||
@ -281,11 +287,10 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
actionButton={
|
actionButton={
|
||||||
<SaveAndCancelButtons
|
isRoleEditable &&
|
||||||
onSave={handleSave}
|
isDirty && (
|
||||||
onCancel={() => navigateSettings(SettingsPath.Roles)}
|
<SaveAndCancelButtons onSave={handleSave} onCancel={handleCancel} />
|
||||||
isSaveDisabled={!isRoleEditable || !isDirty}
|
)
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
|
|||||||
@ -45,4 +45,5 @@ export enum SettingsPath {
|
|||||||
RoleCreate = 'roles/create',
|
RoleCreate = 'roles/create',
|
||||||
RoleDetail = 'roles/:roleId',
|
RoleDetail = 'roles/:roleId',
|
||||||
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
|
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
|
||||||
|
RoleAddObjectLevel = 'roles/:roleId/add-object-permission',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
||||||
|
import { SettingsRolePermissionsObjectLevelObjectPicker } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPicker';
|
||||||
|
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { Navigate, useParams } from 'react-router-dom';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
|
export const SettingsRoleAddObjectLevel = () => {
|
||||||
|
const { roleId } = useParams();
|
||||||
|
const settingsDraftRole = useRecoilValue(
|
||||||
|
settingsDraftRoleFamilyState(roleId ?? ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!roleId) {
|
||||||
|
return <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsRolesQueryEffect />
|
||||||
|
<SubMenuTopBarContainer
|
||||||
|
title={t`1. Select an object`}
|
||||||
|
links={[
|
||||||
|
{ children: t`Roles`, href: '/settings/roles' },
|
||||||
|
{
|
||||||
|
children: settingsDraftRole.label ?? '',
|
||||||
|
href: `/settings/roles/${roleId}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: t`Add object permission`,
|
||||||
|
href: `/settings/roles/${roleId}/add-object-permission`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<SettingsRolePermissionsObjectLevelObjectPicker roleId={roleId} />
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,14 +1,16 @@
|
|||||||
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
||||||
import { SettingsRole } from '@/settings/roles/role/components/SettingsRole';
|
import { SettingsRole } from '@/settings/roles/role/components/SettingsRole';
|
||||||
import { SettingsRoleEditEffect } from '@/settings/roles/role/components/SettingsRoleEditEffect';
|
import { SettingsRoleEditEffect } from '@/settings/roles/role/components/SettingsRoleEditEffect';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Navigate, useParams } from 'react-router-dom';
|
import { Navigate, useParams } from 'react-router-dom';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
export const SettingsRoleEdit = () => {
|
export const SettingsRoleEdit = () => {
|
||||||
const { roleId } = useParams();
|
const { roleId } = useParams();
|
||||||
|
|
||||||
if (!isDefined(roleId)) {
|
if (!isDefined(roleId)) {
|
||||||
return <Navigate to="/settings/roles" />;
|
return <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
import { Navigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
||||||
import { SettingsRolePermissionsObjectLevelObjectForm } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm';
|
import { SettingsRolePermissionsObjectLevelObjectForm } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { Navigate, useParams } from 'react-router-dom';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
export const SettingsRoleObjectLevel = () => {
|
export const SettingsRoleObjectLevel = () => {
|
||||||
const { roleId, objectMetadataId } = useParams();
|
const { roleId, objectMetadataId } = useParams();
|
||||||
|
|
||||||
if (!isDefined(roleId)) {
|
if (!isDefined(roleId)) {
|
||||||
return <Navigate to="/settings/roles" />;
|
return <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDefined(objectMetadataId)) {
|
if (!isDefined(objectMetadataId)) {
|
||||||
return <Navigate to={`/settings/roles/${roleId}`} />;
|
return (
|
||||||
|
<Navigate to={getSettingsPath(SettingsPath.RoleDetail, { roleId })} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user