Add upgrade to org plan card to RLS (#17455)

## Context

<img width="564" height="270" alt="Screenshot 2026-01-26 at 18 19 39"
src="https://github.com/user-attachments/assets/9a6bdd69-2fa1-449b-9985-bfc7a401780e"
/>
This commit is contained in:
Weiko
2026-01-27 12:51:08 +01:00
committed by GitHub
parent d6a7dbb12b
commit c36d112ab4
4 changed files with 119 additions and 10 deletions
@@ -0,0 +1,46 @@
import {
StyledSettingsOptionCardContent,
StyledSettingsOptionCardDescription,
StyledSettingsOptionCardIcon,
StyledSettingsOptionCardTitle,
} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase';
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
import styled from '@emotion/styled';
import { type IconComponent } from 'twenty-ui/display';
type SettingsOptionCardContentButtonProps = {
Icon?: IconComponent;
title: React.ReactNode;
description?: string | React.ReactNode;
disabled?: boolean;
Button?: React.ReactNode;
};
const StyledButtonContainer = styled.div`
margin-left: auto;
`;
export const SettingsOptionCardContentButton = ({
Icon,
title,
description,
disabled = false,
Button,
}: SettingsOptionCardContentButtonProps) => {
return (
<StyledSettingsOptionCardContent disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>{title}</StyledSettingsOptionCardTitle>
<StyledSettingsOptionCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</div>
{Button && <StyledButtonContainer>{Button}</StyledButtonContainer>}
</StyledSettingsOptionCardContent>
);
};
@@ -60,9 +60,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
) ?? false;
const isRowLevelPermissionPredicatesEnabled =
featureFlagsMap[
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
] && isRLSBillingEntitlementEnabled;
featureFlagsMap[FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED];
const objectMetadataItem = objectMetadata.objectMetadataItem;
@@ -170,6 +168,7 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
<SettingsRolePermissionsObjectLevelRecordLevelSection
objectMetadataItem={objectMetadataItem}
roleId={roleId}
hasOrganizationPlan={isRLSBillingEntitlementEnabled}
/>
)}
</SettingsPageContainer>
@@ -2,26 +2,84 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { Pill } from 'twenty-ui/components';
import { H2Title, IconArrowUp, IconLock } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import { billingState } from '@/client-config/states/billingState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
import { SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilder } from '@/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelPermissionFilterBuilder';
import { useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { Button } from 'twenty-ui/input';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledContent = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(4)};
`;
const StyledCard = styled(Card)`
margin-top: ${({ theme }) => theme.spacing(4)};
overflow: hidden;
`;
const StyledPill = styled(Pill)`
border-radius: 40px;
border: 1px solid ${({ theme }) => theme.border.color.light};
background: ${({ theme }) => theme.background.secondary};
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
`;
type SettingsRolePermissionsObjectLevelRecordLevelSectionProps = {
objectMetadataItem: ObjectMetadataItem;
roleId: string;
hasOrganizationPlan: boolean;
};
export const SettingsRolePermissionsObjectLevelRecordLevelSection = ({
objectMetadataItem,
roleId,
hasOrganizationPlan,
}: SettingsRolePermissionsObjectLevelRecordLevelSectionProps) => {
const navigateSettings = useNavigateSettings();
const billing = useRecoilValue(billingState);
const isBillingEnabled = billing?.isBillingEnabled ?? false;
if (!hasOrganizationPlan) {
return (
<Section>
<H2Title
title={t`Record-level`}
description={t`Ability to filter the records a user can interact with`}
adornment={<StyledPill label={t`Organization`} Icon={IconLock} />}
/>
<StyledCard rounded>
<SettingsOptionCardContentButton
Icon={IconLock}
title={t`Upgrade to access`}
description={t`This feature is part of the Organization Plan`}
Button={
isBillingEnabled && (
<Button
title={t`Upgrade`}
variant="primary"
accent="blue"
size="small"
Icon={IconArrowUp}
onClick={() => navigateSettings(SettingsPath.Billing)}
/>
)
}
/>
</StyledCard>
</Section>
);
}
return (
<Section>
<H2Title
@@ -1,9 +1,10 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { type IconComponent } from '@ui/display';
type PillProps = {
className?: string;
label?: string;
Icon?: IconComponent;
};
const StyledPill = styled.span`
@@ -11,17 +12,22 @@ const StyledPill = styled.span`
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.border.radius.pill};
color: ${({ theme }) => theme.font.color.light};
display: inline-block;
display: inline-flex;
font-size: ${({ theme }) => theme.font.size.xs};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(2)};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(4)};
justify-content: flex-end;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
padding: ${({ theme }) => `0 ${theme.spacing(2)}`};
`;
export const Pill = ({ className, label }: PillProps) => {
return <StyledPill className={className}>{label}</StyledPill>;
export const Pill = ({ className, label, Icon }: PillProps) => {
return (
<StyledPill className={className}>
{Icon && <Icon size={12} />}
{label}
</StyledPill>
);
};