diff --git a/.editorconfig b/.editorconfig index 7203adb09..2a5fe28cf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,8 +7,8 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -indent_style = spaces +indent_style = space tab_width = 2 -[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] +[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] indent_size = 2 diff --git a/.gitignore b/.gitignore index e4b14d2f5..11a8c50e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,6 @@ public/packs* *.un~ .jest-cache -#VS Code files -.vscode - # ignore jetbrains IDE files .idea @@ -62,4 +59,4 @@ package-lock.json test/cypress/videos/* /config/master.key -/config/*.enc \ No newline at end of file +/config/*.enc diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..254e696a4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,32 @@ +{ + "recommendations": [ + // Spell check + "streetsidesoftware.code-spell-checker", + // Better Comments + "aaron-bond.better-comments", + // Rails Test Runner + "davidpallinder.rails-test-runner", + // Eslint + "dbaeumer.vscode-eslint", + // Auto Close Tag + "formulahendry.auto-close-tag", + // Auto Rename Tag + "formulahendry.auto-rename-tag", + // Hight light colors + "naumovs.color-highlight", + // GitLens + "eamodio.gitlens", + // Ruby + "rebornix.ruby", + // Vue + "octref.vetur", + // Prettier + "esbenp.prettier-vscode", + // Dot Env + "mikestead.dotenv", + // HTML CSS Support + "ecmel.vscode-html-css", + // Tailwind CSS Intellisense + "bradlc.vscode-tailwindcss", + ] +} diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js index cc4287503..fa4633ec1 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js @@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [ label: 'HELP_CENTER.TITLE', featureFlag: 'help_center', toState: frontendURL(`accounts/${accountId}/portals`), - toStateName: 'list_all_portals', + toStateName: 'default_portal_articles', roles: ['administrator'], }, { diff --git a/app/javascript/dashboard/components/widgets/Avatar.vue b/app/javascript/dashboard/components/widgets/Avatar.vue index daf044288..df654ee7e 100644 --- a/app/javascript/dashboard/components/widgets/Avatar.vue +++ b/app/javascript/dashboard/components/widgets/Avatar.vue @@ -78,7 +78,7 @@ export default { if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) { initials = initials.replace(/[a-z]+/g, ''); } - initials = initials.substr(0, 2).toUpperCase(); + initials = initials.substring(0, 2).toUpperCase(); return initials; }, }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 1b0b9ca36..bbc2f017e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -477,7 +477,7 @@ export default { const hasNextWord = updatedMessage.includes(' '); const isShortCodeActive = this.hasSlashCommand && !hasNextWord; if (isShortCodeActive) { - this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length); + this.mentionSearchKey = updatedMessage.substring(1); this.showMentions = true; } else { this.mentionSearchKey = ''; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue index 9dda5c02c..c9294aa2a 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -239,7 +239,7 @@ export default { const hasNextWord = value.includes(' '); const isShortCodeActive = this.hasSlashCommand && !hasNextWord; if (isShortCodeActive) { - this.cannedResponseSearchKey = value.substr(1, value.length); + this.cannedResponseSearchKey = value.substring(1); this.showCannedResponseMenu = true; } else { this.cannedResponseSearchKey = ''; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue index 528b55575..562d6bd07 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/HelpCenterLayout.vue @@ -59,6 +59,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue'; import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel'; +import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import portalMixin from '../mixins/portalMixin'; import AddCategory from '../pages/categories/AddCategory'; @@ -72,7 +73,7 @@ export default { PortalPopover, AddCategory, }, - mixins: [portalMixin], + mixins: [portalMixin, uiSettingsMixin], data() { return { isSidebarOpen: false, @@ -231,7 +232,13 @@ export default { }, updated() { const slug = this.$route.params.portalSlug; - if (slug) this.lastActivePortalSlug = slug; + if (slug) { + this.lastActivePortalSlug = slug; + this.updateUISettings({ + last_active_portal_slug: slug, + last_active_locale_code: this.selectedLocaleInPortal, + }); + } }, methods: { handleResize() { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue index e0e002002..0354ad22b 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue @@ -188,13 +188,15 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index 01c46f5dc..7931016e4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -19,15 +19,11 @@ :script="currentInbox.callback_webhook_url" /> -
+

{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}

- +

{{ $t( @@ -36,7 +32,6 @@ }}

diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue index 8da5183d7..bdc20e14e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue @@ -243,7 +243,7 @@ export default { this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', { options: JSON.stringify(options), }) + - script.substring(13, script.length) + script.substring(13) ); }, getWidgetViewOptions() { diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 064033841..2fdd6c57b 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -10,7 +10,7 @@ import { getUserCookieName, hasUserKeys, } from '../sdk/cookieHelpers'; -import { addClass, removeClass } from '../sdk/DOMHelpers'; +import { addClasses, removeClasses } from '../sdk/DOMHelpers'; import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents'; const runSDK = ({ baseUrl, websiteToken }) => { if (window.$chatwoot) { @@ -41,12 +41,12 @@ const runSDK = ({ baseUrl, websiteToken }) => { let widgetElm = document.querySelector('.woot--bubble-holder'); let widgetHolder = document.querySelector('.woot-widget-holder'); if (visibility === 'hide') { - addClass(widgetHolder, 'woot-widget--without-bubble'); - addClass(widgetElm, 'woot-hidden'); + addClasses(widgetHolder, 'woot-widget--without-bubble'); + addClasses(widgetElm, 'woot-hidden'); window.$chatwoot.hideMessageBubble = true; } else if (visibility === 'show') { - removeClass(widgetElm, 'woot-hidden'); - removeClass(widgetHolder, 'woot-widget--without-bubble'); + removeClasses(widgetElm, 'woot-hidden'); + removeClasses(widgetHolder, 'woot-widget--without-bubble'); window.$chatwoot.hideMessageBubble = false; } IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, { diff --git a/app/javascript/sdk/DOMHelpers.js b/app/javascript/sdk/DOMHelpers.js index 2ac6188e6..47a45cc78 100644 --- a/app/javascript/sdk/DOMHelpers.js +++ b/app/javascript/sdk/DOMHelpers.js @@ -3,68 +3,20 @@ import { IFrameHelper } from './IFrameHelper'; export const loadCSS = () => { const css = document.createElement('style'); - css.type = 'text/css'; css.innerHTML = `${SDK_CSS}`; document.body.appendChild(css); }; -export const wootOn = (elm, event, fn) => { - if (document.addEventListener) { - elm.addEventListener(event, fn, false); - } else if (document.attachEvent) { - // <= IE 8 loses scope so need to apply, we add this to object so we - // can detach later (can't detach anonymous functions) - // eslint-disable-next-line - elm[event + fn] = function() { - // eslint-disable-next-line - return fn.apply(elm, arguments); - }; - elm.attachEvent(`on${event}`, elm[event + fn]); - } -}; - -export const classHelper = (classes, action, elm) => { - let search; - let replace; - let i; - let has = false; - if (classes) { - // Trim any whitespace - const classarray = classes.split(/\s+/); - for (i = 0; i < classarray.length; i += 1) { - search = new RegExp(`\\b${classarray[i]}\\b`, 'g'); - replace = new RegExp(` *${classarray[i]}\\b`, 'g'); - if (action === 'remove') { - // eslint-disable-next-line - elm.className = elm.className.replace(replace, ''); - } else if (action === 'toggle') { - // eslint-disable-next-line - elm.className = elm.className.match(search) - ? elm.className.replace(replace, '') - : `${elm.className} ${classarray[i]}`; - } else if (action === 'has') { - if (elm.className.match(search)) { - has = true; - break; - } - } - } - } - return has; -}; - -export const addClass = (elm, classes) => { - if (classes) { - elm.className += ` ${classes}`; - } +export const addClasses = (elm, classes) => { + elm.classList.add(...classes.split(' ')); }; export const toggleClass = (elm, classes) => { - classHelper(classes, 'toggle', elm); + elm.classList.toggle(classes); }; -export const removeClass = (elm, classes) => { - classHelper(classes, 'remove', elm); +export const removeClasses = (elm, classes) => { + elm.classList.remove(...classes.split(' ')); }; export const onLocationChange = ({ referrerURL, referrerHost }) => { diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 47596bce3..bbc2cfafd 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -1,9 +1,8 @@ import Cookies from 'js-cookie'; import { - wootOn, - addClass, + addClasses, loadCSS, - removeClass, + removeClasses, onLocationChangeListener, } from './DOMHelpers'; import { @@ -68,7 +67,7 @@ export const IFrameHelper = { holderClassName += ` woot-widget-holder--flat`; } - addClass(widgetHolder, holderClassName); + addClasses(widgetHolder, holderClassName); widgetHolder.appendChild(iframe); body.appendChild(widgetHolder); IFrameHelper.initPostMessageCommunication(); @@ -99,7 +98,7 @@ export const IFrameHelper = { }; }, initWindowSizeListener: () => { - wootOn(window, 'resize', () => IFrameHelper.toggleCloseButton()); + window.addEventListener('resize', () => IFrameHelper.toggleCloseButton()); }, preventDefaultScroll: () => { widgetHolder.addEventListener('wheel', event => { @@ -241,9 +240,9 @@ export const IFrameHelper = { event.unreadMessageCount > 0 && !bubbleElement.classList.contains('unread-notification') ) { - addClass(bubbleElement, 'unread-notification'); + addClasses(bubbleElement, 'unread-notification'); } else if (event.unreadMessageCount === 0) { - removeClass(bubbleElement, 'unread-notification'); + removeClasses(bubbleElement, 'unread-notification'); } }, @@ -284,7 +283,7 @@ export const IFrameHelper = { target: chatBubble, }); - addClass(closeBubble, closeBtnClassName); + addClasses(closeBubble, closeBtnClassName); chatIcon.style.background = widgetColor; closeBubble.style.background = widgetColor; diff --git a/app/javascript/sdk/bubbleHelpers.js b/app/javascript/sdk/bubbleHelpers.js index b1ef9110f..5eab20f5d 100644 --- a/app/javascript/sdk/bubbleHelpers.js +++ b/app/javascript/sdk/bubbleHelpers.js @@ -1,4 +1,4 @@ -import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers'; +import { addClasses, removeClasses, toggleClass } from './DOMHelpers'; import { IFrameHelper } from './IFrameHelper'; import { isExpandedView } from './settingsHelper'; @@ -41,14 +41,14 @@ export const createBubbleIcon = ({ className, src, target }) => { export const createBubbleHolder = hideMessageBubble => { if (hideMessageBubble) { - addClass(bubbleHolder, 'woot-hidden'); + addClasses(bubbleHolder, 'woot-hidden'); } - addClass(bubbleHolder, 'woot--bubble-holder'); + addClasses(bubbleHolder, 'woot--bubble-holder'); body.appendChild(bubbleHolder); }; export const createNotificationBubble = () => { - addClass(notificationBubble, 'woot--notification'); + addClasses(notificationBubble, 'woot--notification'); return notificationBubble; }; @@ -71,15 +71,15 @@ export const onBubbleClick = (props = {}) => { }; export const onClickChatBubble = () => { - wootOn(bubbleHolder, 'click', onBubbleClick); + bubbleHolder.addEventListener('click', onBubbleClick); }; export const addUnreadClass = () => { const holderEl = document.querySelector('.woot-widget-holder'); - addClass(holderEl, 'has-unread-view'); + addClasses(holderEl, 'has-unread-view'); }; export const removeUnreadClass = () => { const holderEl = document.querySelector('.woot-widget-holder'); - removeClass(holderEl, 'has-unread-view'); + removeClasses(holderEl, 'has-unread-view'); }; diff --git a/app/javascript/shared/mixins/campaignMixin.js b/app/javascript/shared/mixins/campaignMixin.js index b9353ff29..ebf98aa37 100644 --- a/app/javascript/shared/mixins/campaignMixin.js +++ b/app/javascript/shared/mixins/campaignMixin.js @@ -1,10 +1,10 @@ import { CAMPAIGN_TYPES } from '../constants/campaign'; + export default { computed: { campaignType() { const pageURL = window.location.href; - const type = pageURL.substr(pageURL.lastIndexOf('/') + 1); - return type; + return pageURL.substring(pageURL.lastIndexOf('/') + 1); }, isOngoingType() { return this.campaignType === CAMPAIGN_TYPES.ONGOING; diff --git a/app/javascript/survey/views/Response.vue b/app/javascript/survey/views/Response.vue index 6ec4ec100..828a9cbee 100644 --- a/app/javascript/survey/views/Response.vue +++ b/app/javascript/survey/views/Response.vue @@ -93,7 +93,7 @@ export default { computed: { surveyId() { const pageURL = window.location.href; - return pageURL.substr(pageURL.lastIndexOf('/') + 1); + return pageURL.substring(pageURL.lastIndexOf('/') + 1); }, isRatingSubmitted() { return this.surveyDetails && this.surveyDetails.rating; diff --git a/app/models/working_hour.rb b/app/models/working_hour.rb index 885165da2..01b972bd4 100644 --- a/app/models/working_hour.rb +++ b/app/models/working_hour.rb @@ -40,7 +40,10 @@ class WorkingHour < ApplicationRecord validate :open_all_day_and_closed_all_day def self.today - find_by(day_of_week: Date.current.wday) + # While getting the day of the week, consider the timezone as well. `first` would + # return the first working hour from the list of working hours available per week. + inbox = first.inbox + find_by(day_of_week: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday) end def open_at?(time) diff --git a/spec/models/working_hour_spec.rb b/spec/models/working_hour_spec.rb index c126f6f87..a5018a28e 100644 --- a/spec/models/working_hour_spec.rb +++ b/spec/models/working_hour_spec.rb @@ -88,4 +88,18 @@ RSpec.describe WorkingHour do 'Validation failed: open_all_day and closed_all_day cannot be true at the same time') end end + + context 'when on monday 9am in Sydney timezone' do + let(:inbox) { create(:inbox) } + + before do + Time.zone = 'Australia/Sydney' + inbox.update(timezone: 'Australia/Sydney') + travel_to '10.10.2022 9:00 AEDT' + end + + it 'is considered working hour' do + expect(described_class.today.open_now?).to be true + end + end end diff --git a/spec/services/contacts/filter_service_spec.rb b/spec/services/contacts/filter_service_spec.rb index 5f52a1a3c..7e6661aed 100644 --- a/spec/services/contacts/filter_service_spec.rb +++ b/spec/services/contacts/filter_service_spec.rb @@ -167,6 +167,7 @@ describe ::Contacts::FilterService do context 'with x_days_before filter' do before do + Time.zone = 'UTC' el_contact.update(last_activity_at: (Time.zone.today - 4.days)) cs_contact.update(last_activity_at: (Time.zone.today - 5.days)) en_contact.update(last_activity_at: (Time.zone.today - 2.days)) diff --git a/spec/services/conversations/filter_service_spec.rb b/spec/services/conversations/filter_service_spec.rb index ed489bd30..a17351ec3 100644 --- a/spec/services/conversations/filter_service_spec.rb +++ b/spec/services/conversations/filter_service_spec.rb @@ -309,6 +309,7 @@ describe ::Conversations::FilterService do context 'with x_days_before filter' do before do + Time.zone = 'UTC' en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days)) en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days)) user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days))