From 40bd79f1ca75fae8e12f35509200d327adae2d0e Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 8 Aug 2024 13:44:08 +0530 Subject: [PATCH 01/44] feat: Add issue status in linear issue search item (#9598) --- .../components/ui/Dropdown/DropdownList.vue | 2 ++ .../ui/Dropdown/DropdownListItemButton.vue | 14 ++++++++++++++ .../widgets/conversation/linear/LinkIssue.vue | 2 ++ lib/linear/queries.rb | 4 ++++ 4 files changed, 22 insertions(+) diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue index c03346a49..ce6f8868c 100644 --- a/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue +++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownList.vue @@ -101,6 +101,8 @@ const shouldShowEmptyState = computed(() => { :key="item.id" :is-active="isFilterActive(item.id)" :button-text="item.name" + :icon="item.icon" + :icon-color="item.iconColor" @click="$emit('click', item)" /> diff --git a/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue b/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue index 9cabd5117..af7a197a3 100644 --- a/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue +++ b/app/javascript/dashboard/components/ui/Dropdown/DropdownListItemButton.vue @@ -8,6 +8,14 @@ defineProps({ type: Boolean, default: false, }, + icon: { + type: String, + default: '', + }, + iconColor: { + type: String, + default: '', + }, }); @@ -20,6 +28,12 @@ defineProps({ @focus="$emit('focus')" >
+ diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue b/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue index 87386f681..c2fb15b21 100644 --- a/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue +++ b/app/javascript/dashboard/components/widgets/conversation/linear/LinkIssue.vue @@ -62,6 +62,8 @@ const onSearch = async value => { issues.value = response.data.map(issue => ({ id: issue.id, name: `${issue.identifier} ${issue.title}`, + icon: 'status', + iconColor: issue.state.color, })); } catch (error) { const errorMessage = parseLinearAPIErrorResponse( diff --git a/lib/linear/queries.rb b/lib/linear/queries.rb index 78b2108f9..54daaf30c 100644 --- a/lib/linear/queries.rb +++ b/lib/linear/queries.rb @@ -54,6 +54,10 @@ module Linear::Queries title description identifier + state { + name + color + } } } } From d1d1398d807167f81919eb84332845567c7fb575 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:37:26 +0530 Subject: [PATCH 02/44] feat: Rewrite `customAttributeMixin` to validation helper (#9916) # Pull Request Template ## Description This PR will replace the use of `customAttributeMixin` with `shared/helpers/Validators` helper. Fixes https://linear.app/chatwoot/issue/CW-3446/rewrite-customattributemixin-mixin-to-a-composable **Files updated** 1. widget/components/PreChat/Form.vue 2. dashboard/components/CustomAttribute.vue 3. dashboard/routes/dashboard/settings/attributes/EditAttribute.vue ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Test the custom validation is working or not with the custom attributes. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../dashboard/components/CustomAttribute.vue | 6 +- .../dashboard/mixins/customAttributeMixin.js | 11 ---- .../settings/attributes/EditAttribute.vue | 5 +- app/javascript/shared/helpers/Validators.js | 59 +++++++++++++++++++ .../helpers/specs/ValidatorsHelper.spec.js | 38 ++++++++++++ .../widget/components/PreChat/Form.vue | 14 ++--- 6 files changed, 105 insertions(+), 28 deletions(-) delete mode 100644 app/javascript/dashboard/mixins/customAttributeMixin.js diff --git a/app/javascript/dashboard/components/CustomAttribute.vue b/app/javascript/dashboard/components/CustomAttribute.vue index e42660ed7..4bf97320b 100644 --- a/app/javascript/dashboard/components/CustomAttribute.vue +++ b/app/javascript/dashboard/components/CustomAttribute.vue @@ -5,7 +5,7 @@ import { BUS_EVENTS } from 'shared/constants/busEvents'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue'; import { isValidURL } from '../helper/URLHelper'; -import customAttributeMixin from '../mixins/customAttributeMixin'; +import { getRegexp } from 'shared/helpers/Validators'; import { useVuelidate } from '@vuelidate/core'; const DATE_FORMAT = 'yyyy-MM-dd'; @@ -15,7 +15,6 @@ export default { MultiselectDropdown, HelperTextPopup, }, - mixins: [customAttributeMixin], props: { label: { type: String, required: true }, description: { type: String, default: '' }, @@ -128,8 +127,7 @@ export default { required, regexValidation: value => { return !( - this.attributeRegex && - !this.getRegexp(this.attributeRegex).test(value) + this.attributeRegex && !getRegexp(this.attributeRegex).test(value) ); }, }, diff --git a/app/javascript/dashboard/mixins/customAttributeMixin.js b/app/javascript/dashboard/mixins/customAttributeMixin.js deleted file mode 100644 index a0617685d..000000000 --- a/app/javascript/dashboard/mixins/customAttributeMixin.js +++ /dev/null @@ -1,11 +0,0 @@ -export default { - methods: { - getRegexp(regexPatternValue) { - let lastSlash = regexPatternValue.lastIndexOf('/'); - return new RegExp( - regexPatternValue.slice(1, lastSlash), - regexPatternValue.slice(lastSlash + 1) - ); - }, - }, -}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue b/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue index 0d237d866..03d78e8d1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/EditAttribute.vue @@ -2,11 +2,10 @@ import { useVuelidate } from '@vuelidate/core'; import { useAlert } from 'dashboard/composables'; import { required, minLength } from '@vuelidate/validators'; +import { getRegexp } from 'shared/helpers/Validators'; import { ATTRIBUTE_TYPES } from './constants'; -import customAttributeMixin from '../../../../mixins/customAttributeMixin'; export default { components: {}, - mixins: [customAttributeMixin], props: { selectedAttribute: { type: Object, @@ -116,7 +115,7 @@ export default { }, setFormValues() { const regexPattern = this.selectedAttribute.regex_pattern - ? this.getRegexp(this.selectedAttribute.regex_pattern).source + ? getRegexp(this.selectedAttribute.regex_pattern).source : null; this.displayName = this.selectedAttribute.attribute_display_name; this.description = this.selectedAttribute.attribute_description; diff --git a/app/javascript/shared/helpers/Validators.js b/app/javascript/shared/helpers/Validators.js index d8cf0b352..6502fef39 100644 --- a/app/javascript/shared/helpers/Validators.js +++ b/app/javascript/shared/helpers/Validators.js @@ -1,22 +1,58 @@ +/** + * Checks if a string is a valid E.164 phone number format. + * @param {string} value - The phone number to validate. + * @returns {boolean} True if the number is in E.164 format, false otherwise. + */ export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/); +/** + * Validates a phone number after removing the dial code. + * @param {string} value - The full phone number including dial code. + * @param {string} dialCode - The dial code to remove before validation. + * @returns {boolean} True if the number (without dial code) is valid, false otherwise. + */ export const isPhoneNumberValid = (value, dialCode) => { const number = value.replace(dialCode, ''); return !!number.match(/^[0-9]{1,14}$/); }; +/** + * Checks if a string is either a valid E.164 phone number or empty. + * @param {string} value - The phone number to validate. + * @returns {boolean} True if the number is in E.164 format or empty, false otherwise. + */ export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === ''; +/** + * Validates a phone number with dial code, requiring at least 5 digits. + * @param {string} value - The full phone number including dial code. + * @returns {boolean} True if the number is valid, false otherwise. + */ export const isPhoneNumberValidWithDialCode = value => { const number = value.replace(/^\+/, ''); // Remove the '+' sign return !!number.match(/^[1-9]\d{4,}$/); // Validate the phone number with minimum 5 digits }; +/** + * Checks if a string starts with a plus sign. + * @param {string} value - The string to check. + * @returns {boolean} True if the string starts with '+', false otherwise. + */ export const startsWithPlus = value => value.startsWith('+'); +/** + * Checks if a string is a valid URL (starts with 'http') or is empty. + * @param {string} [value=''] - The string to check. + * @returns {boolean} True if the string is a valid URL or empty, false otherwise. + */ export const shouldBeUrl = (value = '') => value ? value.startsWith('http') : true; +/** + * Validates a password for complexity requirements. + * @param {string} value - The password to validate. + * @returns {boolean} True if the password meets all requirements, false otherwise. + */ export const isValidPassword = value => { const containsUppercase = /[A-Z]/.test(value); const containsLowercase = /[a-z]/.test(value); @@ -32,8 +68,18 @@ export const isValidPassword = value => { ); }; +/** + * Checks if a string consists only of digits. + * @param {string} value - The string to check. + * @returns {boolean} True if the string contains only digits, false otherwise. + */ export const isNumber = value => /^\d+$/.test(value); +/** + * Validates a domain name. + * @param {string} value - The domain name to validate. + * @returns {boolean} True if the domain is valid or empty, false otherwise. + */ export const isDomain = value => { if (value !== '') { const domainRegex = /^([\p{L}0-9]+(-[\p{L}0-9]+)*\.)+[a-z]{2,}$/gmu; @@ -41,3 +87,16 @@ export const isDomain = value => { } return true; }; + +/** + * Creates a RegExp object from a string representation of a regular expression. + * @param {string} regexPatternValue - The string representation of the regex (e.g., '/pattern/flags'). + * @returns {RegExp} A RegExp object created from the input string. + */ +export const getRegexp = regexPatternValue => { + let lastSlash = regexPatternValue.lastIndexOf('/'); + return new RegExp( + regexPatternValue.slice(1, lastSlash), + regexPatternValue.slice(lastSlash + 1) + ); +}; diff --git a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js index 6d65cb36e..fc15b772c 100644 --- a/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js +++ b/app/javascript/shared/helpers/specs/ValidatorsHelper.spec.js @@ -8,6 +8,7 @@ import { isPhoneNumberValid, isNumber, isDomain, + getRegexp, } from '../Validators'; describe('#shouldBeUrl', () => { @@ -115,3 +116,40 @@ describe('#startsWithPlus', () => { expect(startsWithPlus('123456789')).toEqual(false); }); }); + +describe('#getRegexp', () => { + it('should create a correct RegExp object', () => { + const regexPattern = '/^[a-z]+$/i'; + const regex = getRegexp(regexPattern); + + expect(regex).toBeInstanceOf(RegExp); + expect(regex.toString()).toBe(regexPattern); + + expect(regex.test('abc')).toBe(true); + expect(regex.test('ABC')).toBe(true); + expect(regex.test('123')).toBe(false); + }); + + it('should handle regex with flags', () => { + const regexPattern = '/hello/gi'; + const regex = getRegexp(regexPattern); + + expect(regex).toBeInstanceOf(RegExp); + expect(regex.toString()).toBe(regexPattern); + + expect(regex.test('hello')).toBe(true); + expect(regex.test('HELLO')).toBe(false); + expect(regex.test('Hello World')).toBe(true); + }); + + it('should handle regex with special characters', () => { + const regexPattern = '/\\d{3}-\\d{2}-\\d{4}/'; + const regex = getRegexp(regexPattern); + + expect(regex).toBeInstanceOf(RegExp); + expect(regex.toString()).toBe(regexPattern); + + expect(regex.test('123-45-6789')).toBe(true); + expect(regex.test('12-34-5678')).toBe(false); + }); +}); diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 9b8da3798..596ba16d9 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -3,25 +3,19 @@ import CustomButton from 'shared/components/Button.vue'; import Spinner from 'shared/components/Spinner.vue'; import { mapGetters } from 'vuex'; import { getContrastingTextColor } from '@chatwoot/utils'; -import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import { isEmptyObject } from 'widget/helpers/utils'; +import { getRegexp } from 'shared/helpers/Validators'; +import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import routerMixin from 'widget/mixins/routerMixin'; import darkModeMixin from 'widget/mixins/darkModeMixin'; import configMixin from 'widget/mixins/configMixin'; -import customAttributeMixin from '../../../dashboard/mixins/customAttributeMixin'; export default { components: { CustomButton, Spinner, }, - mixins: [ - routerMixin, - darkModeMixin, - messageFormatterMixin, - configMixin, - customAttributeMixin, - ], + mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin], props: { options: { type: Object, @@ -184,7 +178,7 @@ export default { return this.formValues[name] || null; }, getValidation({ type, name, field_type, regex_pattern }) { - let regex = regex_pattern ? this.getRegexp(regex_pattern) : null; + let regex = regex_pattern ? getRegexp(regex_pattern) : null; const validations = { emailAddress: 'email', phoneNumber: ['startsWithPlus', 'isValidPhoneNumber'], From 3558878ae2de44dc3fc9aad808beb1df9c8becd2 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:40:06 +0530 Subject: [PATCH 03/44] feat: Replace the use of `macroMixin` with a composable (#9912) # Pull Request Template ## Description This PR will replace usage of `macroMixin` with the `useMacros` composable. And updated components from option API to composition API. **Files updated** 1. dashboard/routes/dashboard/settings/macros/MacroNode.vue 2. dashboard/routes/dashboard/settings/macros/MacroEditor.vue Fixes https://linear.app/chatwoot/issue/CW-3449/rewrite-macrosmixin-mixin-to-a-composable ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? **Test cases** 1. Check whether we can create a new macro. 2. Check whether validations and error animation are working or not. 3. Ability to drag the macro files 4. Check whether the edit pages and functionality is working or not. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../composables/spec/useMacros.spec.js | 175 +++++++++++++ .../dashboard/composables/useMacros.js | 44 ++++ .../dashboard/mixins/macrosMixin.js | 34 --- .../dashboard/mixins/specs/macros.spec.js | 50 ---- .../dashboard/settings/macros/MacroEditor.vue | 234 +++++++++--------- .../dashboard/settings/macros/MacroNode.vue | 158 +++++------- tailwind.config.js | 7 + 7 files changed, 401 insertions(+), 301 deletions(-) create mode 100644 app/javascript/dashboard/composables/spec/useMacros.spec.js create mode 100644 app/javascript/dashboard/composables/useMacros.js delete mode 100644 app/javascript/dashboard/mixins/macrosMixin.js delete mode 100644 app/javascript/dashboard/mixins/specs/macros.spec.js diff --git a/app/javascript/dashboard/composables/spec/useMacros.spec.js b/app/javascript/dashboard/composables/spec/useMacros.spec.js new file mode 100644 index 000000000..d268547a5 --- /dev/null +++ b/app/javascript/dashboard/composables/spec/useMacros.spec.js @@ -0,0 +1,175 @@ +import { describe, it, expect, vi } from 'vitest'; +import { useMacros } from '../useMacros'; +import { useStoreGetters } from 'dashboard/composables/store'; +import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js'; + +vi.mock('dashboard/composables/store'); +vi.mock('dashboard/helper/automationHelper.js'); + +describe('useMacros', () => { + const mockLabels = [ + { + id: 6, + title: 'sales', + description: 'sales team', + color: '#8EA20F', + show_on_sidebar: true, + }, + { + id: 2, + title: 'billing', + description: 'billing', + color: '#4077DA', + show_on_sidebar: true, + }, + { + id: 1, + title: 'snoozed', + description: 'Items marked for later', + color: '#D12F42', + show_on_sidebar: true, + }, + { + id: 5, + title: 'mobile-app', + description: 'tech team', + color: '#2DB1CC', + show_on_sidebar: true, + }, + { + id: 14, + title: 'human-resources-department-with-long-title', + description: 'Test', + color: '#FF6E09', + show_on_sidebar: true, + }, + { + id: 22, + title: 'priority', + description: 'For important sales leads', + color: '#7E7CED', + show_on_sidebar: true, + }, + ]; + const mockTeams = [ + { + id: 1, + name: '⚙️ sales team', + description: 'This is our internal sales team', + allow_auto_assign: true, + account_id: 1, + is_member: true, + }, + { + id: 2, + name: '🤷‍♂️ fayaz', + description: 'Test', + allow_auto_assign: true, + account_id: 1, + is_member: true, + }, + { + id: 3, + name: '🇮🇳 apac sales', + description: 'Sales team for France Territory', + allow_auto_assign: true, + account_id: 1, + is_member: true, + }, + ]; + const mockAgents = [ + { + id: 1, + account_id: 1, + availability_status: 'offline', + auto_offline: true, + confirmed: true, + email: 'john@doe.com', + available_name: 'John Doe', + name: 'John Doe', + role: 'agent', + thumbnail: 'https://example.com/image.png', + }, + { + id: 9, + account_id: 1, + availability_status: 'offline', + auto_offline: true, + confirmed: true, + email: 'clark@kent.com', + available_name: 'Clark Kent', + name: 'Clark Kent', + role: 'agent', + thumbnail: '', + }, + ]; + + beforeEach(() => { + useStoreGetters.mockReturnValue({ + 'labels/getLabels': { value: mockLabels }, + 'teams/getTeams': { value: mockTeams }, + 'agents/getAgents': { value: mockAgents }, + }); + }); + + it('initializes computed properties correctly', () => { + const { getMacroDropdownValues } = useMacros(); + expect(getMacroDropdownValues('add_label')).toHaveLength(mockLabels.length); + expect(getMacroDropdownValues('assign_team')).toHaveLength( + mockTeams.length + ); + expect(getMacroDropdownValues('assign_agent')).toHaveLength( + mockAgents.length + 1 + ); // +1 for "Self" + }); + + it('returns teams for assign_team and send_email_to_team types', () => { + const { getMacroDropdownValues } = useMacros(); + expect(getMacroDropdownValues('assign_team')).toEqual(mockTeams); + expect(getMacroDropdownValues('send_email_to_team')).toEqual(mockTeams); + }); + + it('returns agents with "Self" option for assign_agent type', () => { + const { getMacroDropdownValues } = useMacros(); + const result = getMacroDropdownValues('assign_agent'); + expect(result[0]).toEqual({ id: 'self', name: 'Self' }); + expect(result.slice(1)).toEqual(mockAgents); + }); + + it('returns formatted labels for add_label and remove_label types', () => { + const { getMacroDropdownValues } = useMacros(); + const expectedLabels = mockLabels.map(i => ({ + id: i.title, + name: i.title, + })); + expect(getMacroDropdownValues('add_label')).toEqual(expectedLabels); + expect(getMacroDropdownValues('remove_label')).toEqual(expectedLabels); + }); + + it('returns PRIORITY_CONDITION_VALUES for change_priority type', () => { + const { getMacroDropdownValues } = useMacros(); + expect(getMacroDropdownValues('change_priority')).toEqual( + PRIORITY_CONDITION_VALUES + ); + }); + + it('returns an empty array for unknown types', () => { + const { getMacroDropdownValues } = useMacros(); + expect(getMacroDropdownValues('unknown_type')).toEqual([]); + }); + + it('handles empty data correctly', () => { + useStoreGetters.mockReturnValue({ + 'labels/getLabels': { value: [] }, + 'teams/getTeams': { value: [] }, + 'agents/getAgents': { value: [] }, + }); + + const { getMacroDropdownValues } = useMacros(); + expect(getMacroDropdownValues('add_label')).toEqual([]); + expect(getMacroDropdownValues('assign_team')).toEqual([]); + expect(getMacroDropdownValues('assign_agent')).toEqual([ + { id: 'self', name: 'Self' }, + ]); + }); +}); diff --git a/app/javascript/dashboard/composables/useMacros.js b/app/javascript/dashboard/composables/useMacros.js new file mode 100644 index 000000000..6967331ea --- /dev/null +++ b/app/javascript/dashboard/composables/useMacros.js @@ -0,0 +1,44 @@ +import { computed } from 'vue'; +import { useStoreGetters } from 'dashboard/composables/store'; +import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js'; + +/** + * Composable for handling macro-related functionality + * @returns {Object} An object containing the getMacroDropdownValues function + */ +export const useMacros = () => { + const getters = useStoreGetters(); + + const labels = computed(() => getters['labels/getLabels'].value); + const teams = computed(() => getters['teams/getTeams'].value); + const agents = computed(() => getters['agents/getAgents'].value); + + /** + * Get dropdown values based on the specified type + * @param {string} type - The type of dropdown values to retrieve + * @returns {Array} An array of dropdown values + */ + const getMacroDropdownValues = type => { + switch (type) { + case 'assign_team': + case 'send_email_to_team': + return teams.value; + case 'assign_agent': + return [{ id: 'self', name: 'Self' }, ...agents.value]; + case 'add_label': + case 'remove_label': + return labels.value.map(i => ({ + id: i.title, + name: i.title, + })); + case 'change_priority': + return PRIORITY_CONDITION_VALUES; + default: + return []; + } + }; + + return { + getMacroDropdownValues, + }; +}; diff --git a/app/javascript/dashboard/mixins/macrosMixin.js b/app/javascript/dashboard/mixins/macrosMixin.js deleted file mode 100644 index 4aea89227..000000000 --- a/app/javascript/dashboard/mixins/macrosMixin.js +++ /dev/null @@ -1,34 +0,0 @@ -import { mapGetters } from 'vuex'; -import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js'; -export default { - computed: { - ...mapGetters({ - labels: 'labels/getLabels', - teams: 'teams/getTeams', - agents: 'agents/getAgents', - }), - }, - methods: { - getDropdownValues(type) { - switch (type) { - case 'assign_team': - case 'send_email_to_team': - return this.teams; - case 'assign_agent': - return [{ id: 'self', name: 'Self' }, ...this.agents]; - case 'add_label': - case 'remove_label': - return this.labels.map(i => { - return { - id: i.title, - name: i.title, - }; - }); - case 'change_priority': - return PRIORITY_CONDITION_VALUES; - default: - return []; - } - }, - }, -}; diff --git a/app/javascript/dashboard/mixins/specs/macros.spec.js b/app/javascript/dashboard/mixins/specs/macros.spec.js deleted file mode 100644 index a8b3ee2b1..000000000 --- a/app/javascript/dashboard/mixins/specs/macros.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import { createWrapper } from '@vue/test-utils'; -import macrosMixin from '../macrosMixin'; -import Vue from 'vue'; -import { teams, labels, agents } from '../../helper/specs/macrosFixtures'; -import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js'; -describe('webhookMixin', () => { - describe('#getEventLabel', () => { - it('returns correct i18n translation:', () => { - const Component = { - render() {}, - title: 'MyComponent', - mixins: [macrosMixin], - data: () => { - return { - teams, - labels, - agents, - }; - }, - methods: { - $t(text) { - return text; - }, - }, - }; - - const resolvedLabels = labels.map(i => { - return { - id: i.title, - name: i.title, - }; - }); - - const Constructor = Vue.extend(Component); - const vm = new Constructor().$mount(); - const wrapper = createWrapper(vm); - expect(wrapper.vm.getDropdownValues('assign_team')).toEqual(teams); - expect(wrapper.vm.getDropdownValues('send_email_to_team')).toEqual(teams); - expect(wrapper.vm.getDropdownValues('add_label')).toEqual(resolvedLabels); - expect(wrapper.vm.getDropdownValues('assign_agent')).toEqual([ - { id: 'self', name: 'Self' }, - ...agents, - ]); - expect(wrapper.vm.getDropdownValues('change_priority')).toEqual( - PRIORITY_CONDITION_VALUES - ); - expect(wrapper.vm.getDropdownValues()).toEqual([]); - }); - }); -}); diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue index 1b02ba2e5..f29d822ff 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroEditor.vue @@ -1,126 +1,123 @@ - @@ -128,11 +125,12 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroNode.vue b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroNode.vue index 2e0a5aecd..649f3efbb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/macros/MacroNode.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/MacroNode.vue @@ -1,84 +1,80 @@ - - - diff --git a/tailwind.config.js b/tailwind.config.js index f4b0793f6..93fa296e5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -66,12 +66,19 @@ module.exports = { transform: 'translateX(1px)', }, }, + shake: { + '0%, 100%': { transform: 'translateX(0)' }, + '25%': { transform: 'translateX(0.234375rem)' }, + '50%': { transform: 'translateX(-0.234375rem)' }, + '75%': { transform: 'translateX(0.234375rem)' }, + }, }, animation: { ...defaultTheme.animation, wiggle: 'wiggle 0.5s ease-in-out', 'loader-pulse': 'loader-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite', 'card-select': 'card-select 0.25s ease-in-out', + shake: 'shake 0.3s ease-in-out 0s 2', }, }, plugins: [ From d5f34bf9d0d2e0cdd3773ad2b7cb76911cbf1c23 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Sun, 11 Aug 2024 10:09:08 +0530 Subject: [PATCH 04/44] feat: Replace `conversation/teamMixin` within the components (#9923) --- .../i18n/locale/en/teamsSettings.json | 3 ++- .../mixins/conversation/teamMixin.js | 22 ------------------- .../routes/dashboard/commands/commandbar.vue | 2 -- .../dashboard/commands/conversationHotKeys.js | 14 +++++++++++- .../conversation/ConversationAction.vue | 16 ++++++++++++-- 5 files changed, 29 insertions(+), 28 deletions(-) delete mode 100644 app/javascript/dashboard/mixins/conversation/teamMixin.js diff --git a/app/javascript/dashboard/i18n/locale/en/teamsSettings.json b/app/javascript/dashboard/i18n/locale/en/teamsSettings.json index c39c03569..6cbe55032 100644 --- a/app/javascript/dashboard/i18n/locale/en/teamsSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/teamsSettings.json @@ -7,7 +7,8 @@ "LEARN_MORE": "Learn more about teams", "LIST": { "404": "There are no teams created on this account.", - "EDIT_TEAM": "Edit team" + "EDIT_TEAM": "Edit team", + "NONE": "None" }, "CREATE_FLOW": { "CREATE": { diff --git a/app/javascript/dashboard/mixins/conversation/teamMixin.js b/app/javascript/dashboard/mixins/conversation/teamMixin.js deleted file mode 100644 index 745f91589..000000000 --- a/app/javascript/dashboard/mixins/conversation/teamMixin.js +++ /dev/null @@ -1,22 +0,0 @@ -import { mapGetters } from 'vuex'; - -export default { - computed: { - ...mapGetters({ teams: 'teams/getTeams' }), - hasAnAssignedTeam() { - return !!this.currentChat?.meta?.team; - }, - teamsList() { - if (this.hasAnAssignedTeam) { - return [ - { - id: 0, - name: 'None', - }, - ...this.teams, - ]; - } - return this.teams; - }, - }, -}; diff --git a/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue b/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue index dff1f53f4..136295228 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue +++ b/app/javascript/dashboard/routes/dashboard/commands/commandbar.vue @@ -8,7 +8,6 @@ import goToCommandHotKeys from './goToCommandHotKeys'; import appearanceHotKeys from './appearanceHotKeys'; import agentMixin from 'dashboard/mixins/agentMixin'; import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin'; -import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin'; import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events'; export default { @@ -18,7 +17,6 @@ export default { bulkActionsHotKeysMixin, inboxHotKeysMixin, conversationLabelMixin, - conversationTeamMixin, appearanceHotKeys, goToCommandHotKeys, ], diff --git a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js index 9c4a638b6..c680788fd 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js @@ -65,6 +65,7 @@ export default { currentChat: 'getSelectedChat', replyMode: 'draftMessages/getReplyEditorMode', contextMenuChatId: 'getContextMenuChatId', + teams: 'teams/getTeams', }), draftMessage() { return this.$store.getters['draftMessages/get'](this.draftKey); @@ -78,7 +79,18 @@ export default { conversationId() { return this.currentChat?.id; }, - + hasAnAssignedTeam() { + return !!this.currentChat?.meta?.team; + }, + teamsList() { + if (this.hasAnAssignedTeam) { + return [ + { id: 0, name: this.$t('TEAMS_SETTINGS.LIST.NONE') }, + ...this.teams, + ]; + } + return this.teams; + }, statusActions() { const isOpen = this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue index 96b30d1fd..d718d3598 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationAction.vue @@ -6,7 +6,6 @@ import ContactDetailsItem from './ContactDetailsItem.vue'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import ConversationLabels from './labels/LabelBox.vue'; import agentMixin from 'dashboard/mixins/agentMixin'; -import teamMixin from 'dashboard/mixins/conversation/teamMixin'; import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; @@ -16,7 +15,7 @@ export default { MultiselectDropdown, ConversationLabels, }, - mixins: [agentMixin, teamMixin], + mixins: [agentMixin], props: { conversationId: { type: [Number, String], @@ -65,7 +64,20 @@ export default { ...mapGetters({ currentChat: 'getSelectedChat', currentUser: 'getCurrentUser', + teams: 'teams/getTeams', }), + hasAnAssignedTeam() { + return !!this.currentChat?.meta?.team; + }, + teamsList() { + if (this.hasAnAssignedTeam) { + return [ + { id: 0, name: this.$t('TEAMS_SETTINGS.LIST.NONE') }, + ...this.teams, + ]; + } + return this.teams; + }, assignedAgent: { get() { return this.currentChat.meta.assignee; From 4a63d1d89659bc5c41b6c62fd8222dfe4f2b433c Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 11 Aug 2024 20:59:39 -0700 Subject: [PATCH 05/44] feat: Update the design for label management page (#9932) This PR is part of the settings design update series. It updates the design for the label management page. I've made a few changes to the SettingsLayout page to reduce boilerplate code. --- .../dashboard/i18n/locale/en/labelsMgmt.json | 9 +- .../dashboard/settings/SettingsLayout.vue | 26 +- .../dashboard/settings/labels/Index.vue | 284 ++++++++---------- .../settings/labels/labels.routes.js | 9 +- 4 files changed, 160 insertions(+), 168 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json b/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json index cb98993bd..a24266fb4 100644 --- a/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json @@ -3,13 +3,18 @@ "HEADER": "Labels", "HEADER_BTN_TXT": "Add label", "LOADING": "Fetching labels", + "DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.", + "LEARN_MORE": "Learn more about labels", "SEARCH_404": "There are no items matching this query", - "SIDEBAR_TXT": "

Labels

Labels help you to categorize conversations and prioritize them. You can assign label to a conversation from the sidepanel.

Labels are tied to the account and can be used to create custom workflows in your organization. You can assign custom color to a label, it makes it easier to identify the label. You will be able to display the label on the sidebar to filter the conversations easily.

", "LIST": { "404": "There are no labels available in this account.", "TITLE": "Manage labels", "DESC": "Labels let you group the conversations together.", - "TABLE_HEADER": ["Name", "Description", "Color"] + "TABLE_HEADER": [ + "Name", + "Description", + "Color" + ] }, "FORM": { "NAME": { diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue index cfe642f54..916f821ad 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue @@ -4,21 +4,35 @@ defineProps({ type: Boolean, default: false, }, + noRecordsFound: { + type: Boolean, + default: false, + }, loadingMessage: { type: String, default: '', }, + noRecordsMessage: { + type: String, + default: '', + }, }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue index 9aed65d00..0fdaf16e8 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue @@ -1,138 +1,137 @@ - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js b/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js index 088c565d0..9f5edc2dd 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js @@ -1,18 +1,13 @@ import { frontendURL } from '../../../../helper/URLHelper'; -const SettingsContent = () => import('../Wrapper.vue'); +const SettingsWrapper = () => import('../SettingsWrapper.vue'); const Index = () => import('./Index.vue'); export default { routes: [ { path: frontendURL('accounts/:accountId/settings/labels'), - component: SettingsContent, - props: { - headerTitle: 'LABEL_MGMT.HEADER', - icon: 'tag', - showNewButton: false, - }, + component: SettingsWrapper, children: [ { path: '', From 6196a6d99a169cc95c9dc562acc27e9da984a709 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 12 Aug 2024 15:08:06 +0530 Subject: [PATCH 06/44] fix: last_activity_at is nil when conv is created (#9934) The payload does not include last_activity_at when the conversation is created. Because of this the frontend is not able to sort the conversations when appending this. Another problem is that the last_activity_at is not always present, it is added only when a message is created, and it updates it. So this can be nil when the conversation is created, so we fallback to created_at only at the presentation layer --- app/models/conversation.rb | 4 +++ .../conversations/event_data_presenter.rb | 1 + spec/models/conversation_spec.rb | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 3a39b82fc..df4bbf31c 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -124,6 +124,10 @@ class Conversation < ApplicationRecord last_message_in_messaging_window?(messaging_window) end + def last_activity_at + self[:last_activity_at] || created_at + end + def last_incoming_message messages&.incoming&.last end diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index 67c7dc1dd..2617721ec 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -40,6 +40,7 @@ class Conversations::EventDataPresenter < SimpleDelegator { agent_last_seen_at: agent_last_seen_at.to_i, contact_last_seen_at: contact_last_seen_at.to_i, + last_activity_at: last_activity_at.to_i, timestamp: last_activity_at.to_i, created_at: created_at.to_i } diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 0ca7303ef..5813d8c3c 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -525,6 +525,7 @@ RSpec.describe Conversation do id: conversation.display_id, messages: [], labels: [], + last_activity_at: conversation.last_activity_at.to_i, inbox_id: conversation.inbox_id, status: conversation.status, contact_inbox: conversation.contact_inbox, @@ -881,4 +882,39 @@ RSpec.describe Conversation do expect(conversation.cached_label_list_array).to eq %w[customer-support enterprise paid-customer] end end + + describe '#last_activity_at' do + let(:conversation) { create(:conversation) } + let(:message_params) do + { + conversation: conversation, + account: conversation.account, + inbox: conversation.inbox, + sender: conversation.assignee + } + end + + context 'when a new conversation is created' do + it 'sets last_activity_at to the created_at time' do + expect(conversation.last_activity_at).to eq(conversation.created_at) + end + end + + context 'when a new message is added' do + it 'updates the last_activity_at to the new message\'s created_at time' do + message = create(:message, created_at: 1.hour.from_now, **message_params) + conversation.reload + expect(conversation.last_activity_at).to be_within(1.second).of(message.created_at) + end + end + + context 'when multiple messages are added' do + it 'sets last_activity_at to the most recent message\'s created_at time' do + create(:message, created_at: 2.hours.ago, **message_params) + latest_message = create(:message, created_at: 1.hour.from_now, **message_params) + conversation.reload + expect(conversation.last_activity_at).to be_within(1.second).of(latest_message.created_at) + end + end + end end From dcefd58240fdeff5b3edd1c8e1b41c4dc7ecddc5 Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 12 Aug 2024 15:10:42 +0530 Subject: [PATCH 07/44] Bump version to v3.11.1 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index 7b4ee064b..7a5e89326 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '3.11.0' + version: '3.11.1' development: <<: *shared diff --git a/package.json b/package.json index cce8ecd32..0e31bb65b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "3.11.0", + "version": "3.11.1", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From 6e12ba04abd3185c0471603440434725c37d8d9f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 12 Aug 2024 15:08:06 +0530 Subject: [PATCH 08/44] fix: last_activity_at is nil when conv is created (#9934) The payload does not include last_activity_at when the conversation is created. Because of this the frontend is not able to sort the conversations when appending this. Another problem is that the last_activity_at is not always present, it is added only when a message is created, and it updates it. So this can be nil when the conversation is created, so we fallback to created_at only at the presentation layer --- app/models/conversation.rb | 4 +++ .../conversations/event_data_presenter.rb | 1 + spec/models/conversation_spec.rb | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 3a39b82fc..df4bbf31c 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -124,6 +124,10 @@ class Conversation < ApplicationRecord last_message_in_messaging_window?(messaging_window) end + def last_activity_at + self[:last_activity_at] || created_at + end + def last_incoming_message messages&.incoming&.last end diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb index 67c7dc1dd..2617721ec 100644 --- a/app/presenters/conversations/event_data_presenter.rb +++ b/app/presenters/conversations/event_data_presenter.rb @@ -40,6 +40,7 @@ class Conversations::EventDataPresenter < SimpleDelegator { agent_last_seen_at: agent_last_seen_at.to_i, contact_last_seen_at: contact_last_seen_at.to_i, + last_activity_at: last_activity_at.to_i, timestamp: last_activity_at.to_i, created_at: created_at.to_i } diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 0ca7303ef..5813d8c3c 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -525,6 +525,7 @@ RSpec.describe Conversation do id: conversation.display_id, messages: [], labels: [], + last_activity_at: conversation.last_activity_at.to_i, inbox_id: conversation.inbox_id, status: conversation.status, contact_inbox: conversation.contact_inbox, @@ -881,4 +882,39 @@ RSpec.describe Conversation do expect(conversation.cached_label_list_array).to eq %w[customer-support enterprise paid-customer] end end + + describe '#last_activity_at' do + let(:conversation) { create(:conversation) } + let(:message_params) do + { + conversation: conversation, + account: conversation.account, + inbox: conversation.inbox, + sender: conversation.assignee + } + end + + context 'when a new conversation is created' do + it 'sets last_activity_at to the created_at time' do + expect(conversation.last_activity_at).to eq(conversation.created_at) + end + end + + context 'when a new message is added' do + it 'updates the last_activity_at to the new message\'s created_at time' do + message = create(:message, created_at: 1.hour.from_now, **message_params) + conversation.reload + expect(conversation.last_activity_at).to be_within(1.second).of(message.created_at) + end + end + + context 'when multiple messages are added' do + it 'sets last_activity_at to the most recent message\'s created_at time' do + create(:message, created_at: 2.hours.ago, **message_params) + latest_message = create(:message, created_at: 1.hour.from_now, **message_params) + conversation.reload + expect(conversation.last_activity_at).to be_within(1.second).of(latest_message.created_at) + end + end + end end From 0b0e26645564e61ab3e634a12467acc707d0b8ef Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 12 Aug 2024 15:10:42 +0530 Subject: [PATCH 09/44] Bump version to v3.11.1 --- config/app.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.yml b/config/app.yml index 7b4ee064b..7a5e89326 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '3.11.0' + version: '3.11.1' development: <<: *shared diff --git a/package.json b/package.json index 144cf1f3f..efcc03cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "3.11.0", + "version": "3.11.1", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From 96d60674aa53ee5a736f2d5a0bdf72e47d91fc51 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 12 Aug 2024 15:47:54 +0530 Subject: [PATCH 10/44] chore(deps): Update browserlistdb (#9933) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2a4381ecf..6bb82aa15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8287,9 +8287,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001214, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: - version "1.0.30001624" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001624.tgz" - integrity sha512-0dWnQG87UevOCPYaOR49CBcLBwoZLpws+k6W37nLjWUhumP1Isusj0p2u+3KhjNloRWK9OKMgjBBzPujQHw4nA== + version "1.0.30001651" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" + integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== capture-exit@^2.0.0: version "2.0.0" From 452096f4b2b54ca7f05400643765405d0f57c41d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:50:21 +0530 Subject: [PATCH 11/44] feat: Replace `rtlMixin` to a composable (#9924) This PR will replace the usage of `rtlMixin` to the `useUISettings` composable, and moved the method to component itself. --- app/javascript/dashboard/App.vue | 10 ++---- .../widgets/conversation/ReplyBox.vue | 5 ++- .../contacts/components/ContactsTable.vue | 24 +++++++------ .../components/NotificationPanel.vue | 9 ++--- .../settings/reports/components/CsatTable.vue | 11 +++--- .../components/overview/AgentTable.vue | 12 ++++--- .../dashboard/store/modules/accounts.js | 8 +++++ .../modules/specs/account/getters.spec.js | 34 +++++++++++++++++++ app/javascript/shared/mixins/rtlMixin.js | 27 --------------- .../shared/mixins/specs/rtlMixin.spec.js | 29 ---------------- 10 files changed, 74 insertions(+), 95 deletions(-) delete mode 100644 app/javascript/shared/mixins/rtlMixin.js delete mode 100644 app/javascript/shared/mixins/specs/rtlMixin.spec.js diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index efb89f489..da169d61f 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -10,7 +10,6 @@ import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue'; import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer.vue'; -import rtlMixin from 'shared/mixins/rtlMixin'; import { setColorTheme } from './helper/themeHelper'; import { isOnOnboardingView } from 'v3/helpers/RouteHelper'; import { @@ -32,9 +31,6 @@ export default { UpgradeBanner, PendingEmailVerificationBanner, }, - - mixins: [rtlMixin], - data() { return { showAddAccountModal: false, @@ -46,6 +42,7 @@ export default { computed: { ...mapGetters({ getAccount: 'accounts/getAccount', + isRTL: 'accounts/isRTL', currentUser: 'getCurrentUser', authUIFlags: 'getAuthUIFlags', accountUIFlags: 'accounts/getUIFlags', @@ -102,7 +99,6 @@ export default { this.getAccount(this.currentAccountId); const { pubsub_token: pubsubToken } = this.currentUser || {}; this.setLocale(locale); - this.updateRTLDirectionView(locale); this.latestChatwootVersion = latestChatwootVersion; vueActionCable.init(pubsubToken); this.reconnectService = new ReconnectService(this.$store, router); @@ -124,8 +120,8 @@ export default { v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" id="app" class="flex-grow-0 w-full h-full min-h-0 app-wrapper" - :class="{ 'app-rtl--wrapper': isRTLView }" - :dir="isRTLView ? 'rtl' : 'ltr'" + :class="{ 'app-rtl--wrapper': isRTL }" + :dir="isRTL ? 'rtl' : 'ltr'" >