Add Import CSV and Export CSV Permissions (#13421)
Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -53,6 +53,7 @@ import {
|
||||
IconTrashX,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
| NoSelectionRecordActionKeys
|
||||
@ -169,6 +170,7 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
isDefined(selectedRecord) && !selectedRecord.isRemote,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
component: <ExportSingleRecordAction />,
|
||||
requiredPermissionFlag: PermissionFlagType.EXPORT_CSV,
|
||||
},
|
||||
[MultipleRecordsActionKeys.EXPORT]: {
|
||||
type: ActionType.Standard,
|
||||
@ -183,6 +185,7 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
shouldBeRegistered: () => true,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
component: <ExportMultipleRecordsAction />,
|
||||
requiredPermissionFlag: PermissionFlagType.EXPORT_CSV,
|
||||
},
|
||||
[NoSelectionRecordActionKeys.IMPORT_RECORDS]: {
|
||||
type: ActionType.Standard,
|
||||
@ -198,6 +201,7 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
!isSoftDeleteFilterActive,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
component: <ImportRecordsNoSelectionRecordAction />,
|
||||
requiredPermissionFlag: PermissionFlagType.IMPORT_CSV,
|
||||
},
|
||||
[NoSelectionRecordActionKeys.EXPORT_VIEW]: {
|
||||
type: ActionType.Standard,
|
||||
@ -212,6 +216,7 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
shouldBeRegistered: () => true,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
component: <ExportMultipleRecordsAction />,
|
||||
requiredPermissionFlag: PermissionFlagType.EXPORT_CSV,
|
||||
},
|
||||
[SingleRecordActionKeys.DELETE]: {
|
||||
type: ActionType.Standard,
|
||||
|
||||
@ -5,6 +5,7 @@ import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/Sh
|
||||
import { MessageDescriptor } from '@lingui/core';
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
import { MenuItemAccent } from 'twenty-ui/navigation';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type ActionConfig = {
|
||||
type: ActionType;
|
||||
@ -21,4 +22,5 @@ export type ActionConfig = {
|
||||
shouldBeRegistered: (params: ShouldBeRegisteredFunctionParams) => boolean;
|
||||
component: React.ReactNode;
|
||||
hotKeys?: string[];
|
||||
requiredPermissionFlag?: PermissionFlagType;
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
|
||||
import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
@ -48,6 +49,8 @@ export const useRegisteredActions = (
|
||||
...recordAgnosticActionConfig,
|
||||
};
|
||||
|
||||
const permissionMap = usePermissionFlagMap();
|
||||
|
||||
const actionsToRegister = isDefined(viewType)
|
||||
? Object.values(actionsConfig).filter(
|
||||
(action) =>
|
||||
@ -57,7 +60,15 @@ export const useRegisteredActions = (
|
||||
: [];
|
||||
|
||||
const actions = actionsToRegister
|
||||
.filter((action) => action.shouldBeRegistered(shouldBeRegisteredParams))
|
||||
.filter((action) => {
|
||||
if (
|
||||
isDefined(action.requiredPermissionFlag) &&
|
||||
!permissionMap[action.requiredPermissionFlag]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return action.shouldBeRegistered(shouldBeRegisteredParams);
|
||||
})
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
return actions;
|
||||
|
||||
@ -3,7 +3,7 @@ import { UserWorkspace } from '~/generated/graphql';
|
||||
|
||||
export type CurrentUserWorkspace = Pick<
|
||||
UserWorkspace,
|
||||
| 'settingsPermissions'
|
||||
| 'permissionFlags'
|
||||
| 'objectRecordsPermissions'
|
||||
| 'objectPermissions'
|
||||
| 'twoFactorAuthenticationMethodSummary'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -21,7 +21,7 @@ export const InformationBannerBillingSubscriptionPaused = () => {
|
||||
|
||||
const {
|
||||
[PermissionFlagType.WORKSPACE]: hasPermissionToUpdateBillingDetails,
|
||||
} = useSettingsPermissionMap();
|
||||
} = usePermissionFlagMap();
|
||||
|
||||
const openBillingPortal = () => {
|
||||
if (isDefined(data) && isDefined(data.billingPortalSession.url)) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
|
||||
@ -9,7 +9,7 @@ export const InformationBannerEndTrialPeriod = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { [PermissionFlagType.WORKSPACE]: hasPermissionToEndTrialPeriod } =
|
||||
useSettingsPermissionMap();
|
||||
usePermissionFlagMap();
|
||||
|
||||
return (
|
||||
<InformationBanner
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -21,7 +21,7 @@ export const InformationBannerFailPaymentInfo = () => {
|
||||
|
||||
const {
|
||||
[PermissionFlagType.WORKSPACE]: hasPermissionToUpdateBillingDetails,
|
||||
} = useSettingsPermissionMap();
|
||||
} = usePermissionFlagMap();
|
||||
|
||||
const openBillingPortal = () => {
|
||||
if (isDefined(data) && isDefined(data.billingPortalSession.url)) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
|
||||
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
@ -16,7 +16,7 @@ export const InformationBannerNoBillingSubscription = () => {
|
||||
});
|
||||
|
||||
const { [PermissionFlagType.WORKSPACE]: hasPermissionToSubscribe } =
|
||||
useSettingsPermissionMap();
|
||||
usePermissionFlagMap();
|
||||
|
||||
return (
|
||||
<InformationBanner
|
||||
|
||||
@ -132,7 +132,7 @@ export const queries = {
|
||||
...WorkspaceMemberQueryFragment
|
||||
}
|
||||
currentUserWorkspace {
|
||||
settingsPermissions
|
||||
permissionFlags
|
||||
objectRecordsPermissions
|
||||
}
|
||||
currentWorkspace {
|
||||
@ -287,7 +287,7 @@ export const responseData = {
|
||||
},
|
||||
workspaceMembers: [],
|
||||
currentUserWorkspace: {
|
||||
settingsPermissions: ['DATA_MODEL'],
|
||||
permissionFlags: ['DATA_MODEL'],
|
||||
objectRecordsPermissions: [
|
||||
PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS,
|
||||
PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS,
|
||||
|
||||
@ -6,7 +6,7 @@ import { recordGroupFieldMetadataComponentState } from '@/object-record/record-g
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
@ -90,7 +90,7 @@ export const useRecordGroupActions = ({
|
||||
recordGroupFieldMetadata,
|
||||
]);
|
||||
|
||||
const hasAccessToDataModelSettings = useHasSettingsPermission(
|
||||
const hasAccessToDataModelSettings = useHasPermissionFlag(
|
||||
PermissionFlagType.DATA_MODEL,
|
||||
);
|
||||
const currentIndex = visibleRecordGroupIds.findIndex(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { ReactNode } from 'react';
|
||||
@ -17,7 +17,7 @@ export const SettingsProtectedRouteWrapper = ({
|
||||
settingsPermission,
|
||||
requiredFeatureFlag,
|
||||
}: SettingsProtectedRouteWrapperProps) => {
|
||||
const hasPermission = useHasSettingsPermission(settingsPermission);
|
||||
const hasPermission = useHasPermissionFlag(settingsPermission);
|
||||
const requiredFeatureFlagEnabled = useIsFeatureEnabled(
|
||||
requiredFeatureFlag || null,
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
const mockCurrentUser = {
|
||||
@ -53,13 +53,13 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
jest.mock('@/settings/roles/hooks/useSettingsPermissionMap', () => ({
|
||||
useSettingsPermissionMap: jest.fn(),
|
||||
jest.mock('@/settings/roles/hooks/usePermissionFlagMap', () => ({
|
||||
usePermissionFlagMap: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useSettingsNavigationItems', () => {
|
||||
it('should hide workspace settings when no permissions', () => {
|
||||
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
|
||||
(usePermissionFlagMap as jest.Mock).mockImplementation(() => ({
|
||||
[PermissionFlagType.WORKSPACE]: false,
|
||||
[PermissionFlagType.WORKSPACE_MEMBERS]: false,
|
||||
[PermissionFlagType.DATA_MODEL]: false,
|
||||
@ -80,7 +80,7 @@ describe('useSettingsNavigationItems', () => {
|
||||
});
|
||||
|
||||
it('should show workspace settings when has permissions', () => {
|
||||
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
|
||||
(usePermissionFlagMap as jest.Mock).mockImplementation(() => ({
|
||||
[PermissionFlagType.WORKSPACE]: true,
|
||||
[PermissionFlagType.WORKSPACE_MEMBERS]: true,
|
||||
[PermissionFlagType.DATA_MODEL]: true,
|
||||
|
||||
@ -4,7 +4,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@ -63,7 +63,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
false;
|
||||
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
|
||||
|
||||
const permissionMap = useSettingsPermissionMap();
|
||||
const permissionMap = usePermissionFlagMap();
|
||||
return [
|
||||
{
|
||||
label: t`User`,
|
||||
|
||||
@ -4,9 +4,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { PermissionFlagType } from '~/generated/graphql';
|
||||
|
||||
export const useHasSettingsPermission = (
|
||||
permissionFlag?: PermissionFlagType,
|
||||
) => {
|
||||
export const useHasPermissionFlag = (permissionFlag?: PermissionFlagType) => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
|
||||
|
||||
@ -22,7 +20,7 @@ export const useHasSettingsPermission = (
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentUserWorkspaceSetting = currentUserWorkspace?.settingsPermissions;
|
||||
const currentUserWorkspaceSetting = currentUserWorkspace?.permissionFlags;
|
||||
|
||||
if (!currentUserWorkspaceSetting) {
|
||||
return false;
|
||||
@ -3,14 +3,11 @@ import { useRecoilValue } from 'recoil';
|
||||
import { PermissionFlagType } from '~/generated/graphql';
|
||||
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
|
||||
|
||||
export const useSettingsPermissionMap = (): Record<
|
||||
PermissionFlagType,
|
||||
boolean
|
||||
> => {
|
||||
export const usePermissionFlagMap = (): Record<PermissionFlagType, boolean> => {
|
||||
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
|
||||
|
||||
const currentUserWorkspaceSettingsPermissions =
|
||||
currentUserWorkspace?.settingsPermissions;
|
||||
currentUserWorkspace?.permissionFlags;
|
||||
|
||||
const initialPermissions = buildRecordFromKeysWithSameValue(
|
||||
Object.values(PermissionFlagType),
|
||||
@ -7,7 +7,13 @@ import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { H2Title, IconMail, IconTool } from 'twenty-ui/display';
|
||||
import {
|
||||
H2Title,
|
||||
IconFileExport,
|
||||
IconFileImport,
|
||||
IconMail,
|
||||
IconTool,
|
||||
} from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
|
||||
@ -45,19 +51,30 @@ export const SettingsRolePermissionsToolSection = ({
|
||||
Icon: IconMail,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.IMPORT_CSV,
|
||||
name: t`Import CSV`,
|
||||
description: t`Allow importing data from CSV files`,
|
||||
Icon: IconFileImport,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.EXPORT_CSV,
|
||||
name: t`Export CSV`,
|
||||
description: t`Allow exporting data to CSV files`,
|
||||
Icon: IconFileExport,
|
||||
isToolPermission: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Action Permissions`}
|
||||
description={t`Permissions for performing automated actions.`}
|
||||
/>
|
||||
<H2Title title={t`Actions`} description={t`Actions permissions`} />
|
||||
<StyledCard rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconTool}
|
||||
title={t`All Actions Access`}
|
||||
description={t`Grants permission to perform all available actions without restriction.`}
|
||||
description={t`Grants permission to perform all available actions without restriction`}
|
||||
checked={settingsDraftRole.canAccessAllTools}
|
||||
disabled={!isEditable}
|
||||
onChange={() => {
|
||||
|
||||
@ -34,7 +34,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
...DeletedWorkspaceMemberQueryFragment
|
||||
}
|
||||
currentUserWorkspace {
|
||||
settingsPermissions
|
||||
permissionFlags
|
||||
objectRecordsPermissions
|
||||
objectPermissions {
|
||||
...ObjectPermissionFragment
|
||||
|
||||
Reference in New Issue
Block a user