Skip to content

Commit

Permalink
feat: added Livechat's new theming settings to Appearance page (#32043)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>
  • Loading branch information
aleksandernsilva and KevLehman committed Mar 25, 2024
1 parent 95540f5 commit d1b1ffe
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 4 deletions.
13 changes: 13 additions & 0 deletions .changeset/good-baboons-shop.md
@@ -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
@@ -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
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
@@ -0,0 +1,32 @@
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;
};

const FieldLabel = ({ children: label, premium = false }: FieldLabelProps) => {
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>
);
};

export default FieldLabel;
104 changes: 103 additions & 1 deletion apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx
@@ -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
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
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

0 comments on commit d1b1ffe

Please sign in to comment.