diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index a54ef7f196..27be58ba62 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -950,6 +950,7 @@ export type CreateRouteTriggerInput = { export type CreateRowLevelPermissionPredicateGroupInput = { logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: Scalars['String']; parentRowLevelPermissionPredicateGroupId?: InputMaybe; positionInRowLevelPermissionPredicateGroup?: InputMaybe; roleId: Scalars['String']; @@ -2028,6 +2029,7 @@ export type Mutation = { upsertFieldPermissions: Array; upsertObjectPermissions: Array; upsertPermissionFlags: Array; + upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; verifyEmailAndGetLoginToken: VerifyEmailAndGetLoginTokenOutput; @@ -2942,6 +2944,11 @@ export type MutationUpsertPermissionFlagsArgs = { }; +export type MutationUpsertRowLevelPermissionPredicatesArgs = { + input: UpsertRowLevelPermissionPredicatesInput; +}; + + export type MutationUserLookupAdminPanelArgs = { userIdentifier: Scalars['String']; }; @@ -3891,6 +3898,8 @@ export type Role = { label: Scalars['String']; objectPermissions?: Maybe>; permissionFlags?: Maybe>; + rowLevelPermissionPredicateGroups?: Maybe>; + rowLevelPermissionPredicates?: Maybe>; standardId?: Maybe; universalIdentifier?: Maybe; workspaceMembers: Array; @@ -3929,16 +3938,37 @@ export type RowLevelPermissionPredicateGroup = { __typename?: 'RowLevelPermissionPredicateGroup'; id: Scalars['String']; logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: Scalars['String']; parentRowLevelPermissionPredicateGroupId?: Maybe; positionInRowLevelPermissionPredicateGroup?: Maybe; roleId: Scalars['String']; }; +export type RowLevelPermissionPredicateGroupInput = { + id?: InputMaybe; + logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: Scalars['UUID']; + parentRowLevelPermissionPredicateGroupId?: InputMaybe; + positionInRowLevelPermissionPredicateGroup?: InputMaybe; +}; + export enum RowLevelPermissionPredicateGroupLogicalOperator { AND = 'AND', OR = 'OR' } +export type RowLevelPermissionPredicateInput = { + fieldMetadataId: Scalars['UUID']; + id?: InputMaybe; + operand: RowLevelPermissionPredicateOperand; + positionInRowLevelPermissionPredicateGroup?: InputMaybe; + rowLevelPermissionPredicateGroupId?: InputMaybe; + subFieldName?: InputMaybe; + value?: InputMaybe; + workspaceMemberFieldMetadataId?: InputMaybe; + workspaceMemberSubFieldName?: InputMaybe; +}; + export enum RowLevelPermissionPredicateOperand { CONTAINS = 'CONTAINS', DOES_NOT_CONTAIN = 'DOES_NOT_CONTAIN', @@ -4597,6 +4627,7 @@ export type UpdateRowLevelPermissionPredicateGroupInput = { }; export type UpdateRowLevelPermissionPredicateInput = { + fieldMetadataId?: InputMaybe; id: Scalars['String']; operand?: InputMaybe; positionInRowLevelPermissionPredicateGroup?: InputMaybe; @@ -4778,6 +4809,19 @@ export type UpsertPermissionFlagsInput = { roleId: Scalars['UUID']; }; +export type UpsertRowLevelPermissionPredicatesInput = { + objectMetadataId: Scalars['UUID']; + predicateGroups: Array; + predicates: Array; + roleId: Scalars['UUID']; +}; + +export type UpsertRowLevelPermissionPredicatesResult = { + __typename?: 'UpsertRowLevelPermissionPredicatesResult'; + predicateGroups: Array; + predicates: Array; +}; + export type User = { __typename?: 'User'; availableWorkspaces: AvailableWorkspaces; @@ -6096,6 +6140,10 @@ export type PermissionFlagFragmentFragment = { __typename?: 'PermissionFlag', id export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean }; +export type RowLevelPermissionPredicateFragmentFragment = { __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }; + +export type RowLevelPermissionPredicateGroupFragmentFragment = { __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }; + export type CreateOneRoleMutationVariables = Exact<{ createRoleInput: CreateRoleInput; }>; @@ -6146,10 +6194,17 @@ export type UpsertPermissionFlagsMutationVariables = Exact<{ export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPermissionFlags: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> }; +export type UpsertRowLevelPermissionPredicatesMutationVariables = Exact<{ + input: UpsertRowLevelPermissionPredicatesInput; +}>; + + +export type UpsertRowLevelPermissionPredicatesMutation = { __typename?: 'Mutation', upsertRowLevelPermissionPredicates: { __typename?: 'UpsertRowLevelPermissionPredicatesResult', predicates: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }>, predicateGroups: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> } }; + 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, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null }> }; +export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, apiKeys: Array<{ __typename?: 'ApiKeyForRole', id: string, name: string, expiresAt: string, revokedAt?: string | null }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, fieldPermissions?: Array<{ __typename?: 'FieldPermission', objectMetadataId: string, fieldMetadataId: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null, id: string, roleId: string }> | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> }; export type CreateApprovedAccessDomainMutationVariables = Exact<{ input: CreateApprovedAccessDomainInput; @@ -7081,6 +7136,31 @@ export const PermissionFlagFragmentFragmentDoc = gql` roleId } `; +export const RowLevelPermissionPredicateFragmentFragmentDoc = gql` + fragment RowLevelPermissionPredicateFragment on RowLevelPermissionPredicate { + id + fieldMetadataId + objectMetadataId + operand + subFieldName + workspaceMemberFieldMetadataId + workspaceMemberSubFieldName + rowLevelPermissionPredicateGroupId + positionInRowLevelPermissionPredicateGroup + roleId + value +} + `; +export const RowLevelPermissionPredicateGroupFragmentFragmentDoc = gql` + fragment RowLevelPermissionPredicateGroupFragment on RowLevelPermissionPredicateGroup { + id + parentRowLevelPermissionPredicateGroupId + logicalOperator + positionInRowLevelPermissionPredicateGroup + roleId + objectMetadataId +} + `; export const WorkspaceMemberQueryFragmentFragmentDoc = gql` fragment WorkspaceMemberQueryFragment on WorkspaceMember { id @@ -12109,6 +12189,45 @@ export function useUpsertPermissionFlagsMutation(baseOptions?: Apollo.MutationHo export type UpsertPermissionFlagsMutationHookResult = ReturnType; export type UpsertPermissionFlagsMutationResult = Apollo.MutationResult; export type UpsertPermissionFlagsMutationOptions = Apollo.BaseMutationOptions; +export const UpsertRowLevelPermissionPredicatesDocument = gql` + mutation UpsertRowLevelPermissionPredicates($input: UpsertRowLevelPermissionPredicatesInput!) { + upsertRowLevelPermissionPredicates(input: $input) { + predicates { + ...RowLevelPermissionPredicateFragment + } + predicateGroups { + ...RowLevelPermissionPredicateGroupFragment + } + } +} + ${RowLevelPermissionPredicateFragmentFragmentDoc} +${RowLevelPermissionPredicateGroupFragmentFragmentDoc}`; +export type UpsertRowLevelPermissionPredicatesMutationFn = Apollo.MutationFunction; + +/** + * __useUpsertRowLevelPermissionPredicatesMutation__ + * + * To run a mutation, you first call `useUpsertRowLevelPermissionPredicatesMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpsertRowLevelPermissionPredicatesMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [upsertRowLevelPermissionPredicatesMutation, { data, loading, error }] = useUpsertRowLevelPermissionPredicatesMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUpsertRowLevelPermissionPredicatesMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpsertRowLevelPermissionPredicatesDocument, options); + } +export type UpsertRowLevelPermissionPredicatesMutationHookResult = ReturnType; +export type UpsertRowLevelPermissionPredicatesMutationResult = Apollo.MutationResult; +export type UpsertRowLevelPermissionPredicatesMutationOptions = Apollo.BaseMutationOptions; export const GetRolesDocument = gql` query GetRoles { getRoles { @@ -12131,6 +12250,12 @@ export const GetRolesDocument = gql` fieldPermissions { ...FieldPermissionFragment } + rowLevelPermissionPredicates { + ...RowLevelPermissionPredicateFragment + } + rowLevelPermissionPredicateGroups { + ...RowLevelPermissionPredicateGroupFragment + } } } ${RoleFragmentFragmentDoc} @@ -12139,7 +12264,9 @@ ${AgentFieldsFragmentDoc} ${ApiKeyForRoleFragmentFragmentDoc} ${PermissionFlagFragmentFragmentDoc} ${ObjectPermissionFragmentFragmentDoc} -${FieldPermissionFragmentFragmentDoc}`; +${FieldPermissionFragmentFragmentDoc} +${RowLevelPermissionPredicateFragmentFragmentDoc} +${RowLevelPermissionPredicateGroupFragmentFragmentDoc}`; /** * __useGetRolesQuery__ diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index dd9dba3174..1658a874b9 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -941,6 +941,7 @@ export type CreateRouteTriggerInput = { export type CreateRowLevelPermissionPredicateGroupInput = { logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: Scalars['String']; parentRowLevelPermissionPredicateGroupId?: InputMaybe; positionInRowLevelPermissionPredicateGroup?: InputMaybe; roleId: Scalars['String']; @@ -2003,6 +2004,7 @@ export type Mutation = { upsertFieldPermissions: Array; upsertObjectPermissions: Array; upsertPermissionFlags: Array; + upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; verifyEmailAndGetLoginToken: VerifyEmailAndGetLoginTokenOutput; @@ -2876,6 +2878,11 @@ export type MutationUpsertPermissionFlagsArgs = { }; +export type MutationUpsertRowLevelPermissionPredicatesArgs = { + input: UpsertRowLevelPermissionPredicatesInput; +}; + + export type MutationUserLookupAdminPanelArgs = { userIdentifier: Scalars['String']; }; @@ -3766,6 +3773,8 @@ export type Role = { label: Scalars['String']; objectPermissions?: Maybe>; permissionFlags?: Maybe>; + rowLevelPermissionPredicateGroups?: Maybe>; + rowLevelPermissionPredicates?: Maybe>; standardId?: Maybe; universalIdentifier?: Maybe; workspaceMembers: Array; @@ -3804,16 +3813,37 @@ export type RowLevelPermissionPredicateGroup = { __typename?: 'RowLevelPermissionPredicateGroup'; id: Scalars['String']; logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: Scalars['String']; parentRowLevelPermissionPredicateGroupId?: Maybe; positionInRowLevelPermissionPredicateGroup?: Maybe; roleId: Scalars['String']; }; +export type RowLevelPermissionPredicateGroupInput = { + id?: InputMaybe; + logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + objectMetadataId: Scalars['UUID']; + parentRowLevelPermissionPredicateGroupId?: InputMaybe; + positionInRowLevelPermissionPredicateGroup?: InputMaybe; +}; + export enum RowLevelPermissionPredicateGroupLogicalOperator { AND = 'AND', OR = 'OR' } +export type RowLevelPermissionPredicateInput = { + fieldMetadataId: Scalars['UUID']; + id?: InputMaybe; + operand: RowLevelPermissionPredicateOperand; + positionInRowLevelPermissionPredicateGroup?: InputMaybe; + rowLevelPermissionPredicateGroupId?: InputMaybe; + subFieldName?: InputMaybe; + value?: InputMaybe; + workspaceMemberFieldMetadataId?: InputMaybe; + workspaceMemberSubFieldName?: InputMaybe; +}; + export enum RowLevelPermissionPredicateOperand { CONTAINS = 'CONTAINS', DOES_NOT_CONTAIN = 'DOES_NOT_CONTAIN', @@ -4472,6 +4502,7 @@ export type UpdateRowLevelPermissionPredicateGroupInput = { }; export type UpdateRowLevelPermissionPredicateInput = { + fieldMetadataId?: InputMaybe; id: Scalars['String']; operand?: InputMaybe; positionInRowLevelPermissionPredicateGroup?: InputMaybe; @@ -4643,6 +4674,19 @@ export type UpsertPermissionFlagsInput = { roleId: Scalars['UUID']; }; +export type UpsertRowLevelPermissionPredicatesInput = { + objectMetadataId: Scalars['UUID']; + predicateGroups: Array; + predicates: Array; + roleId: Scalars['UUID']; +}; + +export type UpsertRowLevelPermissionPredicatesResult = { + __typename?: 'UpsertRowLevelPermissionPredicatesResult'; + predicateGroups: Array; + predicates: Array; +}; + export type User = { __typename?: 'User'; availableWorkspaces: AvailableWorkspaces; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx index a2db7eb158..c854e2caa2 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect.tsx @@ -15,9 +15,9 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; +import { t } from '@lingui/core/macro'; import { useContext } from 'react'; import { RecordFilterGroupLogicalOperator } from 'twenty-shared/types'; -import { t } from '@lingui/core/macro'; import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils'; import { IconLibraryPlus, IconPlus } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldType.ts new file mode 100644 index 0000000000..a83b147b67 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldType.ts @@ -0,0 +1,23 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { + compositeTypeDefinitions, + type FieldMetadataType, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const getCompositeSubFieldType = ( + fieldMetadataItem: FieldMetadataItem, + subFieldName: string, +): FieldMetadataType | null => { + const compositeType = compositeTypeDefinitions.get(fieldMetadataItem.type); + + if (!isDefined(compositeType)) { + return null; + } + + const subFieldProperty = compositeType.properties.find( + (property) => property.name === subFieldName, + ); + + return subFieldProperty?.type ?? null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx index a1ee2bd032..c876bf1313 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput.tsx @@ -37,6 +37,7 @@ type FormMultiSelectFieldInputProps = { placeholder?: string; testId?: string; hint?: string; + dropdownWidth?: number; }; const StyledDisplayModeReadonlyContainer = styled.div` @@ -86,6 +87,7 @@ export const FormMultiSelectFieldInput = ({ placeholder, testId, hint, + dropdownWidth, }: FormMultiSelectFieldInputProps) => { const instanceId = useId(); const theme = useTheme(); @@ -269,7 +271,9 @@ export const FormMultiSelectFieldInput = ({ onCancel={onCancel} onOptionSelected={onOptionSelected} values={selectedNames} - dropdownWidth={GenericDropdownContentWidth.ExtraLarge} + dropdownWidth={ + dropdownWidth ?? GenericDropdownContentWidth.ExtraLarge + } /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts index ae9da12e4e..74d59b6345 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts @@ -5,6 +5,12 @@ import { type ViewFilterOperand, } from 'twenty-shared/types'; +// RLS-specific: references a workspace member field for dynamic "Me" comparisons +export type RLSDynamicValue = { + workspaceMemberFieldMetadataId: string; + workspaceMemberSubFieldName?: string | null; +}; + export type RecordFilter = { id: string; fieldMetadataId: string; @@ -18,6 +24,8 @@ export type RecordFilter = { positionInRecordFilterGroup?: number | null; label: string; subFieldName?: CompositeFieldSubFieldName | null | undefined; + // RLS-specific: when set, filter compares against current user's field value + rlsDynamicValue?: RLSDynamicValue | null; }; export type RecordFilterToRecordInputOperand< diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment.ts new file mode 100644 index 0000000000..f2e3de444d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment.ts @@ -0,0 +1,19 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT = gql` + fragment RowLevelPermissionPredicateFragment on RowLevelPermissionPredicate { + id + fieldMetadataId + objectMetadataId + operand + subFieldName + workspaceMemberFieldMetadataId + workspaceMemberSubFieldName + rowLevelPermissionPredicateGroupId + positionInRowLevelPermissionPredicateGroup + roleId + value + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment.ts new file mode 100644 index 0000000000..f338b88031 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment.ts @@ -0,0 +1,14 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT = gql` + fragment RowLevelPermissionPredicateGroupFragment on RowLevelPermissionPredicateGroup { + id + parentRowLevelPermissionPredicateGroupId + logicalOperator + positionInRowLevelPermissionPredicateGroup + roleId + objectMetadataId + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/hooks/useUpsertRowLevelPermissionPredicatesMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/hooks/useUpsertRowLevelPermissionPredicatesMutation.ts new file mode 100644 index 0000000000..f6330a2136 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/hooks/useUpsertRowLevelPermissionPredicatesMutation.ts @@ -0,0 +1,48 @@ +/* @license Enterprise */ + +import { useMutation } from '@apollo/client'; + +import { UPSERT_ROW_LEVEL_PERMISSION_PREDICATES } from '@/settings/roles/graphql/mutations/upsertRowLevelPermissionPredicatesMutation'; +import { + type RowLevelPermissionPredicate, + type RowLevelPermissionPredicateGroup, + type RowLevelPermissionPredicateGroupLogicalOperator, + type RowLevelPermissionPredicateOperand, +} from '~/generated-metadata/graphql'; + +export type UpsertRowLevelPermissionPredicatesInput = { + roleId: string; + objectMetadataId: string; + predicates: Array<{ + id?: string; + fieldMetadataId: string; + operand: RowLevelPermissionPredicateOperand; + value?: unknown; + subFieldName?: string | null; + workspaceMemberFieldMetadataId?: string | null; + workspaceMemberSubFieldName?: string | null; + rowLevelPermissionPredicateGroupId?: string | null; + positionInRowLevelPermissionPredicateGroup?: number | null; + }>; + predicateGroups: Array<{ + id?: string; + objectMetadataId: string; + parentRowLevelPermissionPredicateGroupId?: string | null; + logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator; + positionInRowLevelPermissionPredicateGroup?: number | null; + }>; +}; + +type UpsertRowLevelPermissionPredicatesResult = { + upsertRowLevelPermissionPredicates: { + predicates: RowLevelPermissionPredicate[]; + predicateGroups: RowLevelPermissionPredicateGroup[]; + }; +}; + +export const useUpsertRowLevelPermissionPredicatesMutation = () => { + return useMutation< + UpsertRowLevelPermissionPredicatesResult, + { input: UpsertRowLevelPermissionPredicatesInput } + >(UPSERT_ROW_LEVEL_PERMISSION_PREDICATES); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertRowLevelPermissionPredicatesMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertRowLevelPermissionPredicatesMutation.ts new file mode 100644 index 0000000000..6ef2ab375f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertRowLevelPermissionPredicatesMutation.ts @@ -0,0 +1,22 @@ +/* @license Enterprise */ + +import { ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment'; +import { ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment'; +import { gql } from '@apollo/client'; + +export const UPSERT_ROW_LEVEL_PERMISSION_PREDICATES = gql` + ${ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT} + ${ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT} + mutation UpsertRowLevelPermissionPredicates( + $input: UpsertRowLevelPermissionPredicatesInput! + ) { + upsertRowLevelPermissionPredicates(input: $input) { + predicates { + ...RowLevelPermissionPredicateFragment + } + predicateGroups { + ...RowLevelPermissionPredicateGroupFragment + } + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts b/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts index d285fe2ab8..4fe85d8604 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts @@ -4,6 +4,8 @@ import { FIELD_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/fi import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment'; import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment'; import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; +import { ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateFragment'; +import { ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT } from '@/settings/roles/graphql/fragments/rowLevelPermissionPredicateGroupFragment'; import { PARTIAL_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/partialWorkspaceMemberQueryFragment'; import { gql } from '@apollo/client'; @@ -15,6 +17,8 @@ export const GET_ROLES = gql` ${PERMISSION_FLAG_FRAGMENT} ${OBJECT_PERMISSION_FRAGMENT} ${FIELD_PERMISSION_FRAGMENT} + ${ROW_LEVEL_PERMISSION_PREDICATE_FRAGMENT} + ${ROW_LEVEL_PERMISSION_PREDICATE_GROUP_FRAGMENT} query GetRoles { getRoles { ...RoleFragment @@ -36,6 +40,12 @@ export const GET_ROLES = gql` fieldPermissions { ...FieldPermissionFragment } + rowLevelPermissionPredicates { + ...RowLevelPermissionPredicateFragment + } + rowLevelPermissionPredicateGroups { + ...RowLevelPermissionPredicateGroupFragment + } } } `; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx index d9172853be..c0cd3ffe31 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm.tsx @@ -1,3 +1,4 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsRolePermissionsObjectLevelObjectFieldPermissionTable } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/components/SettingsRolePermissionsObjectLevelObjectFieldPermissionTable'; @@ -29,6 +30,9 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ const [searchParams] = useSearchParams(); const fromAgentId = searchParams.get('fromAgent'); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const hasValidEnterpriseKey = + currentWorkspace?.hasValidEnterpriseKey === true; const settingsDraftRole = useRecoilValue( settingsDraftRoleFamilyState(roleId), ); @@ -44,7 +48,9 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ const featureFlagsMap = useFeatureFlagsMap(); const isRowLevelPermissionPredicatesEnabled = - featureFlagsMap[FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED]; + featureFlagsMap[ + FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED + ] && hasValidEnterpriseKey; const objectMetadataItem = objectMetadata.objectMetadataItem; @@ -123,7 +129,10 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({ roleId={roleId} /> {isRowLevelPermissionPredicatesEnabled && ( - + )} diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelect.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelect.tsx new file mode 100644 index 0000000000..221473162d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelect.tsx @@ -0,0 +1,60 @@ +/* @license Enterprise */ + +import styled from '@emotion/styled'; + +import { AdvancedFilterFieldSelectDropdownButtonClickableSelect } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButtonClickableSelect'; +import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset'; +import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; +import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; +import { SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu'; +import { SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; + +const StyledContainer = styled.div` + flex: 2; + min-width: 0; +`; + +type SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectProps = { + recordFilterId: string; +}; + +export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelect = + ({ + recordFilterId, + }: SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectProps) => { + const { advancedFilterFieldSelectDropdownId } = + useAdvancedFilterFieldSelectDropdown(recordFilterId); + + const [objectFilterDropdownIsSelectingCompositeField] = + useRecoilComponentState( + objectFilterDropdownIsSelectingCompositeFieldComponentState, + ); + + return ( + + + } + dropdownComponents={ + objectFilterDropdownIsSelectingCompositeField ? ( + + ) : ( + + ) + } + dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET} + dropdownPlacement="bottom-start" + /> + + ); + }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx new file mode 100644 index 0000000000..1bc73c1b7c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu.tsx @@ -0,0 +1,137 @@ +/* @license Enterprise */ + +import { useLingui } from '@lingui/react/macro'; +import { getFilterTypeFromFieldType } from 'twenty-shared/utils'; + +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { AdvancedFilterFieldSelectSearchInput } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectSearchInput'; +import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; +import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown'; +import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; +import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; +import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; +import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType'; +import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useContext } from 'react'; + +type SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenuProps = + { + recordFilterId: string; + }; + +export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenu = + ({ + recordFilterId, + }: SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectFieldMenuProps) => { + const { t } = useLingui(); + + const { + closeAdvancedFilterFieldSelectDropdown, + advancedFilterFieldSelectDropdownId, + } = useAdvancedFilterFieldSelectDropdown(recordFilterId); + + const [objectFilterDropdownSearchInput] = useRecoilComponentState( + objectFilterDropdownSearchInputComponentState, + ); + + const { objectMetadataItem } = useContext(AdvancedFilterContext); + + const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems( + objectMetadataItem.id, + ); + + const filteredFieldMetadataItems = filterableFieldMetadataItems + .filter((fieldMetadataItem) => + fieldMetadataItem.label + .toLocaleLowerCase() + .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), + ) + .sort((a, b) => a.label.localeCompare(b.label)); + + const { resetSelectedItem } = useSelectableList( + advancedFilterFieldSelectDropdownId, + ); + + const { selectFieldUsedInAdvancedFilterDropdown } = + useSelectFieldUsedInAdvancedFilterDropdown(); + + const [, setObjectFilterDropdownSubMenuFieldType] = useRecoilComponentState( + objectFilterDropdownSubMenuFieldTypeComponentState, + ); + + const [, setObjectFilterDropdownIsSelectingCompositeField] = + useRecoilComponentState( + objectFilterDropdownIsSelectingCompositeFieldComponentState, + ); + + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentState( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + + const handleFieldSelect = ( + selectedFieldMetadataItem: FieldMetadataItem, + ) => { + resetSelectedItem(); + + const filterType = getFilterTypeFromFieldType( + selectedFieldMetadataItem.type, + ); + + if (isCompositeFieldType(filterType)) { + setObjectFilterDropdownSubMenuFieldType(filterType); + setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id); + setObjectFilterDropdownIsSelectingCompositeField(true); + } else { + selectFieldUsedInAdvancedFilterDropdown({ + fieldMetadataItemId: selectedFieldMetadataItem.id, + recordFilterId, + }); + closeAdvancedFilterFieldSelectDropdown(); + } + }; + + const selectableItemIdArray = filteredFieldMetadataItems.map( + (fieldMetadataItem) => fieldMetadataItem.id, + ); + + return ( + + + + + + {filteredFieldMetadataItems.map((fieldMetadataItem) => ( + { + handleFieldSelect(fieldMetadataItem); + }} + > + + + ))} + + + + ); + }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx new file mode 100644 index 0000000000..5818bd0fc4 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu.tsx @@ -0,0 +1,185 @@ +/* @license Enterprise */ + +import { + compositeTypeDefinitions, + FieldMetadataType, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { IconChevronLeft, useIcons } from 'twenty-ui/display'; +import { MenuItem } from 'twenty-ui/navigation'; + +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown'; +import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; +import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; +import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; +import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; +import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField'; +import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable'; +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; +import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; + +type SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenuProps = + { + recordFilterId: string; + }; + +export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenu = + ({ + recordFilterId, + }: SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectSubFieldMenuProps) => { + const { getIcon } = useIcons(); + + const fieldMetadataItemUsedInDropdown = useRecoilComponentValue( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + + const [, setObjectFilterDropdownIsSelectingCompositeField] = + useRecoilComponentState( + objectFilterDropdownIsSelectingCompositeFieldComponentState, + ); + + const [ + objectFilterDropdownSubMenuFieldType, + setObjectFilterDropdownSubMenuFieldType, + ] = useRecoilComponentState( + objectFilterDropdownSubMenuFieldTypeComponentState, + ); + + const { closeAdvancedFilterFieldSelectDropdown } = + useAdvancedFilterFieldSelectDropdown(recordFilterId); + + const { selectFieldUsedInAdvancedFilterDropdown } = + useSelectFieldUsedInAdvancedFilterDropdown(); + + const handleSelectFilter = ( + selectedFieldMetadataItem: FieldMetadataItem | null | undefined, + subFieldName?: CompositeFieldSubFieldName | null | undefined, + ) => { + if (!isDefined(selectedFieldMetadataItem)) { + return; + } + + selectFieldUsedInAdvancedFilterDropdown({ + fieldMetadataItemId: selectedFieldMetadataItem.id, + recordFilterId, + subFieldName, + }); + + closeAdvancedFilterFieldSelectDropdown(); + }; + + const handleSubMenuBack = () => { + setObjectFilterDropdownSubMenuFieldType(null); + setObjectFilterDropdownIsSelectingCompositeField(false); + }; + + const { advancedFilterFieldSelectDropdownId } = + useAdvancedFilterFieldSelectDropdown(recordFilterId); + + const selectedItemId = useRecoilComponentValue( + selectedItemIdComponentState, + advancedFilterFieldSelectDropdownId, + ); + + if (!isDefined(objectFilterDropdownSubMenuFieldType)) { + return null; + } + + const compositeType = compositeTypeDefinitions.get( + objectFilterDropdownSubMenuFieldType as FieldMetadataType, + ); + + const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ + objectFilterDropdownSubMenuFieldType + ].subFields + .filter((subField) => { + if (!subField.isFilterable) { + return false; + } + + const subFieldProperty = compositeType?.properties.find( + (property) => property.name === subField.subFieldName, + ); + + return subFieldProperty?.type !== FieldMetadataType.RAW_JSON; + }) + .map((subField) => subField.subFieldName); + + const subFieldsAreFilterable = + isDefined(fieldMetadataItemUsedInDropdown) && + areCompositeTypeSubFieldsFilterable(fieldMetadataItemUsedInDropdown.type); + + const selectableItemIdArray = subFieldNames.map( + (subFieldName) => subFieldName, + ); + + return ( + + + } + > + {fieldMetadataItemUsedInDropdown?.label} + + + + {subFieldsAreFilterable && + subFieldNames.map((subFieldName, index) => ( + { + handleSelectFilter( + fieldMetadataItemUsedInDropdown, + subFieldName, + ); + }} + > + { + if (isDefined(fieldMetadataItemUsedInDropdown)) { + handleSelectFilter( + fieldMetadataItemUsedInDropdown, + subFieldName, + ); + } + }} + text={getCompositeSubFieldLabel( + objectFilterDropdownSubMenuFieldType, + subFieldName, + )} + LeftIcon={getIcon( + ICON_NAME_BY_SUB_FIELD[subFieldName] ?? + fieldMetadataItemUsedInDropdown?.icon, + )} + /> + + ))} + + + + ); + }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilder.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilder.tsx new file mode 100644 index 0000000000..e21daa6b69 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilder.tsx @@ -0,0 +1,33 @@ +/* @license Enterprise */ + +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; +import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContent } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContent'; + +type SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderProps = + { + roleId: string; + objectMetadataItem: ObjectMetadataItem; + }; + +export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilder = + ({ + roleId, + objectMetadataItem, + }: SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderProps) => { + const instanceId = `record-level-permission-filter-${roleId}-${objectMetadataItem.id}`; + + return ( + + + + + + ); + }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContent.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContent.tsx new file mode 100644 index 0000000000..7f91d53930 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContent.tsx @@ -0,0 +1,161 @@ +/* @license Enterprise */ + +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { IconFilter, IconPlus } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; + +import { ActionButton } from '@/action-menu/actions/display/components/ActionButton'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups'; +import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow'; +import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext'; +import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector'; +import { isRecordFilterGroupChildARecordFilterGroup } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilterGroup'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRow } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterRow'; +import { useRecordLevelPermissionFilterActions } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/hooks/useRecordLevelPermissionFilterActions'; +import { useRecordLevelPermissionFilterInitialization } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/hooks/useRecordLevelPermissionFilterInitialization'; +import { useRecordLevelPermissionSyncToDraftRole } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/hooks/useRecordLevelPermissionSyncToDraftRole'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; + +const StyledContainer = styled.div` + align-items: start; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledFiltersContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledActionButtonWrapper = styled.div` + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +type SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContentProps = + { + roleId: string; + objectMetadataItem: ObjectMetadataItem; + }; + +export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContent = + ({ + roleId, + objectMetadataItem, + }: SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilderContentProps) => { + const settingsDraftRole = useRecoilValue( + settingsDraftRoleFamilyState(roleId), + ); + + const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems( + objectMetadataItem.id, + ); + + const setCurrentRecordFilters = useSetRecoilComponentState( + currentRecordFiltersComponentState, + ); + + const setCurrentRecordFilterGroups = useSetRecoilComponentState( + currentRecordFilterGroupsComponentState, + ); + + const currentRecordFilters = useRecoilComponentValue( + currentRecordFiltersComponentState, + ); + + const currentRecordFilterGroups = useRecoilComponentValue( + currentRecordFilterGroupsComponentState, + ); + + const { setRecordFilterUsedInAdvancedFilterDropdownRow } = + useSetRecordFilterUsedInAdvancedFilterDropdownRow(); + + const rootRecordFilterGroup = useRecoilComponentValue( + rootLevelRecordFilterGroupComponentSelector, + ); + + const { childRecordFiltersAndRecordFilterGroups } = + useChildRecordFiltersAndRecordFilterGroups({ + recordFilterGroupId: rootRecordFilterGroup?.id, + }); + + const { hasInitialized } = useRecordLevelPermissionFilterInitialization({ + roleId, + objectMetadataItem, + settingsDraftRole, + filterableFieldMetadataItems, + setCurrentRecordFilters, + setCurrentRecordFilterGroups, + setRecordFilterUsedInAdvancedFilterDropdownRow, + }); + + useRecordLevelPermissionSyncToDraftRole({ + roleId, + objectMetadataItem, + currentRecordFilters, + currentRecordFilterGroups, + hasInitialized, + }); + + const { handleCreateFirstFilter, handleAddFilter } = + useRecordLevelPermissionFilterActions({ + objectMetadataItem, + }); + + return ( + + {isDefined(rootRecordFilterGroup) ? ( + + + {childRecordFiltersAndRecordFilterGroups + .filter( + (child): child is RecordFilter => + !isRecordFilterGroupChildARecordFilterGroup(child), + ) + .map((child, index) => ( + + ))} + + + handleAddFilter(rootRecordFilterGroup)} + /> + + + ) : ( +