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 = {
|
||||
isFunctionSettingsEnabled?: boolean;
|
||||
isAdminPageEnabled?: boolean;
|
||||
@ -420,6 +428,10 @@ export const SettingsRoutes = ({
|
||||
path={SettingsPath.RoleObjectLevel}
|
||||
element={<SettingsRoleObjectLevel />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.RoleAddObjectLevel}
|
||||
element={<SettingsRoleAddObjectLevel />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
element={
|
||||
|
||||
@ -47,10 +47,6 @@ const StyledSearchContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => 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)};
|
||||
|
||||
@ -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 { 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 = ({
|
||||
</StyledTableRows>
|
||||
</Table>
|
||||
<StyledCreateObjectOverrideSection>
|
||||
<Dropdown
|
||||
dropdownId="role-object-select"
|
||||
dropdownHotkeyScope={{ scope: 'roleObject' }}
|
||||
clickableComponent={
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Add rule`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
}
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
dropdownComponents={
|
||||
<SettingsRolePermissionsObjectLevelObjectPickerDropdownContent
|
||||
excludedObjectMetadataIds={
|
||||
filteredObjectPermissions?.map(
|
||||
(objectPermission) => objectPermission.objectMetadataId,
|
||||
) ?? []
|
||||
}
|
||||
onSelect={handleSelectObjectMetadata}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Add rule`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!isEditable || allObjectsHaveSetPermission}
|
||||
onClick={handleAddRule}
|
||||
/>
|
||||
</StyledCreateObjectOverrideSection>
|
||||
</Section>
|
||||
|
||||
@ -1,28 +1,14 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
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 { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H3Title } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
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 = {
|
||||
roleId: string;
|
||||
objectMetadataId: string;
|
||||
@ -42,23 +28,19 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
|
||||
const objectMetadataItem = objectMetadata.objectMetadataItem;
|
||||
|
||||
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
|
||||
const objectLabelSingular = objectMetadataItem.labelSingular;
|
||||
const objectLabelPlural = objectMetadataItem.labelPlural;
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<H3Title title={objectMetadataItem.labelPlural} />
|
||||
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
||||
</StyledTitleContainer>
|
||||
}
|
||||
title={t`2. Set ${objectLabelPlural} permissions`}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'Roles',
|
||||
children: t`Roles`,
|
||||
href: getSettingsPath(SettingsPath.Roles),
|
||||
},
|
||||
{
|
||||
@ -68,13 +50,13 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
}),
|
||||
},
|
||||
{
|
||||
children: `Permissions · ${objectMetadataItem.labelSingular}`,
|
||||
children: t`Permissions · ${objectLabelSingular}`,
|
||||
},
|
||||
]}
|
||||
actionButton={
|
||||
<Button
|
||||
title={t`Back`}
|
||||
variant="primary"
|
||||
title={t`Finish`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
accent="blue"
|
||||
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 { getOperationName } from '@apollo/client/utilities';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLockOpen, IconSettings, IconUserPlus } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
@ -74,7 +74,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
|
||||
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
|
||||
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
@ -110,6 +110,12 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
|
||||
const isDirty = !isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
|
||||
|
||||
const handleCancel = () => {
|
||||
if (isDefined(settingsPersistedRole)) {
|
||||
setSettingsDraftRole(settingsPersistedRole);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const dirtyFields = getDirtyFields(
|
||||
settingsDraftRole,
|
||||
@ -281,11 +287,10 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
},
|
||||
]}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSave}
|
||||
onCancel={() => navigateSettings(SettingsPath.Roles)}
|
||||
isSaveDisabled={!isRoleEditable || !isDirty}
|
||||
/>
|
||||
isRoleEditable &&
|
||||
isDirty && (
|
||||
<SaveAndCancelButtons onSave={handleSave} onCancel={handleCancel} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
|
||||
@ -45,4 +45,5 @@ export enum SettingsPath {
|
||||
RoleCreate = 'roles/create',
|
||||
RoleDetail = 'roles/:roleId',
|
||||
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 { SettingsRole } from '@/settings/roles/role/components/SettingsRole';
|
||||
import { SettingsRoleEditEffect } from '@/settings/roles/role/components/SettingsRoleEditEffect';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
export const SettingsRoleEdit = () => {
|
||||
const { roleId } = useParams();
|
||||
|
||||
if (!isDefined(roleId)) {
|
||||
return <Navigate to="/settings/roles" />;
|
||||
return <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
|
||||
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 { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
export const SettingsRoleObjectLevel = () => {
|
||||
const { roleId, objectMetadataId } = useParams();
|
||||
|
||||
if (!isDefined(roleId)) {
|
||||
return <Navigate to="/settings/roles" />;
|
||||
return <Navigate to={getSettingsPath(SettingsPath.Roles)} />;
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
return <Navigate to={`/settings/roles/${roleId}`} />;
|
||||
return (
|
||||
<Navigate to={getSettingsPath(SettingsPath.RoleDetail, { roleId })} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user