Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BREAK] [NEW] Custom roles upsell modal #27707

Merged
merged 21 commits into from Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/meteor/client/components/GenericModal.tsx
Expand Up @@ -17,10 +17,11 @@ type GenericModalProps = RequiredModalProps & {
title?: string | ReactElement;
icon?: ComponentProps<typeof Icon>['name'] | ReactElement | null;
confirmDisabled?: boolean;
tagline?: ReactNode;
onCancel?: () => void;
onClose?: () => void;
onConfirm: () => void;
};
} & Omit<ComponentProps<typeof Modal>, 'title'>;

const iconMap: Record<string, ComponentProps<typeof Icon>['name']> = {
danger: 'modal-warning',
Expand Down Expand Up @@ -68,6 +69,7 @@ const GenericModal: FC<GenericModalProps> = ({
onConfirm,
dontAskAgain,
confirmDisabled,
tagline,
...props
}) => {
const t = useTranslation();
Expand All @@ -77,6 +79,7 @@ const GenericModal: FC<GenericModalProps> = ({
<Modal.Header>
{renderIcon(icon, variant)}
<Modal.HeaderText>
{tagline && <Modal.Tagline>{tagline}</Modal.Tagline>}
<Modal.Title>{title ?? t('Are_you_sure')}</Modal.Title>
</Modal.HeaderText>
<Modal.Close title={t('Close')} onClick={onClose} />
Expand Down
@@ -0,0 +1,36 @@
import { Modal, Box } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { VFC } from 'react';
import React from 'react';

import GenericModal from '../../../components/GenericModal';

type CustomRoleUpsellModalProps = {
onClose: () => void;
};

const CustomRoleUpsellModal: VFC<CustomRoleUpsellModalProps> = ({ onClose }) => {
const t = useTranslation();
return (
<GenericModal
id='custom-roles'
tagline={t('Enterprise_feature')}
title={t('Custom_roles')}
onCancel={onClose}
cancelText={t('Close')}
confirmText={t('Talk_to_sales')}
onConfirm={() => window.open('https://go.rocket.chat/i/ce-custom-roles')}
variant='warning'
icon={null}
>
<Modal.HeroImage maxHeight='initial' src={'images/custom-role-upsell-modal.png'} />
<Box is='h3' fontScale='h3'>
{t('Custom_roles_upsell_add_custom_roles_workspace')}
</Box>
<br />
<p>{t('Custom_roles_upsell_add_custom_roles_workspace_description')}</p>
</GenericModal>
);
};

export default CustomRoleUpsellModal;
12 changes: 10 additions & 2 deletions apps/meteor/client/views/admin/permissions/EditRolePage.tsx
Expand Up @@ -7,7 +7,9 @@ import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';

import GenericModal from '../../../components/GenericModal';
import { FormSkeleton } from '../../../components/Skeleton';
import VerticalBar from '../../../components/VerticalBar';
import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import RoleForm from './RoleForm';

const EditRolePage = ({ role }: { role?: IRole }): ReactElement => {
Expand All @@ -16,6 +18,8 @@ const EditRolePage = ({ role }: { role?: IRole }): ReactElement => {
const setModal = useSetModal();
const usersInRoleRouter = useRoute('admin-permissions');
const router = useRoute('admin-permissions');
const { data, isLoading } = useIsEnterprise();
const isEnterprise = data?.isEnterprise;

const createRole = useEndpoint('POST', '/v1/roles.create');
const updateRole = useEndpoint('POST', '/v1/roles.update');
Expand Down Expand Up @@ -87,20 +91,24 @@ const EditRolePage = ({ role }: { role?: IRole }): ReactElement => {
);
});

if (isLoading) {
return <FormSkeleton />;
}

return (
<>
<VerticalBar.ScrollableContent>
<Box w='full' alignSelf='center' mb='neg-x8'>
<Margins block='x8'>
<FormProvider {...methods}>
<RoleForm editing={Boolean(role?._id)} isProtected={role?.protected} />
<RoleForm editing={Boolean(role?._id)} isProtected={role?.protected} isDisabled={!isEnterprise} />
</FormProvider>
</Margins>
</Box>
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup vertical stretch>
<Button primary disabled={!methods.formState.isDirty} onClick={methods.handleSubmit(handleSave)}>
<Button primary disabled={!methods.formState.isDirty || !isEnterprise} onClick={methods.handleSubmit(handleSave)}>
{t('Save')}
</Button>
{!role?.protected && role?._id && (
Expand Down
@@ -1,21 +1,32 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import { useRouteParameter, useRoute, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';
import React, { useEffect } from 'react';

import VerticalBar from '../../../components/VerticalBar';
import CustomRoleUpsellModal from './CustomRoleUpsellModal';
import EditRolePageWithData from './EditRolePageWithData';

const PermissionsContextBar = (): ReactElement | null => {
const PermissionsContextBar = ({ isEnterprise }: { isEnterprise: boolean }): ReactElement | null => {
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
const t = useTranslation();
const _id = useRouteParameter('_id');
const context = useRouteParameter('context');
const router = useRoute('admin-permissions');
const setModal = useSetModal();

const handleCloseVerticalBar = useMutableCallback(() => {
router.push({});
});

useEffect(() => {
if (context !== 'new' || isEnterprise) {
return;
}

setModal(<CustomRoleUpsellModal onClose={() => setModal()} />);
handleCloseVerticalBar();
}, [context, isEnterprise, handleCloseVerticalBar, setModal]);

return (
(context && (
<VerticalBar>
Expand Down
Expand Up @@ -2,6 +2,8 @@ import { useRouteParameter, usePermission } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';

import PageSkeleton from '../../../components/PageSkeleton';
import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
import PermissionsTable from './PermissionsTable';
import UsersInRole from './UsersInRole';
Expand All @@ -10,6 +12,11 @@ const PermissionsRouter = (): ReactElement => {
const canViewPermission = usePermission('access-permissions');
const canViewSettingPermission = usePermission('access-setting-permissions');
const context = useRouteParameter('context');
const { data, isLoading } = useIsEnterprise();

if (isLoading) {
<PageSkeleton />;
}

if (!canViewPermission && !canViewSettingPermission) {
return <NotAuthorizedPage />;
Expand All @@ -19,7 +26,7 @@ const PermissionsRouter = (): ReactElement => {
return <UsersInRole />;
}

return <PermissionsTable />;
return <PermissionsTable isEnterprise={Boolean(data?.isEnterprise)} />;
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
};

export default PermissionsRouter;
@@ -1,26 +1,28 @@
import { Margins, Icon, Tabs, Button, Pagination, Tile } from '@rocket.chat/fuselage';
import { Margins, Tabs, Button, Pagination, Tile } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useRoute, usePermission, useMethod, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useState } from 'react';

import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import Page from '../../../../components/Page';
import UpsellModal from '../CustomRoleUpsellModal';
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
import PermissionsContextBar from '../PermissionsContextBar';
import { usePermissionsAndRoles } from '../hooks/usePermissionsAndRoles';
import PermissionRow from './PermissionRow';
import PermissionsTableFilter from './PermissionsTableFilter';
import RoleHeader from './RoleHeader';

const PermissionsTable = (): ReactElement => {
const PermissionsTable = ({ isEnterprise }: { isEnterprise: boolean }): ReactElement => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const canViewPermission = usePermission('access-permissions');
const canViewSettingPermission = usePermission('access-setting-permissions');
const defaultType = canViewPermission ? 'permissions' : 'settings';
const [type, setType] = useState(defaultType);
const router = useRoute('admin-permissions');
const setModal = useSetModal();

const grantRole = useMethod('authorization:addPermissionToRole');
const removeRole = useMethod('authorization:removeRoleFromPermission');
Expand All @@ -43,6 +45,10 @@ const PermissionsTable = (): ReactElement => {
});

const handleAdd = useMutableCallback(() => {
if (!isEnterprise) {
setModal(<UpsellModal onClose={() => setModal()} />);
return;
}
router.push({
context: 'new',
});
Expand All @@ -52,8 +58,8 @@ const PermissionsTable = (): ReactElement => {
<Page flexDirection='row'>
<Page>
<Page.Header title={t('Permissions')}>
<Button primary onClick={handleAdd} aria-label={t('New')}>
<Icon name='plus' /> {t('New_role')}
<Button primary onClick={handleAdd} aria-label={t('New')} name={t('New_role')}>
{t('New_role')}
</Button>
</Page.Header>
<Margins blockEnd='x16'>
Expand Down Expand Up @@ -119,7 +125,7 @@ const PermissionsTable = (): ReactElement => {
</Margins>
</Page.Content>
</Page>
<PermissionsContextBar />
<PermissionsContextBar isEnterprise={isEnterprise} />
</Page>
);
};
Expand Down
@@ -1,6 +1,5 @@
import type { IRole } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Margins, Box, Icon } from '@rocket.chat/fuselage';
import { Margins, Icon, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
Expand All @@ -26,24 +25,12 @@ const RoleHeader = ({ _id, name, description }: RoleHeaderProps): ReactElement =

return (
<GenericTable.HeaderCell clickable pi='x4' p='x8'>
<Box
className={css`
white-space: nowrap;
`}
pb='x8'
pi='x12'
mi='neg-x2'
borderStyle='solid'
borderWidth='default'
borderRadius='x2'
borderColor='light'
onClick={handleEditRole}
>
<Button secondary onClick={handleEditRole}>
<Margins inline='x2'>
<span>{description || name}</span>
<Icon name='edit' size='x16' />
</Margins>
</Box>
</Button>
</GenericTable.HeaderCell>
);
};
Expand Down
13 changes: 8 additions & 5 deletions apps/meteor/client/views/admin/permissions/RoleForm.tsx
Expand Up @@ -9,9 +9,10 @@ type RoleFormProps = {
className?: string;
editing?: boolean;
isProtected?: boolean;
isDisabled?: boolean;
};

const RoleForm = ({ className, editing = false, isProtected = false }: RoleFormProps): ReactElement => {
const RoleForm = ({ className, editing = false, isProtected = false, isDisabled = false }: RoleFormProps): ReactElement => {
const t = useTranslation();
const {
register,
Expand All @@ -32,14 +33,14 @@ const RoleForm = ({ className, editing = false, isProtected = false }: RoleFormP
<Field className={className}>
<Field.Label>{t('Role')}</Field.Label>
<Field.Row>
<TextInput disabled={editing} placeholder={t('Role')} {...register('name', { required: true })} />
<TextInput disabled={editing || isDisabled} placeholder={t('Role')} {...register('name', { required: true })} />
</Field.Row>
{errors?.name && <Field.Error>{t('error-the-field-is-required', { field: t('Role') })}</Field.Error>}
</Field>
<Field className={className}>
<Field.Label>{t('Description')}</Field.Label>
<Field.Row>
<TextInput placeholder={t('Description')} {...register('description')} />
<TextInput placeholder={t('Description')} disabled={isDisabled} {...register('description')} />
</Field.Row>
<Field.Hint>{'Leave the description field blank if you dont want to show the role'}</Field.Hint>
</Field>
Expand All @@ -49,7 +50,9 @@ const RoleForm = ({ className, editing = false, isProtected = false }: RoleFormP
<Controller
name='scope'
control={control}
render={({ field }): ReactElement => <Select {...field} options={options} disabled={isProtected} placeholder={t('Scope')} />}
render={({ field }): ReactElement => (
<Select {...field} options={options} disabled={isProtected || isDisabled} placeholder={t('Scope')} />
)}
/>
</Field.Row>
</Field>
Expand All @@ -60,7 +63,7 @@ const RoleForm = ({ className, editing = false, isProtected = false }: RoleFormP
<Controller
name='mandatory2fa'
control={control}
render={({ field }): ReactElement => <ToggleSwitch {...field} checked={field.value} />}
render={({ field }): ReactElement => <ToggleSwitch {...field} checked={field.value} disabled={isDisabled} />}
/>
</Field.Row>
</Box>
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Expand Up @@ -1436,6 +1436,9 @@
"Custom_OAuth_has_been_removed": "Custom OAuth has been removed",
"Custom_oauth_helper": "When setting up your OAuth Provider, you'll have to inform a Callback URL. Use <pre>%s</pre> .",
"Custom_oauth_unique_name": "Custom OAuth unique name",
"Custom_roles": "Custom roles",
"Custom_roles_upsell_add_custom_roles_workspace": "Add custom roles to suit your workspace",
"Custom_roles_upsell_add_custom_roles_workspace_description": "Custom roles allow you to set access limits for the people in your workspace. Set all the roles you need to make sure people have a safe environment to work on.",
"Custom_Script_Logged_In": "Custom Script for Logged In Users",
"Custom_Script_Logged_In_Description": "Custom Script that will run ALWAYS and to ANY user that is logged in. e.g. (whenever you enter the chat and you are logged in)",
"Custom_Script_Logged_Out": "Custom Script for Logged Out Users",
Expand Down Expand Up @@ -1837,6 +1840,7 @@
"Enterprise": "Enterprise",
"Enterprise_capabilities": "Enterprise capabilities",
"Enterprise_Description": "Manually update your Enterprise license.",
"Enterprise_feature": "Enterprise feature",
"Enterprise_License": "Enterprise License",
"Enterprise_License_Description": "If your workspace is registered and license is provided by Rocket.Chat Cloud you don't need to manually update the license here.",
"Entertainment": "Entertainment",
Expand Down Expand Up @@ -4586,6 +4590,7 @@
"Take_rocket_chat_with_you_with_mobile_applications": "Take Rocket.Chat with you with mobile applications.",
"Taken_at": "Taken at",
"Talk_Time": "Talk Time",
"Talk_to_sales": "Talk to sales",
"Talk_to_your_workspace_administrator_about_enabling_video_conferencing": "Talk to your workspace administrator about enabling video conferencing",
"Target user not allowed to receive messages": "Target user not allowed to receive messages",
"TargetRoom": "Target Room",
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions apps/meteor/tests/e2e/administration.spec.ts
Expand Up @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker';

import { test, expect } from './utils/test';
import { Admin } from './page-objects';
import { IS_EE } from './config/constants';

test.use({ storageState: 'admin-session.json' });

Expand Down Expand Up @@ -58,6 +59,18 @@ test.describe.parallel('administration', () => {
});
});

test.describe('Permissions', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/admin/permissions');
});

test('expect open upsell modal if not enterprise', async ({ page }) => {
test.skip(IS_EE);
await poAdmin.btnCreateRole.click();
await page.waitForSelector('dialog[id="custom-roles"]');
});
});

test.describe('Settings', () => {
test.describe('General', () => {
test.beforeEach(async ({ page }) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/tests/e2e/page-objects/admin.ts
Expand Up @@ -129,6 +129,10 @@ export class Admin {
return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> role=button[name="Delete"]');
}

get btnCreateRole(): Locator {
return this.page.locator('button[name="New role"]');
}

get inputAssetsLogo(): Locator {
return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> input[type="file"]');
}
Expand Down