From c17380d48a8c8f6e974b6414ca40f61fd2509219 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas Date: Fri, 19 Feb 2021 20:22:58 +0530 Subject: [PATCH 01/43] Feat: Create contact from contacts page (#1806) * Add contact create modal to contacts page * Test cases * Review fixes --- .../dashboard/i18n/locale/en/contact.json | 9 ++- .../contacts/components/ContactsView.vue | 9 ++- .../dashboard/contacts/components/Header.vue | 30 ++++++++-- .../conversation/contact/ContactForm.vue | 15 ++++- .../conversation/contact/CreateContact.vue | 55 +++++++++++++++++++ .../conversation/contact/EditContact.vue | 5 ++ .../store/modules/contacts/actions.js | 21 ++++++- .../modules/specs/contacts/actions.spec.js | 46 +++++++++++++++- app/javascript/shared/helpers/CustomErrors.js | 8 +++ 9 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/contact/CreateContact.vue diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index cc4e20bb5..f5cabeac7 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -45,6 +45,11 @@ "TITLE": "Edit contact", "DESC": "Edit contact details" }, + "CREATE_CONTACT": { + "BUTTON_LABEL": "New Contact", + "TITLE": "Create new contact", + "DESC": "Add basic information details about the contact." + }, "CONTACT_FORM": { "FORM": { "SUBMIT": "Submit", @@ -95,9 +100,9 @@ } } }, - "SUCCESS_MESSAGE": "Updated contact successfully", + "SUCCESS_MESSAGE": "Contact saved successfully", "CONTACT_ALREADY_EXIST": "This email address is in use for another contact.", - "ERROR_MESSAGE": "There was an error updating the contact, please try again" + "ERROR_MESSAGE": "There was an error, please try again" }, "CONTACTS_PAGE": { "HEADER": "Contacts", diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index b55efcf31..f483523eb 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -6,6 +6,7 @@ :on-search-submit="onSearchSubmit" this-selected-contact-id="" :on-input-search="onInputSearch" + :on-toggle-create="onToggleCreate" /> + @@ -34,6 +36,7 @@ import { mapGetters } from 'vuex'; import ContactsHeader from './Header'; import ContactsTable from './ContactsTable'; import ContactInfoPanel from './ContactInfoPanel'; +import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact'; import TableFooter from 'dashboard/components/widgets/TableFooter'; export default { @@ -42,11 +45,12 @@ export default { ContactsTable, TableFooter, ContactInfoPanel, + CreateContact, }, data() { return { searchQuery: '', - showEditModal: false, + showCreateModal: false, selectedContactId: '', }; }, @@ -123,6 +127,9 @@ export default { this.selectedContactId = ''; this.showContactInfoPanelPane = false; }, + onToggleCreate() { + this.showCreateModal = !this.showCreateModal; + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue index b8ca4c64a..8e774a4ba 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue @@ -24,6 +24,11 @@ @click="onSearchSubmit" /> + + @@ -45,6 +50,15 @@ export default { type: Function, default: () => {}, }, + onToggleCreate: { + type: Function, + default: () => {}, + }, + }, + data() { + return { + showCreateModal: false, + }; }, computed: { searchButtonClass() { @@ -69,35 +83,41 @@ export default { width: 100%; margin-bottom: var(--space-slab); } + +.right-aligned-wrap { + display: flex; +} + .search-wrap { width: 400px; - height: 3.6rem; + height: 3.8rem; display: flex; align-items: center; position: relative; + margin-right: var(--space-small); .search-icon { position: absolute; top: 1px; left: var(--space-one); - height: 3.6rem; + height: 3.8rem; line-height: 3.6rem; font-size: var(--font-size-medium); color: var(--b-700); } .contact-search { margin: 0; - height: 3.6rem; + height: 3.8rem; width: 100%; padding-left: var(--space-large); padding-right: 6rem; + border-color: var(--s-100); } .button { margin-left: var(--space-small); height: 3.2rem; - top: var(--space-micro); - right: var(--space-micro); + right: var(--space-smaller); position: absolute; padding: 0 var(--space-small); transition: transform 100ms linear; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue index 66f0e399f..dfa7b205f 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue @@ -81,7 +81,10 @@ diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue index 9d9569f62..b6bf2c8bd 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/EditContact.vue @@ -11,6 +11,8 @@ :contact="contact" :in-progress="uiFlags.isUpdating" :on-submit="onSubmit" + @success="onSuccess" + @cancel="onCancel" /> @@ -45,6 +47,9 @@ export default { onCancel() { this.$emit('cancel'); }, + onSuccess() { + this.$emit('cancel'); + }, async onSubmit(contactItem) { await this.$store.dispatch('contacts/update', contactItem); }, diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js index fd16a8de6..039b69d2c 100644 --- a/app/javascript/dashboard/store/modules/contacts/actions.js +++ b/app/javascript/dashboard/store/modules/contacts/actions.js @@ -1,4 +1,7 @@ -import { DuplicateContactException } from 'shared/helpers/CustomErrors'; +import { + DuplicateContactException, + ExceptionWithMessage, +} from 'shared/helpers/CustomErrors'; import types from '../../mutation-types'; import ContactAPI from '../../../api/contacts'; @@ -64,6 +67,22 @@ export const actions = { } }, + create: async ({ commit }, userObject) => { + commit(types.SET_CONTACT_UI_FLAG, { isCreating: true }); + try { + const response = await ContactAPI.create(userObject); + commit(types.SET_CONTACT_ITEM, response.data.payload.contact); + commit(types.SET_CONTACT_UI_FLAG, { isCreating: false }); + } catch (error) { + commit(types.SET_CONTACT_UI_FLAG, { isCreating: false }); + if (error.response?.data?.message) { + throw new ExceptionWithMessage(error.response.data.message); + } else { + throw new Error(error); + } + } + }, + updatePresence: ({ commit }, data) => { commit(types.UPDATE_CONTACTS_PRESENCE, data); }, diff --git a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js index 70e937b8b..4bc8b6723 100644 --- a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js @@ -2,7 +2,10 @@ import axios from 'axios'; import Contacts from '../../contacts'; import types from '../../../mutation-types'; import contactList from './fixtures'; -import { DuplicateContactException } from '../../../../../shared/helpers/CustomErrors'; +import { + DuplicateContactException, + ExceptionWithMessage, +} from '../../../../../shared/helpers/CustomErrors'; const { actions } = Contacts; @@ -95,6 +98,47 @@ describe('#actions', () => { }); }); + describe('#create', () => { + it('sends correct mutations if API is success', async () => { + axios.post.mockResolvedValue({ + data: { payload: { contact: contactList[0] } }, + }); + await actions.create({ commit }, contactList[0]); + expect(commit.mock.calls).toEqual([ + [types.SET_CONTACT_UI_FLAG, { isCreating: true }], + [types.SET_CONTACT_ITEM, contactList[0]], + [types.SET_CONTACT_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit }, contactList[0])).rejects.toThrow( + Error + ); + expect(commit.mock.calls).toEqual([ + [types.SET_CONTACT_UI_FLAG, { isCreating: true }], + [types.SET_CONTACT_UI_FLAG, { isCreating: false }], + ]); + }); + + it('sends correct actions if email is already present', async () => { + axios.post.mockRejectedValue({ + response: { + data: { + message: 'Email exists already', + }, + }, + }); + await expect(actions.create({ commit }, contactList[0])).rejects.toThrow( + ExceptionWithMessage + ); + expect(commit.mock.calls).toEqual([ + [types.SET_CONTACT_UI_FLAG, { isCreating: true }], + [types.SET_CONTACT_UI_FLAG, { isCreating: false }], + ]); + }); + }); + describe('#setContact', () => { it('returns correct mutations', () => { const data = { id: 1, name: 'john doe', availability_status: 'online' }; diff --git a/app/javascript/shared/helpers/CustomErrors.js b/app/javascript/shared/helpers/CustomErrors.js index b9e74e917..4f31eb291 100644 --- a/app/javascript/shared/helpers/CustomErrors.js +++ b/app/javascript/shared/helpers/CustomErrors.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ export class DuplicateContactException extends Error { constructor(data) { super('DUPLICATE_CONTACT'); @@ -5,3 +6,10 @@ export class DuplicateContactException extends Error { this.name = 'DuplicateContactException'; } } +export class ExceptionWithMessage extends Error { + constructor(data) { + super('ERROR_WITH_MESSAGE'); + this.data = data; + this.name = 'ExceptionWithMessage'; + } +} From 4cbdbbe4bdc9d99b3f7e6bf16275f07630aac9ca Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+sivin-git@users.noreply.github.com> Date: Mon, 22 Feb 2021 10:52:50 +0530 Subject: [PATCH 02/43] chore: Changes the dropdown ui (#1813) * Changes the dropdown ui --- .../assets/scss/plugins/_multiselect.scss | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 480e31c61..b7a27dff6 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -13,17 +13,16 @@ } .multiselect { - margin-bottom: $space-normal; - min-height: 38px; + margin-bottom: var(--space-normal); - &.multiselect--active { - >.multiselect__tags { + .multiselect--active { + > .multiselect__tags { border-color: $color-woot; } } .multiselect__select { - min-height: 44px; + min-height: 4.6rem; padding: 0; right: 0; top: 0; @@ -39,16 +38,51 @@ font-weight: $font-weight-normal; &.multiselect__option--highlight { - font-weight: $font-weight-medium; + background: var(--white); + color: var(--color-body); + } + + &.multiselect__option--highlight:hover { + background: var(--w-50); + color: var(--color-body); + + &::after { + background: var(--w-50); + color: var(--s-600); + } + } + + &.multiselect__option--highlight::after { + background: transparent; + } + + &.multiselect__option--selected { + background: var(--w-400); + color: var(--white); + + &.multiselect__option--highlight:hover { + background: var(--w-600); + color: var(--white); + + &::after { + background: transparent; + color: var(--white); + + &:hover { + color: var(--color-body); + } + } + } } } -} -.multiselect>.multiselect__tags { - @include margin(0); - border: 1px solid $color-border; - min-height: 44px; - padding-top: $zero; + .multiselect__tags { + @include margin(0); + border: 1px solid $color-border; + border-color: $color-border; + min-height: 4.4rem; + padding-top: $zero; + } .multiselect__tags-wrap { display: inline-block; @@ -59,7 +93,7 @@ .multiselect__placeholder { color: $color-gray; font-weight: $font-weight-normal; - padding-top: $space-small; + padding-top: var(--space-slab); } .multiselect__tag { @@ -79,14 +113,13 @@ @include ghost-input; @include padding($zero); font-size: $font-size-small; - + height: 4.4rem; margin-bottom: $zero; } .multiselect__single { - @include padding($space-one); - margin-bottom: 0; + padding: var(--space-slab) var(--space-one); } } @@ -96,26 +129,18 @@ .multiselect { cursor: pointer; } - - .multiselect>.multiselect__tags { - border-color: $color-border; - } - - .multiselect>.multiselect__select { - visibility: visible; - } } .multiselect { - >.multiselect__select { + > .multiselect__select { visibility: hidden; } - >.multiselect__tags { + > .multiselect__tags { border-color: transparent; } - &.multiselect--active>.multiselect__tags { + &.multiselect--active > .multiselect__tags { border-color: $color-woot; } } From a3b0de63de32cccbf3ef501e300b3eaa7bf260b7 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas Date: Mon, 22 Feb 2021 16:30:58 +0530 Subject: [PATCH 03/43] Feat: Component for setting business hour availability (#1794) * Component business hour setting availability --- .../dashboard/i18n/locale/en/inboxMgmt.json | 18 +- .../settings/inbox/components/BusinessDay.vue | 223 ++++++++++++++++++ .../settings/inbox/helpers/businessHour.js | 20 ++ .../inbox/helpers/specs/businessHour.spec.js | 17 ++ 4 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 16217e5ae..4cb645577 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -5,8 +5,7 @@ "LIST": { "404": "There are no inboxes attached to this account." }, - "CREATE_FLOW": [ - { + "CREATE_FLOW": [{ "title": "Choose Channel", "route": "settings_inbox_new", "body": "Choose the provider you want to integrate with Chatwoot." @@ -226,7 +225,8 @@ "SETTINGS": "Settings", "COLLABORATORS": "Collaborators", "CONFIGURATION": "Configuration", - "PRE_CHAT_FORM": "Pre Chat Form" + "PRE_CHAT_FORM": "Pre Chat Form", + "BUSINESS_HOURS": "Business Hours" }, "SETTINGS": "Settings", "FEATURES": { @@ -269,6 +269,18 @@ "REQUIRE_EMAIL": { "LABEL": "Visitors should provide their name and email address before starting the chat" } + }, + "BUSINESS_HOURS": { + "TITLE": "Set your availability", + "SUBTITLE": "Set your availability on your livechat widget", + "WEEKLY_TITLE": "Set your weekly hours", + "DAY": { + "ENABLE": "Enable availability for this day", + "UNAVAILABLE": "Unavailable", + "HOURS": "hours", + "VALIDATION_ERROR": "Starting time should be before closing time.", + "CHOOSE": "Choose" + } } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue new file mode 100644 index 000000000..6e2cbb151 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/BusinessDay.vue @@ -0,0 +1,223 @@ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js new file mode 100644 index 000000000..dba5ad7e0 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js @@ -0,0 +1,20 @@ +export const generateTimeSlots = (step = 15) => { + /* + Generates a list of time strings from 12:00 AM to next 24 hours. Each new string + will be generated by adding `step` minutes to the previous one. + The list is generated by starting with a random day and adding step minutes till end of the same day. + */ + const date = new Date(1970, 1, 1); + const slots = []; + while (date.getDate() === 1) { + slots.push( + date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: true, + }) + ); + date.setMinutes(date.getMinutes() + step); + } + return slots; +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js new file mode 100644 index 000000000..b5f0cbef7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/specs/businessHour.spec.js @@ -0,0 +1,17 @@ +import { generateTimeSlots } from '../businessHour'; + +describe('#generateTimeSlots', () => { + it('returns correct number of time slots', () => { + expect(generateTimeSlots(15).length).toStrictEqual((60 / 15) * 24); + }); + it('returns correct time slots', () => { + expect(generateTimeSlots(240)).toStrictEqual([ + '12:00 AM', + '04:00 AM', + '08:00 AM', + '12:00 PM', + '04:00 PM', + '08:00 PM', + ]); + }); +}); From 0e721653e5edd31bc56b1ad8c4751a7ca8ef78ef Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 23 Feb 2021 12:11:15 +0530 Subject: [PATCH 04/43] feat: Business hour Inbox APIs (#1821) * feat: Business hour Inbox APIs --- .../api/v1/accounts/inboxes_controller.rb | 3 +- app/models/account.rb | 1 - app/models/concerns/out_of_offisable.rb | 24 +++++++++++++- app/models/inbox.rb | 2 ++ app/models/working_hour.rb | 2 +- .../hook_execution_service.rb | 3 +- app/views/api/v1/models/_inbox.json.jbuilder | 4 +++ ...0222131048_change_working_hours_to_zero.rb | 11 +++++++ ..._switch_time_zone_from_account_to_inbox.rb | 6 ++++ db/schema.rb | 4 +-- lib/current.rb | 5 --- .../v1/accounts/inboxes_controller_spec.rb | 33 ++++++++++++++----- spec/models/concerns/out_of_offisable_spec.rb | 6 ++++ .../hook_execution_service_spec.rb | 5 +-- 14 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 db/migrate/20210222131048_change_working_hours_to_zero.rb create mode 100644 db/migrate/20210222131155_switch_time_zone_from_account_to_inbox.rb diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 6c078d28f..7928a3d41 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -23,6 +23,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def update @inbox.update(inbox_update_params.except(:channel)) + @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present? @inbox.channel.update!(inbox_update_params[:channel]) @@ -80,7 +81,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def inbox_update_params params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled, - :working_hours_enabled, :out_of_office_message, + :working_hours_enabled, :out_of_office_message, :timezone, channel: [ :website_url, :widget_color, diff --git a/app/models/account.rb b/app/models/account.rb index 1bb9f1a19..d6f004777 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -10,7 +10,6 @@ # name :string not null # settings_flags :integer default(0), not null # support_email :string(100) -# timezone :string default("UTC") # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/concerns/out_of_offisable.rb b/app/models/concerns/out_of_offisable.rb index 0bfcbde32..72d1e0467 100644 --- a/app/models/concerns/out_of_offisable.rb +++ b/app/models/concerns/out_of_offisable.rb @@ -3,6 +3,8 @@ module OutOfOffisable extend ActiveSupport::Concern + OFFISABLE_ATTRS = %w[day_of_week closed_all_day open_hour open_minutes close_hour close_minutes].freeze + included do has_many :working_hours, dependent: :destroy after_create :create_default_working_hours @@ -16,15 +18,35 @@ module OutOfOffisable !out_of_office? end + def weekly_schedule + working_hours.select(*OFFISABLE_ATTRS).as_json(except: :id) + end + + # accepts an array of hashes similiar to the format of weekly_schedule + # [ + # { "day_of_week"=>1, + # "closed_all_day"=>false, + # "open_hour"=>9, + # "open_minutes"=>0, + # "close_hour"=>17, + # "close_minutes"=>0},...] + def update_working_hours(params) + ActiveRecord::Base.transaction do + params.each do |working_hour| + working_hours.find_by(day_of_week: working_hour['day_of_week']).update(working_hour.slice(*OFFISABLE_ATTRS)) + end + end + end + private def create_default_working_hours + working_hours.create!(day_of_week: 0, closed_all_day: true) working_hours.create!(day_of_week: 1, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0) working_hours.create!(day_of_week: 2, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0) working_hours.create!(day_of_week: 3, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0) working_hours.create!(day_of_week: 4, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0) working_hours.create!(day_of_week: 5, open_hour: 9, open_minutes: 0, close_hour: 17, close_minutes: 0) working_hours.create!(day_of_week: 6, closed_all_day: true) - working_hours.create!(day_of_week: 7, closed_all_day: true) end end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 0e5fd490f..398af0352 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -12,6 +12,7 @@ # greeting_message :string # name :string not null # out_of_office_message :string +# timezone :string default("UTC") # working_hours_enabled :boolean default(FALSE) # created_at :datetime not null # updated_at :datetime not null @@ -29,6 +30,7 @@ class Inbox < ApplicationRecord include OutOfOffisable validates :account_id, presence: true + validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers } belongs_to :account diff --git a/app/models/working_hour.rb b/app/models/working_hour.rb index e73759ac0..5874af20d 100644 --- a/app/models/working_hour.rb +++ b/app/models/working_hour.rb @@ -37,7 +37,7 @@ class WorkingHour < ApplicationRecord validate :close_after_open, unless: :closed_all_day? def self.today - find_by(day_of_week: Date.current.cwday) + find_by(day_of_week: Date.current.wday) end def open_at?(time) diff --git a/app/services/message_templates/hook_execution_service.rb b/app/services/message_templates/hook_execution_service.rb index f8371aad9..05429127e 100644 --- a/app/services/message_templates/hook_execution_service.rb +++ b/app/services/message_templates/hook_execution_service.rb @@ -5,7 +5,8 @@ class MessageTemplates::HookExecutionService return if inbox.agent_bot_inbox&.active? ::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message? - ::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting? + # TODO: let's see whether this is needed and remove this and related logic if not + #::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting? ::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect? end diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 0ceaf8c5b..cee4e429d 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -4,6 +4,10 @@ json.name resource.name json.channel_type resource.channel_type json.greeting_enabled resource.greeting_enabled json.greeting_message resource.greeting_message +json.working_hours_enabled resource.working_hours_enabled +json.out_of_office_message resource.out_of_office_message +json.working_hours resource.weekly_schedule +json.timezone resource.timezone json.avatar_url resource.try(:avatar_url) json.page_id resource.channel.try(:page_id) json.widget_color resource.channel.try(:widget_color) diff --git a/db/migrate/20210222131048_change_working_hours_to_zero.rb b/db/migrate/20210222131048_change_working_hours_to_zero.rb new file mode 100644 index 000000000..7d163e55e --- /dev/null +++ b/db/migrate/20210222131048_change_working_hours_to_zero.rb @@ -0,0 +1,11 @@ +class ChangeWorkingHoursToZero < ActiveRecord::Migration[6.0] + # rubocop:disable Rails/SkipsModelValidations + def up + WorkingHour.where(day_of_week: 7).update_all(day_of_week: 0) + end + + def down + WorkingHour.where(day_of_week: 0).update_all(day_of_week: 7) + end + # rubocop:enable Rails/SkipsModelValidations +end diff --git a/db/migrate/20210222131155_switch_time_zone_from_account_to_inbox.rb b/db/migrate/20210222131155_switch_time_zone_from_account_to_inbox.rb new file mode 100644 index 000000000..b30d3dffc --- /dev/null +++ b/db/migrate/20210222131155_switch_time_zone_from_account_to_inbox.rb @@ -0,0 +1,6 @@ +class SwitchTimeZoneFromAccountToInbox < ActiveRecord::Migration[6.0] + def change + remove_column :accounts, :timezone, :string, default: 'UTC' + add_column :inboxes, :timezone, :string, default: 'UTC' + end +end diff --git a/db/schema.rb b/db/schema.rb index bf0f57714..db05d6392 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_02_19_085719) do +ActiveRecord::Schema.define(version: 2021_02_22_131155) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -50,7 +50,6 @@ ActiveRecord::Schema.define(version: 2021_02_19_085719) do t.integer "settings_flags", default: 0, null: false t.integer "feature_flags", default: 0, null: false t.integer "auto_resolve_duration" - t.string "timezone", default: "UTC" end create_table "action_mailbox_inbound_emails", force: :cascade do |t| @@ -303,6 +302,7 @@ ActiveRecord::Schema.define(version: 2021_02_19_085719) do t.string "email_address" t.boolean "working_hours_enabled", default: false t.string "out_of_office_message" + t.string "timezone", default: "UTC" t.index ["account_id"], name: "index_inboxes_on_account_id" end diff --git a/lib/current.rb b/lib/current.rb index 4490932f0..bfa380f43 100644 --- a/lib/current.rb +++ b/lib/current.rb @@ -3,11 +3,6 @@ module Current thread_mattr_accessor :account thread_mattr_accessor :account_user - def account=(account) - super - Time.zone = account.timezone - end - def self.reset Current.user = nil Current.account = nil diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 3b01c36fc..0c52e869c 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -141,7 +141,18 @@ RSpec.describe 'Inboxes API', type: :request do let(:admin) { create(:user, account: account, role: :administrator) } let(:valid_params) { { enable_auto_assignment: false, channel: { website_url: 'test.com' } } } - it 'updates inbox' do + it 'will not update inbox for agent' do + agent = create(:user, account: account, role: :agent) + + patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", + headers: agent.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + + it 'updates inbox when administrator' do patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", headers: admin.create_new_auth_token, params: valid_params, @@ -151,7 +162,7 @@ RSpec.describe 'Inboxes API', type: :request do expect(inbox.reload.enable_auto_assignment).to be_falsey end - it 'updates avatar' do + it 'updates avatar when administrator' do # no avatar before upload expect(inbox.avatar.attached?).to eq(false) file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') @@ -165,15 +176,19 @@ RSpec.describe 'Inboxes API', type: :request do expect(inbox.avatar.attached?).to eq(true) end - it 'will not update inbox for agent' do - agent = create(:user, account: account, role: :agent) - + it 'updates working hours when administrator' do + params = { + working_hours: [{ 'day_of_week' => 0, 'open_hour' => 9, 'open_minutes' => 0, 'close_hour' => 17, 'close_minutes' => 0 }], + working_hours_enabled: true, + out_of_office_message: 'hello' + } patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", - headers: agent.create_new_auth_token, - params: valid_params, - as: :json + params: valid_params.merge(params), + headers: admin.create_new_auth_token - expect(response).to have_http_status(:unauthorized) + expect(response).to have_http_status(:success) + inbox.reload + expect(inbox.reload.weekly_schedule.find { |schedule| schedule['day_of_week'] == 0 }['open_hour']).to eq 9 end end end diff --git a/spec/models/concerns/out_of_offisable_spec.rb b/spec/models/concerns/out_of_offisable_spec.rb index cbdef0e81..8f09f0753 100644 --- a/spec/models/concerns/out_of_offisable_spec.rb +++ b/spec/models/concerns/out_of_offisable_spec.rb @@ -16,4 +16,10 @@ shared_examples_for 'out_of_offisable' do travel_to '01.11.2020 13:00'.to_datetime expect(obj.out_of_office?).to be true end + + it 'updates the office hours via a hash' do + obj.update_working_hours([{ 'day_of_week' => 1, 'open_hour' => 10, 'open_minutes' => 0, + 'close_hour' => 17, 'close_minutes' => 0 }]) + expect(obj.reload.weekly_schedule.find { |schedule| schedule['day_of_week'] == 1 }['open_hour']).to eq 10 + end end diff --git a/spec/services/message_templates/hook_execution_service_spec.rb b/spec/services/message_templates/hook_execution_service_spec.rb index ce64d2bb2..0abc15790 100644 --- a/spec/services/message_templates/hook_execution_service_spec.rb +++ b/spec/services/message_templates/hook_execution_service_spec.rb @@ -19,8 +19,9 @@ describe ::MessageTemplates::HookExecutionService do # described class gets called in message after commit message = create(:message, conversation: conversation) - expect(::MessageTemplates::Template::Greeting).to have_received(:new).with(conversation: message.conversation) - expect(greeting_service).to have_received(:perform) + # TODO: remove this if this hook is removed + # expect(::MessageTemplates::Template::Greeting).to have_received(:new).with(conversation: message.conversation) + # expect(greeting_service).to have_received(:perform) expect(::MessageTemplates::Template::EmailCollect).to have_received(:new).with(conversation: message.conversation) expect(email_collect_service).to have_received(:perform) end From 01ee3d7f8b09a966bf4fa35d662dd7b8312f9651 Mon Sep 17 00:00:00 2001 From: Ankur Patel Date: Tue, 23 Feb 2021 09:34:49 -0500 Subject: [PATCH 05/43] chore: Removing unused index method from Widget Controller (#1819) --- app/controllers/widgets_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index da634875a..44a23568e 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -6,8 +6,6 @@ class WidgetsController < ActionController::Base before_action :build_contact after_action :allow_iframe_requests - def index; end - private def set_global_config From f424a832f435c108ce765fa87246fd34f4e0a6f3 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 23 Feb 2021 20:48:15 +0530 Subject: [PATCH 06/43] fix: Log config env variable typecasting (#1792) --- config/environments/production.rb | 4 ++-- config/environments/staging.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 5b9acb497..ced1f7013 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -76,8 +76,8 @@ Rails.application.configure do # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - - if ENV['RAILS_LOG_TO_STDOUT'].present? + + if ActiveModel::Type::Boolean.new.cast(ENV.fetch('RAILS_LOG_TO_STDOUT', true)) logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 5243a2d59..3adfe1e0c 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -94,7 +94,7 @@ Rails.application.configure do # require 'syslog/logger' config.logger = ActiveSupport::Logger.new(Rails.root.join("log/#{Rails.env}.log"), 1, ENV.fetch('LOG_SIZE', '1024').to_i.megabytes) - if ENV['RAILS_LOG_TO_STDOUT'].present? + if ActiveModel::Type::Boolean.new.cast(ENV.fetch('RAILS_LOG_TO_STDOUT', true)) logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) From 82a1ad891daaf8d523da80db1ff3df3939a1fe15 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 24 Feb 2021 11:16:38 +0530 Subject: [PATCH 07/43] feat: Add a view to assist user while onboarding (#1826) Co-authored-by: Nithin David --- .../widgets/conversation/EmptyState.vue | 61 ++++--- .../widgets/conversation/OnboardingView.vue | 157 ++++++++++++++++++ .../i18n/locale/en/conversation.json | 24 +++ 3 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/conversation/OnboardingView.vue diff --git a/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue b/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue index 958e1b9da..1aaf66c14 100644 --- a/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue +++ b/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue @@ -1,28 +1,31 @@