Add application installation validation modale (#20907)
## After <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/231d4f0d-6052-4c4e-9a2a-0244d2b3832e" />
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Avatar, IconRefresh } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type AppConnectionHeaderProps = {
|
||||
appLogoUrl?: string | null;
|
||||
appName: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledAppLogoTile = styled.div`
|
||||
align-items: center;
|
||||
backdrop-filter: ${themeCssVariables.blur.strong};
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[12]};
|
||||
justify-content: center;
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
width: ${themeCssVariables.spacing[12]};
|
||||
`;
|
||||
|
||||
const StyledAppLogo = styled.img`
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
height: ${themeCssVariables.spacing[10]};
|
||||
object-fit: cover;
|
||||
width: ${themeCssVariables.spacing[10]};
|
||||
`;
|
||||
|
||||
const StyledLinkIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.rounded};
|
||||
box-shadow: ${themeCssVariables.boxShadow.strong};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
justify-content: center;
|
||||
width: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
export const AppConnectionHeader = ({
|
||||
appLogoUrl,
|
||||
appName,
|
||||
}: AppConnectionHeaderProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const [hasAppLogoError, setHasAppLogoError] = useState(false);
|
||||
|
||||
const showAppLogoImage = isNonEmptyString(appLogoUrl) && !hasAppLogoError;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledAppLogoTile>
|
||||
<StyledAppLogo src={'/images/integrations/twenty-logo.svg'} alt="" />
|
||||
</StyledAppLogoTile>
|
||||
<StyledLinkIconContainer aria-hidden>
|
||||
<IconRefresh size={theme.icon.size.md} stroke={theme.icon.stroke.lg} />
|
||||
</StyledLinkIconContainer>
|
||||
<StyledAppLogoTile>
|
||||
{showAppLogoImage ? (
|
||||
<StyledAppLogo
|
||||
src={appLogoUrl}
|
||||
alt=""
|
||||
onError={() => setHasAppLogoError(true)}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
size="xl"
|
||||
placeholder={appName}
|
||||
placeholderColorSeed={appName}
|
||||
type="squared"
|
||||
/>
|
||||
)}
|
||||
</StyledAppLogoTile>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type AuthorizeActionButtonsProps = {
|
||||
onAuthorize: () => void;
|
||||
onCancel: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: grid;
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
grid-template-columns: repeat(
|
||||
2,
|
||||
minmax(${themeCssVariables.spacing[0]}, 1fr)
|
||||
);
|
||||
margin-top: ${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAuthorizeButton = styled(MainButton)`
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
const StyledCancelButton = styled(MainButton)`
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
export const AuthorizeActionButtons = ({
|
||||
onAuthorize,
|
||||
onCancel,
|
||||
isLoading,
|
||||
}: AuthorizeActionButtonsProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<StyledCancelButton
|
||||
title={t`Cancel`}
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<StyledAuthorizeButton
|
||||
title={isLoading ? t`Authorizing...` : t`Authorize`}
|
||||
onClick={onAuthorize}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
};
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { AppConnectionHeader } from '@/applications/components/AppConnectionHeader';
|
||||
import { AuthorizeActionButtons } from '@/applications/components/AuthorizeActionButtons';
|
||||
import {
|
||||
buildPermissionSummaryFromRoleManifest,
|
||||
type PermissionSummaryItem,
|
||||
} from '@/marketplace/utils/buildPermissionSummaryFromRoleManifest';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { LightButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { StyledAppModal } from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
|
||||
type SettingsApplicationInstallPermissionValidationModalProps = {
|
||||
modalInstanceId: string;
|
||||
appDisplayName: string;
|
||||
appLogoUrl?: string;
|
||||
defaultRole?: RoleManifest;
|
||||
onAuthorize: () => void;
|
||||
isInstalling?: boolean;
|
||||
};
|
||||
|
||||
const StyledFullscreenContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLightButton = styled(LightButton)`
|
||||
left: ${themeCssVariables.spacing[4]};
|
||||
position: absolute;
|
||||
top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAppConnectionHeaderContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledPermissionsCard = styled.div`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledPermissionsTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
margin-bottom: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledPermissionRow = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
`;
|
||||
|
||||
const StyledPermissionIcon = styled.div`
|
||||
color: ${themeCssVariables.color.blue9};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationInstallPermissionValidationModal = ({
|
||||
modalInstanceId,
|
||||
appDisplayName,
|
||||
appLogoUrl,
|
||||
defaultRole,
|
||||
onAuthorize,
|
||||
isInstalling,
|
||||
}: SettingsApplicationInstallPermissionValidationModalProps) => {
|
||||
const { closeModal } = useModal();
|
||||
|
||||
const permissionItems: PermissionSummaryItem[] = useMemo(() => {
|
||||
if (!defaultRole) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buildPermissionSummaryFromRoleManifest(defaultRole);
|
||||
}, [defaultRole]);
|
||||
|
||||
const handleAuthorize = () => {
|
||||
closeModal(modalInstanceId);
|
||||
onAuthorize();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
closeModal(modalInstanceId);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAppModal
|
||||
modalId={modalInstanceId}
|
||||
isClosable
|
||||
onClose={handleClose}
|
||||
size="fullscreen"
|
||||
padding="none"
|
||||
overlay="transparent"
|
||||
>
|
||||
<StyledFullscreenContainer>
|
||||
<StyledLightButton
|
||||
Icon={IconChevronLeft}
|
||||
title={t`Back to settings`}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<StyledContent>
|
||||
<StyledAppConnectionHeaderContainer>
|
||||
<AppConnectionHeader
|
||||
appLogoUrl={appLogoUrl}
|
||||
appName={appDisplayName}
|
||||
/>
|
||||
</StyledAppConnectionHeaderContainer>
|
||||
|
||||
<StyledTitle>
|
||||
{t`Install ${appDisplayName} on your workspace`}
|
||||
</StyledTitle>
|
||||
|
||||
{permissionItems.length > 0 && (
|
||||
<StyledPermissionsCard>
|
||||
<StyledPermissionsTitle>
|
||||
{t`${appDisplayName} would like to:`}
|
||||
</StyledPermissionsTitle>
|
||||
{permissionItems.map((item) => (
|
||||
<StyledPermissionRow key={item.label}>
|
||||
<StyledPermissionIcon>
|
||||
<item.Icon size={16} />
|
||||
</StyledPermissionIcon>
|
||||
{item.label}
|
||||
</StyledPermissionRow>
|
||||
))}
|
||||
</StyledPermissionsCard>
|
||||
)}
|
||||
|
||||
<AuthorizeActionButtons
|
||||
onCancel={handleClose}
|
||||
onAuthorize={handleAuthorize}
|
||||
isLoading={isInstalling}
|
||||
/>
|
||||
</StyledContent>
|
||||
</StyledFullscreenContainer>
|
||||
</StyledAppModal>
|
||||
);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useInstallMarketplaceApp } from '@/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const INSTALL_PERMISSION_VALIDATION_MODAL_ID =
|
||||
'install-permission-validation-modal';
|
||||
|
||||
export const useInstallMarketplaceAppWithPermissionValidation = () => {
|
||||
const { openModal } = useModal();
|
||||
const { install, isInstalling } = useInstallMarketplaceApp();
|
||||
|
||||
const requestInstall = useCallback(() => {
|
||||
openModal(INSTALL_PERMISSION_VALIDATION_MODAL_ID);
|
||||
}, [openModal]);
|
||||
|
||||
return {
|
||||
requestInstall,
|
||||
install,
|
||||
isInstalling,
|
||||
modalInstanceId: INSTALL_PERMISSION_VALIDATION_MODAL_ID,
|
||||
};
|
||||
};
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
import { buildPermissionSummaryFromRoleManifest } from '@/marketplace/utils/buildPermissionSummaryFromRoleManifest';
|
||||
|
||||
const baseRole: RoleManifest = {
|
||||
universalIdentifier: 'test-role',
|
||||
label: 'Test Role',
|
||||
};
|
||||
|
||||
describe('buildPermissionSummaryFromRoleManifest', () => {
|
||||
it('should return empty array when role has no permissions', () => {
|
||||
const result = buildPermissionSummaryFromRoleManifest(baseRole);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
describe('record permissions', () => {
|
||||
it('should return "Read records" for read-only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read records');
|
||||
});
|
||||
|
||||
it('should return "Write records" for write-only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canUpdateAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Write records');
|
||||
});
|
||||
|
||||
it('should return "Read and write records" for read+write', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read and write records');
|
||||
});
|
||||
|
||||
it('should return "Delete records" for soft-delete only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Delete records');
|
||||
});
|
||||
|
||||
it('should return "Delete records" for destroy only', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Delete records');
|
||||
});
|
||||
|
||||
it('should return "Read and delete records" for read+delete', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read and delete records');
|
||||
});
|
||||
|
||||
it('should return "Read, write, and delete records" with Oxford comma for all three', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read, write, and delete records');
|
||||
});
|
||||
|
||||
it('should treat destroy the same as soft-delete for label purposes', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result[0].label).toBe('Read, write, and delete records');
|
||||
});
|
||||
});
|
||||
|
||||
describe('object permissions fallback', () => {
|
||||
it('should show "Access specific object records" when objectPermissions exist but no global record flags', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
objectPermissions: [
|
||||
{
|
||||
universalIdentifier: 'perm-1',
|
||||
objectUniversalIdentifier: 'obj-1',
|
||||
canReadObjectRecords: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Access specific object records');
|
||||
});
|
||||
|
||||
it('should not show object permissions fallback when global record flags are set', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
objectPermissions: [
|
||||
{
|
||||
universalIdentifier: 'perm-1',
|
||||
objectUniversalIdentifier: 'obj-1',
|
||||
canReadObjectRecords: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read records');
|
||||
});
|
||||
});
|
||||
|
||||
describe('permission flags', () => {
|
||||
it('should add DATA_MODEL flag as "Read and write data model configuration"', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.DATA_MODEL],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Read and write data model configuration');
|
||||
});
|
||||
|
||||
it('should add WORKFLOWS flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKFLOWS],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage workflows');
|
||||
});
|
||||
|
||||
it('should add SECURITY flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.SECURITY],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage security settings');
|
||||
});
|
||||
|
||||
it('should add WORKSPACE_MEMBERS flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.WORKSPACE_MEMBERS,
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage workspace members');
|
||||
});
|
||||
|
||||
it('should add BILLING flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.BILLING],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage billing');
|
||||
});
|
||||
|
||||
it('should add API_KEYS_AND_WEBHOOKS flag', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.API_KEYS_AND_WEBHOOKS,
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Manage API keys and webhooks');
|
||||
});
|
||||
|
||||
it('should ignore unknown permission flags', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
'unknown-flag-00000000-0000-0000-0000-000000000000',
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings and tools', () => {
|
||||
it('should add "Update workspace settings" when canUpdateAllSettings is true', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canUpdateAllSettings: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Update workspace settings');
|
||||
});
|
||||
|
||||
it('should add "Access all tools" when canAccessAllTools is true', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canAccessAllTools: true,
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].label).toBe('Access all tools');
|
||||
});
|
||||
});
|
||||
|
||||
describe('combined permissions', () => {
|
||||
it('should combine all permission types in correct order', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canUpdateAllSettings: true,
|
||||
canAccessAllTools: true,
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.DATA_MODEL,
|
||||
SystemPermissionFlag.WORKFLOWS,
|
||||
],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Read and write records',
|
||||
'Read and write data model configuration',
|
||||
'Update workspace settings',
|
||||
'Access all tools',
|
||||
'Manage workflows',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle a role with only undefined/false permission values', () => {
|
||||
const role: RoleManifest = {
|
||||
...baseRole,
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: false,
|
||||
objectPermissions: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
};
|
||||
|
||||
const result = buildPermissionSummaryFromRoleManifest(role);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconCode,
|
||||
type IconComponent,
|
||||
IconCurrencyDollar,
|
||||
IconDatabase,
|
||||
IconHierarchy,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconSettingsAutomation,
|
||||
IconTool,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
import { SystemPermissionFlag } from 'twenty-shared/constants';
|
||||
|
||||
export type PermissionSummaryItem = {
|
||||
Icon: IconComponent;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const buildPermissionSummaryFromRoleManifest = (
|
||||
defaultRole: RoleManifest,
|
||||
): PermissionSummaryItem[] => {
|
||||
const items: PermissionSummaryItem[] = [];
|
||||
|
||||
const canRead = defaultRole.canReadAllObjectRecords ?? false;
|
||||
const canUpdate = defaultRole.canUpdateAllObjectRecords ?? false;
|
||||
const canSoftDelete = defaultRole.canSoftDeleteAllObjectRecords ?? false;
|
||||
const canDestroy = defaultRole.canDestroyAllObjectRecords ?? false;
|
||||
|
||||
if (canRead || canUpdate || canSoftDelete || canDestroy) {
|
||||
const capabilities: string[] = [];
|
||||
|
||||
if (canRead) {
|
||||
capabilities.push('read');
|
||||
}
|
||||
|
||||
if (canUpdate) {
|
||||
capabilities.push('write');
|
||||
}
|
||||
|
||||
if (canSoftDelete || canDestroy) {
|
||||
capabilities.push('delete');
|
||||
}
|
||||
|
||||
const label =
|
||||
capabilities.length <= 2
|
||||
? capabilities.join(' and ')
|
||||
: capabilities.slice(0, -1).join(', ') +
|
||||
', and ' +
|
||||
capabilities[capabilities.length - 1];
|
||||
|
||||
items.push({
|
||||
Icon: IconDatabase,
|
||||
label: label.charAt(0).toUpperCase() + label.slice(1) + ' records',
|
||||
});
|
||||
}
|
||||
|
||||
if ((defaultRole.objectPermissions ?? []).length > 0 && items.length === 0) {
|
||||
items.push({
|
||||
Icon: IconDatabase,
|
||||
label: 'Access specific object records',
|
||||
});
|
||||
}
|
||||
|
||||
const hasDataModelFlag = (
|
||||
defaultRole.permissionFlagUniversalIdentifiers ?? []
|
||||
).some((flag) => flag === SystemPermissionFlag.DATA_MODEL);
|
||||
|
||||
if (hasDataModelFlag) {
|
||||
items.push({
|
||||
Icon: IconHierarchy,
|
||||
label: 'Read and write data model configuration',
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultRole.canUpdateAllSettings) {
|
||||
items.push({
|
||||
Icon: IconSettings,
|
||||
label: 'Update workspace settings',
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultRole.canAccessAllTools) {
|
||||
items.push({
|
||||
Icon: IconTool,
|
||||
label: 'Access all tools',
|
||||
});
|
||||
}
|
||||
|
||||
const otherFlags = (
|
||||
defaultRole.permissionFlagUniversalIdentifiers ?? []
|
||||
).filter((flag) => flag !== SystemPermissionFlag.DATA_MODEL);
|
||||
|
||||
const flagLabels: Record<string, { label: string; Icon: IconComponent }> = {
|
||||
[SystemPermissionFlag.WORKFLOWS]: {
|
||||
label: 'Manage workflows',
|
||||
Icon: IconSettingsAutomation,
|
||||
},
|
||||
[SystemPermissionFlag.SECURITY]: {
|
||||
label: 'Manage security settings',
|
||||
Icon: IconKey,
|
||||
},
|
||||
[SystemPermissionFlag.WORKSPACE_MEMBERS]: {
|
||||
label: 'Manage workspace members',
|
||||
Icon: IconUsers,
|
||||
},
|
||||
[SystemPermissionFlag.BILLING]: {
|
||||
label: 'Manage billing',
|
||||
Icon: IconCurrencyDollar,
|
||||
},
|
||||
[SystemPermissionFlag.API_KEYS_AND_WEBHOOKS]: {
|
||||
label: 'Manage API keys and webhooks',
|
||||
Icon: IconCode,
|
||||
},
|
||||
};
|
||||
|
||||
for (const flag of otherFlags) {
|
||||
const config = flagLabels[flag];
|
||||
|
||||
if (isDefined(config)) {
|
||||
items.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
Reference in New Issue
Block a user