Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/good-baboons-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/rest-typings": minor
---

**Added Livechat's new theming settings to Appearance page (available for Premium workspaces)**

Newly added settings are:
- `Livechat widget position on the screen`: Changes the widget position between left or right of the viewport
- `Livechat background`: Changes the message list background. Receives the same value as the CSS's background property.
- `Hide system messages`: Changes the visibility of system messages displayed on the widget.
- `Hide "powered by Rocket.Chat"`: Changes the visibility of Rocket.Chat's watermark on the widget.

19 changes: 18 additions & 1 deletion apps/meteor/app/livechat/imports/server/rest/appearance.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ISettingSelectOption } from '@rocket.chat/core-typings';
import { Settings } from '@rocket.chat/models';
import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings';

Expand Down Expand Up @@ -45,6 +46,10 @@ API.v1.addRoute(
'Livechat_name_field_registration_form',
'Livechat_email_field_registration_form',
'Livechat_registration_form_message',
'Livechat_hide_watermark',
'Livechat_background',
'Livechat_widget_position',
'Livechat_hide_system_messages',
];

const valid = settings.every((setting) => validSettingList.includes(setting._id));
Expand All @@ -60,6 +65,10 @@ API.v1.addRoute(
return;
}

if (dbSetting.type === 'multiSelect' && (!Array.isArray(setting.value) || !validateValues(setting.value, dbSetting.values))) {
return;
}

switch (dbSetting?.type) {
case 'boolean':
return {
Expand Down Expand Up @@ -91,7 +100,11 @@ API.v1.addRoute(
},
);

function coerceInt(value: string | number | boolean): number {
function validateValues(values: string[], allowedValues: ISettingSelectOption[] = []): boolean {
return values.every((value) => allowedValues.some((allowedValue) => allowedValue.key === value));
}

function coerceInt(value: string | number | boolean | string[]): number {
if (typeof value === 'number') {
return value;
}
Expand All @@ -100,6 +113,10 @@ function coerceInt(value: string | number | boolean): number {
return 0;
}

if (Array.isArray(value)) {
return 0;
}

const parsedValue = parseInt(value, 10);
if (Number.isNaN(parsedValue)) {
return 0;
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/app/livechat/server/api/lib/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export async function findAppearance(): Promise<{ appearance: ISetting[] }> {
'Livechat_email_field_registration_form',
'Livechat_registration_form_message',
'Livechat_conversation_finished_text',
'Livechat_hide_watermark',
'Livechat_background',
'Livechat_widget_position',
'Livechat_hide_system_messages',
],
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FieldLabel as BaseFieldLabel, Box, Tag } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ComponentProps } from 'react';
import React from 'react';

import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';

type FieldLabelProps = ComponentProps<typeof BaseFieldLabel> & {
premium?: boolean;
children: string;
};

export const FieldLabel = ({ children: label, premium = false }: FieldLabelProps) => {
Comment thread
aleksandernsilva marked this conversation as resolved.
Outdated
const t = useTranslation();
const hasLicense = useHasLicenseModule('livechat-enterprise');
const shouldDisableEnterprise = premium && !hasLicense;

if (!shouldDisableEnterprise) {
return <BaseFieldLabel>{label}</BaseFieldLabel>;
}

return (
<BaseFieldLabel>
<Box is='span' mie={4}>
{label}
</Box>
<Tag variant='primary'>{t('Premium')}</Tag>
</BaseFieldLabel>
);
};
104 changes: 103 additions & 1 deletion apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Field,
FieldLabel,
FieldRow,
TextInput,
ToggleSwitch,
Expand All @@ -9,15 +8,23 @@ import {
InputBox,
TextAreaInput,
NumberInput,
Select,
MultiSelect,
FieldHint,
} from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ChangeEvent } from 'react';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';

import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';
import MarkdownText from '../../../components/MarkdownText';
import { FieldLabel } from './AppearanceFieldLabel';

const AppearanceForm = () => {
const t = useTranslation();
const isEnterprise = useHasLicenseModule('livechat-enterprise');

const { control, watch } = useFormContext();
const { Livechat_enable_message_character_limit } = watch();
Expand All @@ -41,9 +48,101 @@ const AppearanceForm = () => {
const livechatRegistrationFormMessageField = useUniqueId();
const livechatConversationFinishedMessageField = useUniqueId();
const livechatConversationFinishedTextField = useUniqueId();
const livechatHideWatermarkField = useUniqueId();
const livechatWidgetPositionField = useUniqueId();
const livechatBackgroundField = useUniqueId();
const livechatHideSystemMessagesField = useUniqueId();

return (
<Accordion>
<Accordion.Item defaultExpanded title={t('General')}>
<FieldGroup>
<Field>
<FieldRow>
<FieldLabel premium htmlFor={livechatHideWatermarkField}>
{t('Livechat_hide_watermark')}
</FieldLabel>
<Controller
name='Livechat_hide_watermark'
control={control}
render={({ field: { value, ...field } }) => (
<ToggleSwitch id={livechatHideWatermarkField} {...field} checked={value} disabled={!isEnterprise} />
)}
/>
</FieldRow>
</Field>

<Field>
<FieldLabel premium htmlFor={livechatBackgroundField}>
{t('Livechat_background')}
</FieldLabel>
<FieldRow>
<Controller
name='Livechat_background'
control={control}
render={({ field: { value, ...field } }) => (
<TextInput {...field} id={livechatBackgroundField} value={value} disabled={!isEnterprise} />
)}
/>
</FieldRow>
<FieldHint>
<MarkdownText variant='inline' preserveHtml content={t('Livechat_background_description')} />
</FieldHint>
</Field>

<Field>
<FieldLabel premium htmlFor={livechatWidgetPositionField}>
{t('Livechat_widget_position_on_the_screen')}
</FieldLabel>
<FieldRow>
<Controller
name='Livechat_widget_position'
control={control}
render={({ field: { value, ...field } }) => (
<Select
{...field}
id={livechatWidgetPositionField}
value={value}
disabled={!isEnterprise}
options={[
['left', t('Left')],
['right', t('Right')],
]}
/>
)}
/>
</FieldRow>
</Field>

<Field>
<FieldLabel premium htmlFor={livechatHideSystemMessagesField}>
{t('Livechat_hide_system_messages')}
</FieldLabel>
<FieldRow>
<Controller
name='Livechat_hide_system_messages'
control={control}
render={({ field: { value, ...field } }) => (
<MultiSelect
{...field}
id={livechatHideSystemMessagesField}
value={value}
disabled={!isEnterprise}
options={[
['uj', t('Message_HideType_uj')],
['ul', t('Message_HideType_ul')],
['livechat-close', t('Message_HideType_livechat_closed')],
['livechat-started', t('Message_HideType_livechat_started')],
['livechat_transfer_history', t('Message_HideType_livechat_transfer_history')],
]}
/>
)}
/>
</FieldRow>
</Field>
</FieldGroup>
</Accordion.Item>

<Accordion.Item defaultExpanded title={t('Livechat_online')}>
<FieldGroup>
<Field>
Expand Down Expand Up @@ -118,6 +217,7 @@ const AppearanceForm = () => {
</Field>
</FieldGroup>
</Accordion.Item>

<Accordion.Item title={t('Livechat_offline')}>
<FieldGroup>
<Field>
Expand Down Expand Up @@ -192,6 +292,7 @@ const AppearanceForm = () => {
</Field>
</FieldGroup>
</Accordion.Item>

<Accordion.Item title={t('Livechat_registration_form')}>
<FieldGroup>
<Field>
Expand Down Expand Up @@ -242,6 +343,7 @@ const AppearanceForm = () => {
</Field>
</FieldGroup>
</Accordion.Item>

<Accordion.Item title={t('Conversation_finished')}>
<FieldGroup>
<Field>
Expand Down
57 changes: 57 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { sleep } from '../../../data/livechat/utils';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';

describe('LIVECHAT - appearance', function () {
this.retries(0);
Expand Down Expand Up @@ -198,5 +199,61 @@ describe('LIVECHAT - appearance', function () {
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.limitTextLength).to.be.false;
});
(IS_EE ? it : it.skip)('should accept an array setting', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: ['uj'] }])
.expect(200);
await sleep(500);

// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
});
(IS_EE ? it : it.skip)('should accept an array setting with multiple values', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: ['uj', 'ul'] }])
.expect(200);
await sleep(500);

// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
expect(body.config.settings.hiddenSystemMessages).to.include('ul');
});
(IS_EE ? it : it.skip)('should not update an array setting with a value other than array', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: 'uj' }])
.expect(200);

await sleep(500);

// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
});
(IS_EE ? it : it.skip)('should not update an array setting with values that are not valid setting values', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: ['uj', 'invalid'] }])
.expect(200);

await sleep(500);

// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
expect(body.config.settings.hiddenSystemMessages).to.not.include('invalid');
});
});
});
4 changes: 2 additions & 2 deletions packages/rest-typings/src/v1/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3168,7 +3168,7 @@ export const isPOSTLivechatTriggersParams = ajv.compile<POSTLivechatTriggersPara

type POSTLivechatAppearanceParams = {
_id: string;
value: string | boolean | number;
value: string | boolean | number | string[];
}[];

const POSTLivechatAppearanceParamsSchema = {
Expand All @@ -3181,7 +3181,7 @@ const POSTLivechatAppearanceParamsSchema = {
},
value: {
// Be careful with anyOf - https://github.com/ajv-validator/ajv/issues/1140
type: ['string', 'boolean', 'number'],
type: ['string', 'boolean', 'number', 'array'],
},
},
required: ['_id', 'value'],
Expand Down