Add object level permission permissions to role page (ReadOnly) (#11568)
## Context This PR adds the display of object-level permissions. A following PR will add the ability to update those permissions. The PR contains the SettingsRoleObjectLevel page but it's not fully implemented yet (save won't trigger the corresponding mutation) <img width="616" alt="Screenshot 2025-04-14 at 18 02 40" src="https://github.com/user-attachments/assets/f8c58193-31f3-468a-a96d-f06a9f2e1423" />
This commit is contained in:
@ -1891,6 +1891,7 @@ export type Role = {
|
||||
id: Scalars['String']['output'];
|
||||
isEditable: Scalars['Boolean']['output'];
|
||||
label: Scalars['String']['output'];
|
||||
objectPermissions?: Maybe<Array<ObjectPermission>>;
|
||||
settingPermissions?: Maybe<Array<SettingPermission>>;
|
||||
workspaceMembers: Array<WorkspaceMember>;
|
||||
};
|
||||
|
||||
@ -1693,6 +1693,7 @@ export type Role = {
|
||||
id: Scalars['String'];
|
||||
isEditable: Scalars['Boolean'];
|
||||
label: Scalars['String'];
|
||||
objectPermissions?: Maybe<Array<ObjectPermission>>;
|
||||
settingPermissions?: Maybe<Array<SettingPermission>>;
|
||||
workspaceMembers: Array<WorkspaceMember>;
|
||||
};
|
||||
@ -2668,6 +2669,8 @@ export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
|
||||
export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: { __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean } };
|
||||
|
||||
export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', id: string, objectMetadataId: string, roleId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null };
|
||||
|
||||
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 SettingPermissionFragmentFragment = { __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string };
|
||||
@ -2704,7 +2707,7 @@ export type UpsertSettingPermissionsMutation = { __typename?: 'Mutation', upsert
|
||||
export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, settingPermissions?: Array<{ __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }> | null }> };
|
||||
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, settingPermissions?: Array<{ __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', id: string, objectMetadataId: string, roleId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null }> };
|
||||
|
||||
export type CreateApprovedAccessDomainMutationVariables = Exact<{
|
||||
input: CreateApprovedAccessDomainInput;
|
||||
@ -3019,6 +3022,17 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ObjectPermissionFragmentFragmentDoc = gql`
|
||||
fragment ObjectPermissionFragment on ObjectPermission {
|
||||
id
|
||||
objectMetadataId
|
||||
roleId
|
||||
canReadObjectRecords
|
||||
canUpdateObjectRecords
|
||||
canSoftDeleteObjectRecords
|
||||
canDestroyObjectRecords
|
||||
}
|
||||
`;
|
||||
export const SettingPermissionFragmentFragmentDoc = gql`
|
||||
fragment SettingPermissionFragment on SettingPermission {
|
||||
id
|
||||
@ -4984,11 +4998,15 @@ export const GetRolesDocument = gql`
|
||||
settingPermissions {
|
||||
...SettingPermissionFragment
|
||||
}
|
||||
objectPermissions {
|
||||
...ObjectPermissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${RoleFragmentFragmentDoc}
|
||||
${WorkspaceMemberQueryFragmentFragmentDoc}
|
||||
${SettingPermissionFragmentFragmentDoc}`;
|
||||
${SettingPermissionFragmentFragmentDoc}
|
||||
${ObjectPermissionFragmentFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useGetRolesQuery__
|
||||
|
||||
@ -313,6 +313,12 @@ const SettingsRoleEdit = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsRoleObjectLevel = lazy(() =>
|
||||
import('~/pages/settings/roles/SettingsRoleObjectLevel').then((module) => ({
|
||||
default: module.SettingsRoleObjectLevel,
|
||||
})),
|
||||
);
|
||||
|
||||
type SettingsRoutesProps = {
|
||||
isFunctionSettingsEnabled?: boolean;
|
||||
isAdminPageEnabled?: boolean;
|
||||
@ -402,6 +408,10 @@ export const SettingsRoutes = ({
|
||||
path={SettingsPath.RoleCreate}
|
||||
element={<SettingsRoleCreate />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.RoleObjectLevel}
|
||||
element={<SettingsRoleObjectLevel />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
element={
|
||||
|
||||
@ -7,15 +7,21 @@ import { Select } from '@/ui/input/components/Select';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title, IconUserPin } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
Role,
|
||||
UpdateWorkspaceMutation,
|
||||
useUpdateWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { H2Title, IconUserPin } from 'twenty-ui/display';
|
||||
|
||||
export const SettingsRoleDefaultRole = ({ roles }: { roles: Role[] }) => {
|
||||
type SettingsRoleDefaultRoleProps = {
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
export const SettingsRoleDefaultRole = ({
|
||||
roles,
|
||||
}: SettingsRoleDefaultRoleProps) => {
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
|
||||
@ -4,8 +4,6 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
import { Role } from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import {
|
||||
AppTooltip,
|
||||
Avatar,
|
||||
@ -14,6 +12,8 @@ import {
|
||||
TooltipDelay,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { Role } from '~/generated-metadata/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
const StyledAssignedText = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
@ -52,15 +52,13 @@ const StyledTableRow = styled(TableRow)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const SettingsRolesTableRow = ({ role }: { role: Role }) => {
|
||||
type SettingsRolesTableRowProps = {
|
||||
role: Role;
|
||||
};
|
||||
|
||||
export const SettingsRolesTableRow = ({ role }: SettingsRolesTableRowProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const handleRoleClick = (roleId: string) => {
|
||||
navigateSettings(SettingsPath.RoleDetail, { roleId });
|
||||
};
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const Icon = getIcon(role.icon ?? 'IconUser');
|
||||
|
||||
@ -68,7 +66,7 @@ export const SettingsRolesTableRow = ({ role }: { role: Role }) => {
|
||||
<StyledTableRow
|
||||
key={role.id}
|
||||
gridAutoColumns="332px 3fr 2fr 1fr"
|
||||
onClick={() => handleRoleClick(role.id)}
|
||||
to={getSettingsPath(SettingsPath.RoleDetail, { roleId: role.id })}
|
||||
>
|
||||
<TableCell>
|
||||
<StyledNameCell>
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const OBJECT_PERMISSION_FRAGMENT = gql`
|
||||
fragment ObjectPermissionFragment on ObjectPermission {
|
||||
id
|
||||
objectMetadataId
|
||||
roleId
|
||||
canReadObjectRecords
|
||||
canUpdateObjectRecords
|
||||
canSoftDeleteObjectRecords
|
||||
canDestroyObjectRecords
|
||||
}
|
||||
`;
|
||||
@ -1,3 +1,4 @@
|
||||
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
|
||||
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||
import { SETTING_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/settingPermissionFragment';
|
||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||
@ -7,6 +8,7 @@ export const GET_ROLES = gql`
|
||||
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
|
||||
${ROLE_FRAGMENT}
|
||||
${SETTING_PERMISSION_FRAGMENT}
|
||||
${OBJECT_PERMISSION_FRAGMENT}
|
||||
query GetRoles {
|
||||
getRoles {
|
||||
...RoleFragment
|
||||
@ -16,6 +18,9 @@ export const GET_ROLES = gql`
|
||||
settingPermissions {
|
||||
...SettingPermissionFragment
|
||||
}
|
||||
objectPermissions {
|
||||
...ObjectPermissionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,34 +1,9 @@
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
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 { SettingsRolePermissionsObjectLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection';
|
||||
import { SettingsRolePermissionsObjectsSection } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection';
|
||||
import { SettingsRolePermissionsSettingsSection } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsSection';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
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,
|
||||
} from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
SettingPermissionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledRolePermissionsContainer = styled.div`
|
||||
display: flex;
|
||||
@ -36,19 +11,6 @@ const StyledRolePermissionsContainer = styled.div`
|
||||
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)};
|
||||
`;
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsProps = {
|
||||
roleId: string;
|
||||
isEditable: boolean;
|
||||
@ -58,168 +20,26 @@ 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`,
|
||||
Icon: IconCode,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.WORKSPACE,
|
||||
name: t`Workspace`,
|
||||
description: t`Set global workspace preferences`,
|
||||
Icon: IconSettings,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.WORKSPACE_MEMBERS,
|
||||
name: t`Users`,
|
||||
description: t`Add or remove users`,
|
||||
Icon: IconUsers,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.ROLES,
|
||||
name: t`Roles`,
|
||||
description: t`Define user roles and access levels`,
|
||||
Icon: IconLockOpen,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.DATA_MODEL,
|
||||
name: t`Data Model`,
|
||||
description: t`Edit CRM data structure and fields`,
|
||||
Icon: IconHierarchy,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.ADMIN_PANEL,
|
||||
name: t`Admin Panel`,
|
||||
description: t`Admin settings and system tools`,
|
||||
Icon: IconServer,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.SECURITY,
|
||||
name: t`Security`,
|
||||
description: t`Manage security policies`,
|
||||
Icon: IconKey,
|
||||
},
|
||||
];
|
||||
|
||||
const isPermissionsV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRolePermissionsContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Objects`}
|
||||
description={t`Ability to interact with each object`}
|
||||
<SettingsRolePermissionsObjectsSection
|
||||
roleId={roleId}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
{isPermissionsV2Enabled && (
|
||||
<SettingsRolePermissionsObjectLevelSection
|
||||
roleId={roleId}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
<StyledTable>
|
||||
<SettingsRolePermissionsObjectsTableHeader
|
||||
roleId={roleId}
|
||||
objectPermissionsConfig={objectPermissionsConfig}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
<StyledTableRows>
|
||||
{objectPermissionsConfig.map((permission) => (
|
||||
<SettingsRolePermissionsObjectsTableRow
|
||||
key={permission.key}
|
||||
permission={permission}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
))}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title={t`Settings`} description={t`Settings permissions`} />
|
||||
{isPermissionsV2Enabled && (
|
||||
<StyledCard rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconSettings}
|
||||
title={t`Settings All Access`}
|
||||
description={t`Ability to edit all settings`}
|
||||
checked={settingsDraftRole.canUpdateAllSettings}
|
||||
disabled={!isEditable}
|
||||
onChange={() => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
canUpdateAllSettings: !settingsDraftRole.canUpdateAllSettings,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</StyledCard>
|
||||
)}
|
||||
<StyledTable>
|
||||
<SettingsRolePermissionsSettingsTableHeader />
|
||||
<StyledTableRows>
|
||||
{settingsPermissionsConfig.map((permission) => (
|
||||
<SettingsRolePermissionsSettingsTableRow
|
||||
key={permission.key}
|
||||
roleId={roleId}
|
||||
permission={permission}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
))}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
</Section>
|
||||
)}
|
||||
<SettingsRolePermissionsSettingsSection
|
||||
roleId={roleId}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
</StyledRolePermissionsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
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 { 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 (
|
||||
<DropdownMenu>
|
||||
<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>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,96 @@
|
||||
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
|
||||
const StyledIconWrapper = styled.div<{ isForbidden?: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ theme, isForbidden }) =>
|
||||
isForbidden ? theme.adaptiveColors.orange1 : theme.adaptiveColors.blue1};
|
||||
border: 1px solid
|
||||
${({ theme, isForbidden }) =>
|
||||
isForbidden ? theme.adaptiveColors.orange3 : theme.adaptiveColors.blue3};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: center;
|
||||
width: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div<{ isForbidden?: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
color: ${({ theme, isForbidden }) =>
|
||||
isForbidden ? theme.color.orange : theme.color.blue};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsObjectLevelOverrideCellProps = {
|
||||
objectPermission: ObjectPermission;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelOverrideCell = ({
|
||||
objectPermission,
|
||||
}: SettingsRolePermissionsObjectLevelOverrideCellProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(objectPermission.roleId),
|
||||
);
|
||||
|
||||
const permissionMappings = {
|
||||
canReadObjectRecords: 'canReadAllObjectRecords',
|
||||
canUpdateObjectRecords: 'canUpdateAllObjectRecords',
|
||||
canSoftDeleteObjectRecords: 'canSoftDeleteAllObjectRecords',
|
||||
canDestroyObjectRecords: 'canDestroyAllObjectRecords',
|
||||
} as const;
|
||||
|
||||
type ObjectPermissionKey = keyof typeof permissionMappings;
|
||||
|
||||
const isOverridden = (permission: ObjectPermissionKey) => {
|
||||
const rolePermission = permissionMappings[permission];
|
||||
return (
|
||||
isDefined(objectPermission[permission]) &&
|
||||
!!settingsDraftRole[rolePermission as keyof typeof settingsDraftRole] !==
|
||||
!!objectPermission[permission]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSettingsRolePermissionsObjectLevelOverrideCell>
|
||||
{(Object.keys(permissionMappings) as ObjectPermissionKey[]).map(
|
||||
(permission) => {
|
||||
const { Icon, IconForbidden: IconOverride } =
|
||||
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission];
|
||||
const permissionValue = objectPermission[permission];
|
||||
|
||||
if (!isOverridden(permission)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledIconWrapper
|
||||
key={permission}
|
||||
isForbidden={permissionValue === false}
|
||||
>
|
||||
<StyledIcon isForbidden={permissionValue === false}>
|
||||
{permissionValue === false && (
|
||||
<IconOverride size={theme.icon.size.sm} />
|
||||
)}
|
||||
{permissionValue === true && <Icon size={theme.icon.size.sm} />}
|
||||
</StyledIcon>
|
||||
</StyledIconWrapper>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</StyledSettingsRolePermissionsObjectLevelOverrideCell>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,113 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
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 { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
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 { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
// const StyledCreateObjectOverrideSection = 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)};
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsObjectLevelSectionProps = {
|
||||
roleId: string;
|
||||
isEditable: boolean;
|
||||
};
|
||||
|
||||
const StyledNoOverride = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelSection = ({
|
||||
roleId,
|
||||
}: SettingsRolePermissionsObjectLevelSectionProps) => {
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const objectMetadataItems = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataMap = objectMetadataItems.objectMetadataItems.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.id] = item;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ObjectMetadataItem>,
|
||||
);
|
||||
|
||||
const objectPermissions = settingsDraftRole.objectPermissions;
|
||||
|
||||
// const handleSelectObjectMetadata = (objectMetadataId: string) => {
|
||||
// setSettingsDraftRole((draftRole) => ({
|
||||
// ...draftRole,
|
||||
// objectPermissions: [
|
||||
// ...(draftRole.objectPermissions ?? []),
|
||||
// { objectMetadataId, roleId, id: v4() },
|
||||
// ],
|
||||
// }));
|
||||
// };
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Object-Level Permissions`}
|
||||
description={t`Set additional object-level permissions`}
|
||||
/>
|
||||
<Table>
|
||||
<SettingsRolePermissionsObjectLevelTableHeader />
|
||||
<StyledTableRows>
|
||||
{isDefined(objectPermissions) && objectPermissions?.length > 0 ? (
|
||||
objectPermissions?.map((objectPermission) => (
|
||||
<SettingsRolePermissionsObjectLevelTableRow
|
||||
key={objectPermission.id}
|
||||
objectPermission={objectPermission}
|
||||
objectMetadataItem={
|
||||
objectMetadataMap[objectPermission.objectMetadataId]
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<StyledNoOverride>{t`No overrides found`}</StyledNoOverride>
|
||||
)}
|
||||
</StyledTableRows>
|
||||
</Table>
|
||||
{/* <StyledCreateObjectOverrideSection>
|
||||
<Dropdown
|
||||
dropdownId="role-object-select"
|
||||
dropdownHotkeyScope={{ scope: 'roleObject' }}
|
||||
clickableComponent={
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Add Object`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<SettingsRolePermissionsObjectLevelObjectPickerDropdownContent
|
||||
excludedObjectMetadataIds={[]}
|
||||
onSelect={handleSelectObjectMetadata}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledCreateObjectOverrideSection> */}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelTableHeader = () => (
|
||||
<TableRow>
|
||||
<TableHeader>{t`Object`}</TableHeader>
|
||||
<TableHeader>{t`Permission overrides`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
);
|
||||
@ -0,0 +1,73 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsRolePermissionsObjectLevelOverrideCell } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronRight, useIcons } from 'twenty-ui/display';
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledNameLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsObjectLevelTableRowProps = {
|
||||
objectPermission: ObjectPermission;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelTableRow = ({
|
||||
objectPermission,
|
||||
objectMetadataItem,
|
||||
}: SettingsRolePermissionsObjectLevelTableRowProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
throw new Error('Object metadata item not found');
|
||||
}
|
||||
|
||||
const Icon = getIcon(objectMetadataItem.icon);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
to={getSettingsPath(SettingsPath.RoleObjectLevel, {
|
||||
roleId: objectPermission.roleId,
|
||||
objectMetadataId: objectPermission.objectMetadataId,
|
||||
})}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
{!!Icon && (
|
||||
<Icon
|
||||
style={{ minWidth: theme.icon.size.md }}
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
<StyledNameLabel title={objectMetadataItem.labelPlural}>
|
||||
{objectMetadataItem.labelPlural}
|
||||
</StyledNameLabel>
|
||||
</StyledNameTableCell>
|
||||
<TableCell>
|
||||
<SettingsRolePermissionsObjectLevelOverrideCell
|
||||
objectPermission={objectPermission}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align={'right'}>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,94 @@
|
||||
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;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelObjectForm = ({
|
||||
roleId,
|
||||
objectMetadataId,
|
||||
}: SettingsRolePermissionsObjectLevelObjectFormProps) => {
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const objectMetadata = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const objectMetadataItem = objectMetadata.objectMetadataItem;
|
||||
|
||||
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<H3Title title={objectMetadataItem.labelPlural} />
|
||||
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
||||
</StyledTitleContainer>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'Roles',
|
||||
href: getSettingsPath(SettingsPath.Roles),
|
||||
},
|
||||
{
|
||||
children: settingsDraftRole.label,
|
||||
href: getSettingsPath(SettingsPath.RoleDetail, {
|
||||
roleId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
children: `Permissions · ${objectMetadataItem.labelSingular}`,
|
||||
},
|
||||
]}
|
||||
actionButton={
|
||||
<Button
|
||||
title={t`Back`}
|
||||
variant="primary"
|
||||
size="small"
|
||||
accent="blue"
|
||||
to={getSettingsPath(SettingsPath.RoleDetail, {
|
||||
roleId,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsRolePermissionsObjectLevelObjectFormObjectLevel
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
roleId={roleId}
|
||||
/>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,100 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader';
|
||||
import { SettingsRolePermissionsObjectsTableRow } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsTableRow';
|
||||
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
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 SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps = {
|
||||
roleId: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
|
||||
roleId,
|
||||
objectMetadataItem,
|
||||
}: SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps) => {
|
||||
const settingsDraftRole = useRecoilValue(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const settingsDraftRoleObjectPermissions =
|
||||
settingsDraftRole.objectPermissions?.find(
|
||||
(permission) => permission.objectMetadataId === objectMetadataItem.id,
|
||||
);
|
||||
|
||||
if (!settingsDraftRoleObjectPermissions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectLabel = objectMetadataItem.labelPlural;
|
||||
|
||||
const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [
|
||||
{
|
||||
key: 'canReadObjectRecords',
|
||||
label: t`See Records on ${objectLabel}`,
|
||||
value: settingsDraftRoleObjectPermissions.canReadObjectRecords,
|
||||
setValue: (_value: boolean) => {
|
||||
// TODO: Implement
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'canUpdateObjectRecords',
|
||||
label: t`Edit Records on ${objectLabel}`,
|
||||
value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords,
|
||||
setValue: (_value: boolean) => {
|
||||
// TODO: Implement
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'canSoftDeleteObjectRecords',
|
||||
label: t`Delete Records on ${objectLabel}`,
|
||||
value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords,
|
||||
setValue: (_value: boolean) => {
|
||||
// TODO: Implement
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'canDestroyObjectRecords',
|
||||
label: t`Destroy Records on ${objectLabel}`,
|
||||
value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords,
|
||||
setValue: (_value: boolean) => {
|
||||
// TODO: Implement
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Object-Level Permissions`}
|
||||
description={t`Ability to interact with this specific object`}
|
||||
/>
|
||||
<StyledTable>
|
||||
<SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader />
|
||||
<StyledTableRows>
|
||||
{objectPermissionsConfig.map((permission) => (
|
||||
<SettingsRolePermissionsObjectsTableRow
|
||||
key={permission.key}
|
||||
permission={permission}
|
||||
isEditable={settingsDraftRole.isEditable}
|
||||
/>
|
||||
))}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader =
|
||||
() => (
|
||||
<TableRow gridAutoColumns="1fr 24px">
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader aria-label={t`Actions`}></TableHeader>
|
||||
</TableRow>
|
||||
);
|
||||
@ -0,0 +1,135 @@
|
||||
import { SettingsRolePermissionsObjectsTableHeader } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsTableHeader';
|
||||
import { SettingsRolePermissionsObjectsTableRow } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsTableRow';
|
||||
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
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 SettingsRolePermissionsObjectsSectionProps = {
|
||||
roleId: string;
|
||||
isEditable: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissionsObjectsSection = ({
|
||||
roleId,
|
||||
isEditable,
|
||||
}: SettingsRolePermissionsObjectsSectionProps) => {
|
||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const objectPermissions = settingsDraftRole.objectPermissions;
|
||||
|
||||
const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [
|
||||
{
|
||||
key: 'canReadObjectRecords',
|
||||
label: t`See Records on All Objects`,
|
||||
overriddenBy:
|
||||
objectPermissions?.filter(
|
||||
(permission) =>
|
||||
isDefined(permission.canReadObjectRecords) &&
|
||||
permission.canReadObjectRecords !==
|
||||
settingsDraftRole.canReadAllObjectRecords,
|
||||
)?.length ?? 0,
|
||||
value: settingsDraftRole.canReadAllObjectRecords,
|
||||
setValue: (value: boolean) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
canReadAllObjectRecords: value,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'canUpdateObjectRecords',
|
||||
label: t`Edit Records on All Objects`,
|
||||
overriddenBy:
|
||||
objectPermissions?.filter(
|
||||
(permission) =>
|
||||
isDefined(permission.canUpdateObjectRecords) &&
|
||||
permission.canUpdateObjectRecords !==
|
||||
settingsDraftRole.canUpdateAllObjectRecords,
|
||||
)?.length ?? 0,
|
||||
value: settingsDraftRole.canUpdateAllObjectRecords,
|
||||
setValue: (value: boolean) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
canUpdateAllObjectRecords: value,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'canSoftDeleteObjectRecords',
|
||||
label: t`Delete Records on All Objects`,
|
||||
overriddenBy:
|
||||
objectPermissions?.filter(
|
||||
(permission) =>
|
||||
isDefined(permission.canSoftDeleteObjectRecords) &&
|
||||
permission.canSoftDeleteObjectRecords !==
|
||||
settingsDraftRole.canSoftDeleteAllObjectRecords,
|
||||
)?.length ?? 0,
|
||||
value: settingsDraftRole.canSoftDeleteAllObjectRecords,
|
||||
setValue: (value: boolean) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
canSoftDeleteAllObjectRecords: value,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'canDestroyObjectRecords',
|
||||
label: t`Destroy Records on All Objects`,
|
||||
overriddenBy:
|
||||
objectPermissions?.filter(
|
||||
(permission) =>
|
||||
isDefined(permission.canDestroyObjectRecords) &&
|
||||
permission.canDestroyObjectRecords !==
|
||||
settingsDraftRole.canDestroyAllObjectRecords,
|
||||
)?.length ?? 0,
|
||||
value: settingsDraftRole.canDestroyAllObjectRecords,
|
||||
setValue: (value: boolean) => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
canDestroyAllObjectRecords: value,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Objects`}
|
||||
description={t`Ability to interact with each object`}
|
||||
/>
|
||||
<StyledTable>
|
||||
<SettingsRolePermissionsObjectsTableHeader
|
||||
roleId={roleId}
|
||||
objectPermissionsConfig={objectPermissionsConfig}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
<StyledTableRows>
|
||||
{objectPermissionsConfig.map((permission) => (
|
||||
<SettingsRolePermissionsObjectsTableRow
|
||||
key={permission.key}
|
||||
permission={permission}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
))}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
|
||||
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';
|
||||
@ -1,32 +1,24 @@
|
||||
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission';
|
||||
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
|
||||
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/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/input';
|
||||
|
||||
const StyledIconWrapper = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.adaptiveColors.blue1};
|
||||
border: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: center;
|
||||
width: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.color.blue};
|
||||
justify-content: center;
|
||||
`;
|
||||
import { t } from '@lingui/core/macro';
|
||||
import pluralize from 'pluralize';
|
||||
import { Checkbox, CheckboxAccent } from 'twenty-ui/input';
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
|
||||
const StyledOverrideInfo = styled.span`
|
||||
background: ${({ theme }) => theme.adaptiveColors.orange1};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${({ theme }) => theme.color.orange};
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledPermissionCell = styled(TableCell)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -58,21 +50,30 @@ export const SettingsRolePermissionsObjectsTableRow = ({
|
||||
}: SettingsRolePermissionsObjectsTableRowProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const isOverriddenBy = permission.overriddenBy;
|
||||
const isOverridden = isOverriddenBy && isOverriddenBy > 0;
|
||||
const label = permission.label;
|
||||
const pluralizedObject = pluralize('object', isOverriddenBy);
|
||||
|
||||
const { Icon } = SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key];
|
||||
|
||||
return (
|
||||
<StyledTableRow>
|
||||
<StyledPermissionCell>
|
||||
<StyledIconWrapper>
|
||||
<StyledIcon>
|
||||
<permission.Icon size={theme.icon.size.sm} />
|
||||
</StyledIcon>
|
||||
</StyledIconWrapper>
|
||||
<StyledLabel>{permission.label}</StyledLabel>
|
||||
<Icon size={theme.icon.size.sm} />
|
||||
<StyledLabel>{label}</StyledLabel>
|
||||
{isOverridden ? (
|
||||
<StyledOverrideInfo>
|
||||
{t`Overridden on ${isOverriddenBy} ${pluralizedObject}`}
|
||||
</StyledOverrideInfo>
|
||||
) : null}
|
||||
</StyledPermissionCell>
|
||||
<StyledCheckboxCell>
|
||||
<Checkbox
|
||||
checked={permission.value}
|
||||
checked={permission.value ?? false}
|
||||
onChange={() => permission.setValue(!permission.value)}
|
||||
disabled={!isEditable}
|
||||
accent={isOverridden ? CheckboxAccent.Orange : CheckboxAccent.Blue}
|
||||
/>
|
||||
</StyledCheckboxCell>
|
||||
</StyledTableRow>
|
||||
@ -0,0 +1,37 @@
|
||||
import {
|
||||
IconComponent,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconPencil,
|
||||
IconPencilOff,
|
||||
IconTrash,
|
||||
IconTrashOff,
|
||||
IconTrashX,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
type SettingsRoleObjectPermissionIconConfig = {
|
||||
Icon: IconComponent;
|
||||
IconForbidden: IconComponent;
|
||||
};
|
||||
|
||||
export const SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG: Record<
|
||||
string,
|
||||
SettingsRoleObjectPermissionIconConfig
|
||||
> = {
|
||||
canReadObjectRecords: {
|
||||
Icon: IconEye,
|
||||
IconForbidden: IconEyeOff,
|
||||
},
|
||||
canUpdateObjectRecords: {
|
||||
Icon: IconPencil,
|
||||
IconForbidden: IconPencilOff,
|
||||
},
|
||||
canSoftDeleteObjectRecords: {
|
||||
Icon: IconTrash,
|
||||
IconForbidden: IconTrashOff,
|
||||
},
|
||||
canDestroyObjectRecords: {
|
||||
Icon: IconTrashX,
|
||||
IconForbidden: IconTrashX,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { ReactNode } from 'react';
|
||||
export type SettingsRolePermissionsObjectPermission = {
|
||||
key: string;
|
||||
label: string | ReactNode;
|
||||
value?: boolean | null;
|
||||
setValue: (value: boolean) => void;
|
||||
overriddenBy?: number;
|
||||
};
|
||||
@ -0,0 +1,130 @@
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableHeader';
|
||||
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableRow';
|
||||
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
H2Title,
|
||||
IconCode,
|
||||
IconHierarchy,
|
||||
IconKey,
|
||||
IconLockOpen,
|
||||
IconSettings,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
SettingPermissionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
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)};
|
||||
`;
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsSettingsSectionProps = {
|
||||
roleId: string;
|
||||
isEditable: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRolePermissionsSettingsSection = ({
|
||||
roleId,
|
||||
isEditable,
|
||||
}: SettingsRolePermissionsSettingsSectionProps) => {
|
||||
const isPermissionsV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
);
|
||||
|
||||
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[] =
|
||||
[
|
||||
{
|
||||
key: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
|
||||
name: t`API Keys & Webhooks`,
|
||||
description: t`Manage API keys and webhooks`,
|
||||
Icon: IconCode,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.WORKSPACE,
|
||||
name: t`Workspace`,
|
||||
description: t`Set global workspace preferences`,
|
||||
Icon: IconSettings,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.WORKSPACE_MEMBERS,
|
||||
name: t`Users`,
|
||||
description: t`Add or remove users`,
|
||||
Icon: IconUsers,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.ROLES,
|
||||
name: t`Roles`,
|
||||
description: t`Define user roles and access levels`,
|
||||
Icon: IconLockOpen,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.DATA_MODEL,
|
||||
name: t`Data Model`,
|
||||
description: t`Edit CRM data structure and fields`,
|
||||
Icon: IconHierarchy,
|
||||
},
|
||||
{
|
||||
key: SettingPermissionType.SECURITY,
|
||||
name: t`Security`,
|
||||
description: t`Manage security policies`,
|
||||
Icon: IconKey,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title={t`Settings`} description={t`Settings permissions`} />
|
||||
{isPermissionsV2Enabled && (
|
||||
<StyledCard rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconSettings}
|
||||
title={t`Settings All Access`}
|
||||
description={t`Ability to edit all settings`}
|
||||
checked={settingsDraftRole.canUpdateAllSettings}
|
||||
disabled={!isEditable}
|
||||
onChange={() => {
|
||||
setSettingsDraftRole({
|
||||
...settingsDraftRole,
|
||||
canUpdateAllSettings: !settingsDraftRole.canUpdateAllSettings,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</StyledCard>
|
||||
)}
|
||||
<StyledTable>
|
||||
<SettingsRolePermissionsSettingsTableHeader />
|
||||
<StyledTableRows>
|
||||
{settingsPermissionsConfig.map((permission) => (
|
||||
<SettingsRolePermissionsSettingsTableRow
|
||||
key={permission.key}
|
||||
roleId={roleId}
|
||||
permission={permission}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
))}
|
||||
</StyledTableRows>
|
||||
</StyledTable>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/types/SettingsRolePermissionsSettingPermission';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
@ -10,6 +10,8 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
|
||||
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
|
||||
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||
@ -78,6 +80,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
settingsPersistedRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
if (!isDefined(settingsRolesIsLoading)) {
|
||||
return <></>;
|
||||
}
|
||||
@ -110,6 +114,13 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
|
||||
settingsPersistedRole,
|
||||
);
|
||||
|
||||
if (isDefined(dirtyFields.label) && dirtyFields.label === '') {
|
||||
enqueueSnackBar(t`Role name cannot be empty`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreateMode) {
|
||||
const roleId = v4();
|
||||
|
||||
|
||||
@ -5,7 +5,13 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
export const SettingsRoleCreateEffect = ({ roleId }: { roleId: string }) => {
|
||||
type SettingsRoleCreateEffectProps = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export const SettingsRoleCreateEffect = ({
|
||||
roleId,
|
||||
}: SettingsRoleCreateEffectProps) => {
|
||||
const setSettingsDraftRole = useSetRecoilState(
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
@ -4,10 +4,6 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type SettingsRoleLabelContainerProps = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
const ROLE_LABEL_EDIT_HOTKEY_SCOPE = 'role-label-edit';
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
@ -21,6 +17,10 @@ const StyledHeaderTitle = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
type SettingsRoleLabelContainerProps = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export const SettingsRoleLabelContainer = ({
|
||||
roleId,
|
||||
}: SettingsRoleLabelContainerProps) => {
|
||||
|
||||
@ -16,5 +16,6 @@ export const settingsDraftRoleFamilyState = createFamilyState<Role, string>({
|
||||
isEditable: false,
|
||||
workspaceMembers: [],
|
||||
settingPermissions: [],
|
||||
objectPermissions: [],
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
export type SettingsRolePermissionsObjectPermission = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: boolean;
|
||||
Icon: IconComponent;
|
||||
setValue: (value: boolean) => void;
|
||||
};
|
||||
@ -44,4 +44,5 @@ export enum SettingsPath {
|
||||
Roles = 'roles',
|
||||
RoleCreate = 'roles/create',
|
||||
RoleDetail = 'roles/:roleId',
|
||||
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
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 { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const SettingsRoleObjectLevel = () => {
|
||||
const { roleId, objectMetadataId } = useParams();
|
||||
|
||||
if (!isDefined(roleId)) {
|
||||
return <Navigate to="/settings/roles" />;
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
return <Navigate to={`/settings/roles/${roleId}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsRolesQueryEffect />
|
||||
<SettingsRolePermissionsObjectLevelObjectForm
|
||||
roleId={roleId}
|
||||
objectMetadataId={objectMetadataId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -3,6 +3,7 @@ import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
import { Relation } from 'typeorm';
|
||||
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto';
|
||||
|
||||
@ -46,4 +47,7 @@ export class RoleDTO {
|
||||
|
||||
@Field(() => [SettingPermissionDTO], { nullable: true })
|
||||
settingPermissions?: SettingPermissionDTO[];
|
||||
|
||||
@Field(() => [ObjectPermissionDTO], { nullable: true })
|
||||
objectPermissions?: ObjectPermissionDTO[];
|
||||
}
|
||||
|
||||
@ -39,7 +39,11 @@ export class RoleService {
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['userWorkspaceRoles', 'settingPermissions'],
|
||||
relations: [
|
||||
'userWorkspaceRoles',
|
||||
'settingPermissions',
|
||||
'objectPermissions',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -4,18 +4,18 @@ export {
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconApi,
|
||||
IconAppWindow,
|
||||
IconApps,
|
||||
IconAppWindow,
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
IconArrowDown,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconArrowsDiagonal,
|
||||
IconArrowsVertical,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconAt,
|
||||
IconBaselineDensitySmall,
|
||||
IconBell,
|
||||
@ -47,8 +47,8 @@ export {
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconChevronUp,
|
||||
IconChevronsRight,
|
||||
IconChevronUp,
|
||||
IconCircleDot,
|
||||
IconCircleOff,
|
||||
IconCirclePlus,
|
||||
@ -217,6 +217,7 @@ export {
|
||||
IconPaperclip,
|
||||
IconPassword,
|
||||
IconPencil,
|
||||
IconPencilOff,
|
||||
IconPercentage,
|
||||
IconPhone,
|
||||
IconPhoto,
|
||||
@ -283,6 +284,7 @@ export {
|
||||
IconTimelineEvent,
|
||||
IconTool,
|
||||
IconTrash,
|
||||
IconTrashOff,
|
||||
IconTrashX,
|
||||
IconTypography,
|
||||
IconUnlink,
|
||||
|
||||
@ -65,18 +65,18 @@ export {
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconApi,
|
||||
IconAppWindow,
|
||||
IconApps,
|
||||
IconAppWindow,
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
IconArrowDown,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconArrowsDiagonal,
|
||||
IconArrowsVertical,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconAt,
|
||||
IconBaselineDensitySmall,
|
||||
IconBell,
|
||||
@ -108,8 +108,8 @@ export {
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconChevronUp,
|
||||
IconChevronsRight,
|
||||
IconChevronUp,
|
||||
IconCircleDot,
|
||||
IconCircleOff,
|
||||
IconCirclePlus,
|
||||
@ -278,6 +278,7 @@ export {
|
||||
IconPaperclip,
|
||||
IconPassword,
|
||||
IconPencil,
|
||||
IconPencilOff,
|
||||
IconPercentage,
|
||||
IconPhone,
|
||||
IconPhoto,
|
||||
@ -344,6 +345,7 @@ export {
|
||||
IconTimelineEvent,
|
||||
IconTool,
|
||||
IconTrash,
|
||||
IconTrashOff,
|
||||
IconTrashX,
|
||||
IconTypography,
|
||||
IconUnlink,
|
||||
|
||||
@ -19,6 +19,11 @@ export enum CheckboxSize {
|
||||
Small = 'small',
|
||||
}
|
||||
|
||||
export enum CheckboxAccent {
|
||||
Blue = 'blue',
|
||||
Orange = 'orange',
|
||||
}
|
||||
|
||||
type CheckboxProps = {
|
||||
checked: boolean;
|
||||
indeterminate?: boolean;
|
||||
@ -30,11 +35,13 @@ type CheckboxProps = {
|
||||
shape?: CheckboxShape;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
accent?: CheckboxAccent;
|
||||
};
|
||||
|
||||
type InputProps = {
|
||||
checkboxSize: CheckboxSize;
|
||||
variant: CheckboxVariant;
|
||||
accent?: CheckboxAccent;
|
||||
indeterminate?: boolean;
|
||||
hoverable?: boolean;
|
||||
shape?: CheckboxShape;
|
||||
@ -68,12 +75,14 @@ const StyledInputContainer = styled.div<InputProps>`
|
||||
}
|
||||
}};
|
||||
position: relative;
|
||||
${({ hoverable, isChecked, theme, indeterminate, disabled }) => {
|
||||
${({ hoverable, isChecked, theme, indeterminate, disabled, accent }) => {
|
||||
if (!hoverable || disabled === true) return '';
|
||||
return `&:hover{
|
||||
background-color: ${
|
||||
indeterminate || isChecked
|
||||
? theme.background.transparent.blue
|
||||
? accent === CheckboxAccent.Blue
|
||||
? theme.background.transparent.blue
|
||||
: theme.background.transparent.orange
|
||||
: theme.background.transparent.light
|
||||
};
|
||||
}}
|
||||
@ -100,9 +109,15 @@ const StyledInput = styled.input<InputProps>`
|
||||
& + label:before {
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
background: ${({ theme, indeterminate, isChecked, disabled }) => {
|
||||
background: ${({ theme, indeterminate, isChecked, disabled, accent }) => {
|
||||
if (!(indeterminate || isChecked)) return 'transparent';
|
||||
return disabled ? theme.adaptiveColors.blue3 : theme.color.blue;
|
||||
return disabled
|
||||
? accent === CheckboxAccent.Blue
|
||||
? theme.adaptiveColors.blue3
|
||||
: theme.adaptiveColors.orange3
|
||||
: accent === CheckboxAccent.Blue
|
||||
? theme.color.blue
|
||||
: theme.color.orange;
|
||||
}};
|
||||
border-color: ${({
|
||||
theme,
|
||||
@ -110,10 +125,17 @@ const StyledInput = styled.input<InputProps>`
|
||||
isChecked,
|
||||
variant,
|
||||
disabled,
|
||||
accent,
|
||||
}) => {
|
||||
switch (true) {
|
||||
case indeterminate || isChecked:
|
||||
return disabled ? theme.adaptiveColors.blue3 : theme.color.blue;
|
||||
return disabled
|
||||
? accent === CheckboxAccent.Blue
|
||||
? theme.adaptiveColors.blue3
|
||||
: theme.adaptiveColors.orange3
|
||||
: accent === CheckboxAccent.Blue
|
||||
? theme.color.blue
|
||||
: theme.color.orange;
|
||||
case disabled:
|
||||
return theme.border.color.strong;
|
||||
case variant === CheckboxVariant.Primary:
|
||||
@ -165,6 +187,7 @@ export const Checkbox = ({
|
||||
hoverable = true,
|
||||
className,
|
||||
disabled = false,
|
||||
accent = CheckboxAccent.Blue,
|
||||
}: CheckboxProps) => {
|
||||
const [isInternalChecked, setIsInternalChecked] =
|
||||
React.useState<boolean>(false);
|
||||
@ -191,6 +214,7 @@ export const Checkbox = ({
|
||||
indeterminate={indeterminate}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
accent={accent}
|
||||
>
|
||||
<StyledInput
|
||||
autoComplete="off"
|
||||
@ -206,6 +230,7 @@ export const Checkbox = ({
|
||||
isChecked={isInternalChecked}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
accent={accent}
|
||||
/>
|
||||
<label htmlFor={checkboxId}>
|
||||
{indeterminate ? (
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxAccent,
|
||||
CheckboxShape,
|
||||
CheckboxSize,
|
||||
CheckboxVariant,
|
||||
@ -29,6 +30,7 @@ export const Default: Story = {
|
||||
variant: CheckboxVariant.Primary,
|
||||
size: CheckboxSize.Small,
|
||||
shape: CheckboxShape.Squared,
|
||||
accent: CheckboxAccent.Blue,
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
@ -42,6 +44,7 @@ export const Catalog: CatalogStory<Story, typeof Checkbox> = {
|
||||
checked: { control: false },
|
||||
hoverable: { control: false },
|
||||
shape: { control: false },
|
||||
accent: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
catalog: {
|
||||
@ -82,6 +85,11 @@ export const Catalog: CatalogStory<Story, typeof Checkbox> = {
|
||||
values: Object.values(CheckboxSize),
|
||||
props: (size: CheckboxSize) => ({ size }),
|
||||
},
|
||||
{
|
||||
name: 'accent',
|
||||
values: Object.values(CheckboxAccent),
|
||||
props: (accent: CheckboxAccent) => ({ accent }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -82,6 +82,7 @@ export {
|
||||
CheckboxVariant,
|
||||
CheckboxShape,
|
||||
CheckboxSize,
|
||||
CheckboxAccent,
|
||||
Checkbox,
|
||||
} from './components/Checkbox';
|
||||
export { IconListViewGrip } from './components/IconListViewGrip';
|
||||
|
||||
@ -23,6 +23,7 @@ export const BACKGROUND_DARK = {
|
||||
lighter: RGBA(GRAY_SCALE.gray0, 0.03),
|
||||
danger: RGBA(COLOR.red, 0.08),
|
||||
blue: RGBA(COLOR.blue, 0.2),
|
||||
orange: RGBA(COLOR.orange, 0.2),
|
||||
},
|
||||
overlayPrimary: RGBA(GRAY_SCALE.gray100, 0.8),
|
||||
overlaySecondary: RGBA(GRAY_SCALE.gray100, 0.6),
|
||||
|
||||
@ -23,6 +23,7 @@ export const BACKGROUND_LIGHT = {
|
||||
lighter: RGBA(GRAY_SCALE.gray100, 0.02),
|
||||
danger: RGBA(COLOR.red, 0.08),
|
||||
blue: RGBA(COLOR.blue, 0.08),
|
||||
orange: RGBA(COLOR.orange, 0.08),
|
||||
},
|
||||
overlayPrimary: RGBA(GRAY_SCALE.gray80, 0.8),
|
||||
overlaySecondary: RGBA(GRAY_SCALE.gray80, 0.4),
|
||||
|
||||
Reference in New Issue
Block a user