+
@@ -36,6 +48,10 @@
import { mapGetters } from 'vuex';
export default {
props: {
+ buttonLabel: {
+ type: String,
+ default: '',
+ },
items: {
type: Array,
default: () => [],
@@ -116,6 +132,7 @@ export default {
border-radius: $space-smaller;
border: 1px solid $color-border;
display: block;
+ font-family: inherit;
font-size: $font-size-default;
line-height: 1.5;
padding: $space-one;
@@ -126,6 +143,31 @@ export default {
}
}
+ textarea {
+ resize: none;
+ }
+
+ select {
+ width: 110%;
+ padding: $space-smaller;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border: 1px solid $color-border;
+ border-radius: $space-smaller;
+ background-color: $color-white;
+ font-family: inherit;
+ font-size: $space-normal;
+ font-weight: normal;
+ line-height: 1.5;
+ background-image: url("data:image/svg+xml;utf8,
");
+ background-origin: content-box;
+ background-position: right -1.6rem center;
+ background-repeat: no-repeat;
+ background-size: 9px 6px;
+ padding-right: 2.4rem;
+ }
+
.button {
font-size: $font-size-default;
}
diff --git a/app/javascript/shared/constants/busEvents.js b/app/javascript/shared/constants/busEvents.js
new file mode 100644
index 000000000..cae34e75b
--- /dev/null
+++ b/app/javascript/shared/constants/busEvents.js
@@ -0,0 +1,3 @@
+export const BUS_EVENTS = {
+ SET_TWEET_REPLY: 'SET_TWEET_REPLY',
+};
diff --git a/app/javascript/shared/constants/contentType.js b/app/javascript/shared/constants/contentType.js
new file mode 100644
index 000000000..e5b37217f
--- /dev/null
+++ b/app/javascript/shared/constants/contentType.js
@@ -0,0 +1,3 @@
+export const CONTENT_TYPES = {
+ INCOMING_EMAIL: 'incoming_email',
+};
diff --git a/app/javascript/shared/constants/messageTypes.js b/app/javascript/shared/constants/messageTypes.js
new file mode 100644
index 000000000..0dda2dadd
--- /dev/null
+++ b/app/javascript/shared/constants/messageTypes.js
@@ -0,0 +1,6 @@
+export const MESSAGE_TYPE = {
+ INCOMING: 0,
+ OUTGOING: 1,
+ ACTIVITY: 2,
+ TEMPLATE: 3,
+};
diff --git a/app/javascript/shared/helpers/CustomEventHelper.js b/app/javascript/shared/helpers/CustomEventHelper.js
new file mode 100644
index 000000000..df5d4bb0c
--- /dev/null
+++ b/app/javascript/shared/helpers/CustomEventHelper.js
@@ -0,0 +1,15 @@
+export const createEvent = eventName => {
+ let event;
+ if (typeof window.CustomEvent === 'function') {
+ event = new CustomEvent(eventName);
+ } else {
+ event = document.createEvent('CustomEvent');
+ event.initCustomEvent(eventName, false, false, null);
+ }
+ return event;
+};
+
+export const dispatchWindowEvent = eventName => {
+ const event = createEvent(eventName);
+ window.dispatchEvent(event);
+};
diff --git a/app/javascript/shared/helpers/KeyboardHelpers.js b/app/javascript/shared/helpers/KeyboardHelpers.js
new file mode 100644
index 000000000..38bf62321
--- /dev/null
+++ b/app/javascript/shared/helpers/KeyboardHelpers.js
@@ -0,0 +1,11 @@
+export const isEnter = e => {
+ return e.keyCode === 13;
+};
+
+export const isEscape = e => {
+ return e.keyCode === 27;
+};
+
+export const hasPressedShift = e => {
+ return e.shiftKey;
+};
diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js
index a5dc591e8..c62594b26 100644
--- a/app/javascript/shared/helpers/MessageFormatter.js
+++ b/app/javascript/shared/helpers/MessageFormatter.js
@@ -1,13 +1,32 @@
import { escapeHtml } from './HTMLSanitizer';
+const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
+const TWITTER_USERNAME_REPLACEMENT =
+ '$1
@$2';
+
+const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g;
+const TWITTER_HASH_REPLACEMENT =
+ '$1
#$2';
class MessageFormatter {
- constructor(message) {
- this.message = escapeHtml(message) || '';
+ constructor(message, isATweet = false) {
+ this.message = escapeHtml(message || '') || '';
+ this.isATweet = isATweet;
}
formatMessage() {
const linkifiedMessage = this.linkify();
- return linkifiedMessage.replace(/\n/g, '
');
+ const messageWithNextLines = linkifiedMessage.replace(/\n/g, '
');
+ if (this.isATweet) {
+ const messageWithUserName = messageWithNextLines.replace(
+ TWITTER_USERNAME_REGEX,
+ TWITTER_USERNAME_REPLACEMENT
+ );
+ return messageWithUserName.replace(
+ TWITTER_HASH_REGEX,
+ TWITTER_HASH_REPLACEMENT
+ );
+ }
+ return messageWithNextLines;
}
linkify() {
diff --git a/app/javascript/shared/helpers/MessageTypeHelper.js b/app/javascript/shared/helpers/MessageTypeHelper.js
index 67a5d366e..7dc4457b1 100644
--- a/app/javascript/shared/helpers/MessageTypeHelper.js
+++ b/app/javascript/shared/helpers/MessageTypeHelper.js
@@ -1,3 +1,10 @@
export const isAFormMessage = message => message.content_type === 'form';
export const isASubmittedFormMessage = (message = {}) =>
isAFormMessage(message) && !!message.content_attributes?.submitted_values;
+
+export const MESSAGE_MAX_LENGTH = {
+ GENERAL: 10000,
+ FACEBOOK: 640,
+ TWILIO_SMS: 160,
+ TWEET: 280,
+};
diff --git a/app/javascript/shared/helpers/specs/CustomEventHelper.spec.js b/app/javascript/shared/helpers/specs/CustomEventHelper.spec.js
new file mode 100644
index 000000000..f6fe0908f
--- /dev/null
+++ b/app/javascript/shared/helpers/specs/CustomEventHelper.spec.js
@@ -0,0 +1,9 @@
+import { dispatchWindowEvent } from '../CustomEventHelper';
+
+describe('dispatchWindowEvent', () => {
+ it('dispatches correct event', () => {
+ window.dispatchEvent = jest.fn();
+ dispatchWindowEvent('chatwoot:ready');
+ expect(dispatchEvent).toHaveBeenCalled();
+ });
+});
diff --git a/app/javascript/shared/helpers/specs/KeyboardHelpers.spec.js b/app/javascript/shared/helpers/specs/KeyboardHelpers.spec.js
new file mode 100644
index 000000000..851c7dfef
--- /dev/null
+++ b/app/javascript/shared/helpers/specs/KeyboardHelpers.spec.js
@@ -0,0 +1,21 @@
+import { isEnter, isEscape, hasPressedShift } from '../KeyboardHelpers';
+
+describe('#KeyboardHelpers', () => {
+ describe('#isEnter', () => {
+ it('return correct values', () => {
+ expect(isEnter({ keyCode: 13 })).toEqual(true);
+ });
+ });
+
+ describe('#isEscape', () => {
+ it('return correct values', () => {
+ expect(isEscape({ keyCode: 27 })).toEqual(true);
+ });
+ });
+
+ describe('#hasPressedShift', () => {
+ it('return correct values', () => {
+ expect(hasPressedShift({ shiftKey: true })).toEqual(true);
+ });
+ });
+});
diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js
index 441048497..2afc97e92 100644
--- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js
+++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js
@@ -10,4 +10,26 @@ describe('#MessageFormatter', () => {
);
});
});
+
+ describe('tweets', () => {
+ it('should return the same string if not tags or @mentions', () => {
+ const message = 'Chatwoot is an opensource tool';
+ expect(new MessageFormatter(message).formattedMessage).toEqual(message);
+ });
+
+ it('should add links to @mentions', () => {
+ const message =
+ '@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
+ expect(new MessageFormatter(message, true).formattedMessage).toEqual(
+ '
@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername'
+ );
+ });
+
+ it('should add links to #tags', () => {
+ const message = '#chatwootapp is an opensource tool';
+ expect(new MessageFormatter(message, true).formattedMessage).toEqual(
+ '
#chatwootapp is an opensource tool'
+ );
+ });
+ });
});
diff --git a/app/javascript/shared/mixins/alertMixin.js b/app/javascript/shared/mixins/alertMixin.js
index 61aa7e04e..ba0064f44 100644
--- a/app/javascript/shared/mixins/alertMixin.js
+++ b/app/javascript/shared/mixins/alertMixin.js
@@ -1,4 +1,3 @@
-/* global bus */
export default {
methods: {
showAlert(message) {
diff --git a/app/javascript/shared/mixins/contentTypeMixin.js b/app/javascript/shared/mixins/contentTypeMixin.js
new file mode 100644
index 000000000..149d7b7b7
--- /dev/null
+++ b/app/javascript/shared/mixins/contentTypeMixin.js
@@ -0,0 +1,9 @@
+import { CONTENT_TYPES } from '../constants/contentType';
+
+export default {
+ computed: {
+ isEmailContentType() {
+ return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
+ },
+ },
+};
diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js
new file mode 100644
index 000000000..361ffdce2
--- /dev/null
+++ b/app/javascript/shared/mixins/inboxMixin.js
@@ -0,0 +1,42 @@
+export const INBOX_TYPES = {
+ WEB: 'Channel::WebWidget',
+ FB: 'Channel::FacebookPage',
+ TWITTER: 'Channel::TwitterProfile',
+ TWILIO: 'Channel::TwilioSms',
+ API: 'Channel::Api',
+ EMAIL: 'Channel::Email',
+};
+
+export default {
+ computed: {
+ channelType() {
+ return this.inbox.channel_type;
+ },
+ isAPIInbox() {
+ return this.channelType === INBOX_TYPES.API;
+ },
+ isATwitterInbox() {
+ return this.channelType === INBOX_TYPES.TWITTER;
+ },
+ isAFacebookInbox() {
+ return this.channelType === INBOX_TYPES.FB;
+ },
+ isAWebWidgetInbox() {
+ return this.channelType === INBOX_TYPES.WEB;
+ },
+ isATwilioChannel() {
+ return this.channelType === INBOX_TYPES.TWILIO;
+ },
+ isAnEmailChannel() {
+ return this.channelType === INBOX_TYPES.EMAIL;
+ },
+ isATwilioSMSChannel() {
+ const { phone_number: phoneNumber = '' } = this.inbox;
+ return this.isATwilioChannel && !phoneNumber.startsWith('whatsapp');
+ },
+ isATwilioWhatsappChannel() {
+ const { phone_number: phoneNumber = '' } = this.inbox;
+ return this.isATwilioChannel && phoneNumber.startsWith('whatsapp');
+ },
+ },
+};
diff --git a/app/javascript/shared/mixins/messageFormatterMixin.js b/app/javascript/shared/mixins/messageFormatterMixin.js
index a15504eec..44c2fe212 100644
--- a/app/javascript/shared/mixins/messageFormatterMixin.js
+++ b/app/javascript/shared/mixins/messageFormatterMixin.js
@@ -2,8 +2,8 @@ import MessageFormatter from '../helpers/MessageFormatter';
export default {
methods: {
- formatMessage(message) {
- const messageFormatter = new MessageFormatter(message);
+ formatMessage(message, isATweet) {
+ const messageFormatter = new MessageFormatter(message, isATweet);
return messageFormatter.formattedMessage;
},
truncateMessage(description = '') {
diff --git a/app/javascript/shared/mixins/specs/contentTypeMixin.spec.js b/app/javascript/shared/mixins/specs/contentTypeMixin.spec.js
new file mode 100644
index 000000000..a523c0513
--- /dev/null
+++ b/app/javascript/shared/mixins/specs/contentTypeMixin.spec.js
@@ -0,0 +1,32 @@
+import { shallowMount } from '@vue/test-utils';
+import contentTypeMixin from '../contentTypeMixin';
+
+describe('contentTypeMixin', () => {
+ it('returns true if contentType is incoming_email', () => {
+ const Component = {
+ render() {},
+ mixins: [contentTypeMixin],
+ computed: {
+ contentType() {
+ return 'incoming_email';
+ },
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isEmailContentType).toBe(true);
+ });
+
+ it('returns false if contentType is not incoming_email', () => {
+ const Component = {
+ render() {},
+ mixins: [contentTypeMixin],
+ computed: {
+ contentType() {
+ return 'input_select';
+ },
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isEmailContentType).toBe(false);
+ });
+});
diff --git a/app/javascript/shared/mixins/specs/inboxMixin.spec.js b/app/javascript/shared/mixins/specs/inboxMixin.spec.js
new file mode 100644
index 000000000..093f36b57
--- /dev/null
+++ b/app/javascript/shared/mixins/specs/inboxMixin.spec.js
@@ -0,0 +1,114 @@
+import { shallowMount } from '@vue/test-utils';
+import inboxMixin from '../inboxMixin';
+
+describe('inboxMixin', () => {
+ it('returns the correct channel type', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return { inbox: { channel_type: 'Channel::WebWidget' } };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.channelType).toBe('Channel::WebWidget');
+ });
+
+ it('isAPIInbox returns true if channel type is API', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return { inbox: { channel_type: 'Channel::Api' } };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isAPIInbox).toBe(true);
+ });
+
+ it('isATwitterInbox returns true if channel type is twitter', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return { inbox: { channel_type: 'Channel::TwitterProfile' } };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isATwitterInbox).toBe(true);
+ });
+
+ it('isAFacebookInbox returns true if channel type is Facebook', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return { inbox: { channel_type: 'Channel::FacebookPage' } };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isAFacebookInbox).toBe(true);
+ });
+
+ it('isAWebWidgetInbox returns true if channel type is Facebook', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return { inbox: { channel_type: 'Channel::WebWidget' } };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isAWebWidgetInbox).toBe(true);
+ });
+
+ it('isATwilioChannel returns true if channel type is Twilio', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return {
+ inbox: {
+ channel_type: 'Channel::TwilioSms',
+ phone_number: '+91944444444',
+ },
+ };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isATwilioChannel).toBe(true);
+ expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
+ });
+
+ it('isATwilioWhatsappChannel returns true if channel type is Twilio and phonenumber is a whatsapp number', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return {
+ inbox: {
+ channel_type: 'Channel::TwilioSms',
+ phone_number: 'whatsapp:+91944444444',
+ },
+ };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isATwilioChannel).toBe(true);
+ expect(wrapper.vm.isATwilioWhatsappChannel).toBe(true);
+ });
+
+ it('isAnEmailChannel returns true if channel type is email', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return {
+ inbox: { channel_type: 'Channel::Email' },
+ };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isAnEmailChannel).toBe(true);
+ });
+});
diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js
index 547bb7dfc..c761781c6 100644
--- a/app/javascript/shared/store/globalConfig.js
+++ b/app/javascript/shared/store/globalConfig.js
@@ -1,19 +1,25 @@
const {
+ APP_VERSION: appVersion,
+ CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
+ BRAND_NAME: brandName,
+ INSTALLATION_NAME: installationName,
LOGO_THUMBNAIL: logoThumbnail,
LOGO: logo,
- INSTALLATION_NAME: installationName,
- WIDGET_BRAND_URL: widgetBrandURL,
- TERMS_URL: termsURL,
PRIVACY_URL: privacyURL,
+ TERMS_URL: termsURL,
+ WIDGET_BRAND_URL: widgetBrandURL,
} = window.globalConfig;
const state = {
- logoThumbnail,
- logo,
+ appVersion,
+ createNewAccountFromDashboard,
+ brandName,
installationName,
- widgetBrandURL,
- termsURL,
+ logo,
+ logoThumbnail,
privacyURL,
+ termsURL,
+ widgetBrandURL,
};
export const getters = {
diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue
index bdbd1388c..45b45fb48 100755
--- a/app/javascript/widget/App.vue
+++ b/app/javascript/widget/App.vue
@@ -77,12 +77,13 @@ export default {
const message = JSON.parse(e.data.replace(wootPrefix, ''));
if (message.event === 'config-set') {
+ this.setLocale(message.locale);
+ this.setBubbleLabel();
+ this.setPosition(message.position);
this.fetchOldConversations().then(() => {
this.setUnreadView();
});
this.fetchAvailableAgents(websiteToken);
- this.setLocale(message.locale);
- this.setPosition(message.position);
this.setHideMessageBubble(message.hideMessageBubble);
} else if (message.event === 'widget-visible') {
this.scrollConversationToBottom();
@@ -100,6 +101,7 @@ export default {
this.$store.dispatch('contacts/update', message);
} else if (message.event === 'set-locale') {
this.setLocale(message.locale);
+ this.setBubbleLabel();
} else if (message.event === 'set-unread-view') {
this.showUnreadView = true;
} else if (message.event === 'unset-unread-view') {
@@ -118,6 +120,12 @@ export default {
const container = this.$el.querySelector('.conversation-wrap');
container.scrollTop = container.scrollHeight;
},
+ setBubbleLabel() {
+ IFrameHelper.sendMessage({
+ event: 'setBubbleLabel',
+ label: this.$t('BUBBLE.LABEL'),
+ });
+ },
setLocale(locale) {
const { enabledLanguages } = window.chatwootWebChannel;
if (enabledLanguages.some(lang => lang.iso_639_1_code === locale)) {
diff --git a/app/javascript/widget/assets/scss/_forms.scss b/app/javascript/widget/assets/scss/_forms.scss
index 752fa223c..6ea4666aa 100755
--- a/app/javascript/widget/assets/scss/_forms.scss
+++ b/app/javascript/widget/assets/scss/_forms.scss
@@ -69,6 +69,8 @@ $input-height: $space-two * 2;
// Form element: Textarea
textarea.form-input {
+ font-family: $font-family;
+
@include placeholder {
color: $color-light-gray;
}
diff --git a/app/javascript/widget/assets/scss/woot.scss b/app/javascript/widget/assets/scss/woot.scss
index 222275592..ba6d93e23 100755
--- a/app/javascript/widget/assets/scss/woot.scss
+++ b/app/javascript/widget/assets/scss/woot.scss
@@ -50,3 +50,7 @@ body {
}
}
}
+
+.cursor-pointer {
+ cursor: pointer;
+}
diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue
index 70aa01b1c..3c27773d0 100755
--- a/app/javascript/widget/components/AgentMessage.vue
+++ b/app/javascript/widget/components/AgentMessage.vue
@@ -114,8 +114,10 @@ export default {
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
return 'Bot';
}
-
- return this.message.sender ? this.message.sender.name : 'Bot';
+ if (this.message.sender) {
+ return this.message.sender.available_name || this.message.sender.name;
+ }
+ return 'Bot';
},
avatarUrl() {
// eslint-disable-next-line
diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue
index 2676839a6..135c9f0e1 100755
--- a/app/javascript/widget/components/AgentMessageBubble.vue
+++ b/app/javascript/widget/components/AgentMessageBubble.vue
@@ -4,7 +4,7 @@
v-if="!isCards && !isOptions && !isForm && !isArticle"
class="chat-bubble agent"
>
-
+
diff --git a/app/javascript/widget/components/Branding.vue b/app/javascript/widget/components/Branding.vue
index 4c6c787bf..2c4e4fc35 100644
--- a/app/javascript/widget/components/Branding.vue
+++ b/app/javascript/widget/components/Branding.vue
@@ -1,18 +1,17 @@
-
+
- {{ useInstallationName($t('POWERED_BY'), globalConfig.installationName) }}
+ {{ useInstallationName($t('POWERED_BY'), globalConfig.brandName) }}
+
diff --git a/app/views/widgets/show.html.erb b/app/views/widgets/show.html.erb
index 765151101..82e094118 100644
--- a/app/views/widgets/show.html.erb
+++ b/app/views/widgets/show.html.erb
@@ -17,6 +17,7 @@
welcomeTagline: '<%= @web_widget.welcome_tagline %>',
welcomeTitle: '<%= @web_widget.welcome_title %>',
widgetColor: '<%= @web_widget.widget_color %>',
+ enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
}
window.chatwootWidgetDefaults = {
diff --git a/app/workers/conversation_reply_email_worker.rb b/app/workers/conversation_reply_email_worker.rb
index e63d8fafb..da6e18fe0 100644
--- a/app/workers/conversation_reply_email_worker.rb
+++ b/app/workers/conversation_reply_email_worker.rb
@@ -6,7 +6,11 @@ class ConversationReplyEmailWorker
@conversation = Conversation.find(conversation_id)
# send the email
- ConversationReplyMailer.reply_with_summary(@conversation, queued_time).deliver_later
+ if @conversation.messages.incoming&.last&.content_type == 'incoming_email'
+ ConversationReplyMailer.reply_without_summary(@conversation, queued_time).deliver_later
+ else
+ ConversationReplyMailer.reply_with_summary(@conversation, queued_time).deliver_later
+ end
# delete the redis set from the first new message on the conversation
Redis::Alfred.delete(conversation_mail_key)
diff --git a/config/app.yml b/config/app.yml
index f6ac215d9..a33711257 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '1.5.3'
+ version: '1.7.0'
development:
<<: *shared
@@ -7,8 +7,8 @@ development:
production:
<<: *shared
-staging:
+staging:
<<: *shared
-test:
- <<: *shared
\ No newline at end of file
+test:
+ <<: *shared
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 88ebfe82c..bdba46ea3 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -30,7 +30,7 @@ Rails.application.configure do
config.public_file_server.enabled = true
# Store uploaded files on the local file system (see config/storage.yml for options).
- config.active_storage.service = :local
+ config.active_storage.service = ENV.fetch('ACTIVE_STORAGE_SERVICE', 'local').to_sym
config.active_job.queue_adapter = :sidekiq
diff --git a/config/initializers/cypress_on_rails.rb b/config/initializers/cypress_on_rails.rb
new file mode 100644
index 000000000..e43587d79
--- /dev/null
+++ b/config/initializers/cypress_on_rails.rb
@@ -0,0 +1,9 @@
+if defined?(CypressOnRails)
+ CypressOnRails.configure do |c|
+ c.cypress_folder = File.expand_path("#{__dir__}/../../spec/cypress")
+ # WARNING!! CypressOnRails can execute arbitrary ruby code
+ # please use with extra caution if enabling on hosted servers or starting your local server on 0.0.0.0
+ c.use_middleware = Rails.env.test?
+ c.logger = Rails.logger
+ end
+end
diff --git a/config/initializers/languages.rb b/config/initializers/languages.rb
index f24571ca7..5cfbd1279 100644
--- a/config/initializers/languages.rb
+++ b/config/initializers/languages.rb
@@ -20,7 +20,8 @@ LANGUAGES_CONFIG = {
15 => { name: 'Greek', iso_639_3_code: 'ell', iso_639_1_code: 'el', enabled: true },
16 => { name: 'Portuguese, Brazilian', iso_639_3_code: '', iso_639_1_code: 'pt_BR', enabled: true },
17 => { name: 'Romanian', iso_639_3_code: 'ron', iso_639_1_code: 'ro', enabled: true },
- 18 => { name: 'Tamil', iso_639_3_code: 'tam', iso_639_1_code: 'ta', enabled: true }
+ 18 => { name: 'Tamil', iso_639_3_code: 'tam', iso_639_1_code: 'ta', enabled: true },
+ 19 => { name: 'Persian', iso_639_3_code: 'fas', iso_639_1_code: 'fa', enabled: true }
}.filter { |_key, val| val[:enabled] }.freeze
Rails.configuration.i18n.available_locales = LANGUAGES_CONFIG.map { |_index, lang| lang[:iso_639_1_code].to_sym }
diff --git a/config/initializers/liquid_handler.rb b/config/initializers/liquid_handler.rb
new file mode 100644
index 000000000..7ce86da51
--- /dev/null
+++ b/config/initializers/liquid_handler.rb
@@ -0,0 +1 @@
+ActionView::Template.register_template_handler :liquid, ActionView::Template::Handlers::Liquid
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index dbb766290..9d0d1f35b 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.session_store :cookie_store, key: '_chatwoot_session'
+Rails.application.config.session_store :cookie_store, key: '_chatwoot_session', same_site: :lax
diff --git a/config/installation_config.yml b/config/installation_config.yml
index 8a49567e5..b48d3a9d3 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -14,5 +14,11 @@
value: 'https://www.chatwoot.com/privacy-policy'
- name: DISPLAY_MANIFEST
value: true
-- name: FALLBACK_DOMAIN
- value: chatwoot.com
+- name: MAILER_INBOUND_EMAIL_DOMAIN
+ value:
+- name: MAILER_SUPPORT_EMAIL
+ value:
+- name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD
+ value: false
+- name: BRAND_NAME
+ value: 'Chatwoot'
diff --git a/config/integration/apps.yml b/config/integration/apps.yml
index a63b5f606..e6b349698 100644
--- a/config/integration/apps.yml
+++ b/config/integration/apps.yml
@@ -4,10 +4,9 @@ slack:
logo: slack.png
description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack."
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:read,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history
-webhooks:
+webhooks:
id: webhook
name: Webhooks
logo: cable.svg
description: Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks.
action: /webhook
-
\ No newline at end of file
diff --git a/config/locales/devise.fa.yml b/config/locales/devise.fa.yml
new file mode 100644
index 000000000..25b70dd8f
--- /dev/null
+++ b/config/locales/devise.fa.yml
@@ -0,0 +1,62 @@
+# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
+
+fa:
+ devise:
+ confirmations:
+ confirmed: "ایمیل شما با موفقیت فعال شد."
+ send_instructions: "ظرف چند دقیقه ایمیلی حاوی روش فعال کردن حساب خود دریافت خواهید کرد."
+ send_paranoid_instructions: "در صورت موجود بودن ایمیل شما در پایگاه داده ما، ظرف چند دقیقه یک ایمیل حاوی روش فعال کردن حساب خود دریافت خواهید کرد."
+ failure:
+ already_authenticated: "شما قبلا از حساب کاربری خود خارج شدهاید."
+ inactive: "حساب کاربری شما هنوز فعال نشده است."
+ invalid: "شناسه %{authentication_keys} یا رمز عبور اشتباه است یا هنوز فعال نشده است."
+ locked: "حساب کاربری شما مسدود شده است."
+ last_attempt: "یک مرتبه دیگر میتوانید امتحان کنید و پس از آن حساب کاربری شما مسدود میشود."
+ not_found_in_database: "شناسه %{authentication_keys} یا رمز عبور اشتباه است."
+ timeout: "مدت زیادی است که با سایت کار نکردهاید، جهت تامین امنیت شما لازم است مجددا به حساب کاربری خود وارد شوید."
+ unauthenticated: "برای استفاده از این بخش لازم است به ثبت نام کرده و به حساب کاربری خود وارد شوید."
+ unconfirmed: "برای استفاده از این بخش لازم است از طریق ایمیل خود، حساب کاربریتان را فعال کنید."
+ mailer:
+ confirmation_instructions:
+ subject: "روش فعالسازی"
+ reset_password_instructions:
+ subject: "روش تغییر رمزعبور"
+ unlock_instructions:
+ subject: "روش باز کردن حساب مسدود شده"
+ password_change:
+ subject: "رمز عبور تغییر کرد"
+ omniauth_callbacks:
+ failure: "متاسفانه به علت \"%{reason}\" امکان ورود از %{kind} وجود ندارد."
+ success: "ورود از حساب %{kind} با موفقیت انجام شد."
+ passwords:
+ no_token: "این صفحه تنها در صورتی قابل مشاهده است که از طریق ایمیل باز شده باشد. چنانچه از طریق ایمیل وارد این لینک شدهاید مطمئن شوید لینک را به طور کامل کپی کردهاید."
+ send_instructions: "ظرف چند دقیقه ایمیلی حاوی روش تغییر دادن رمز عبور برای شما ارسال میشود."
+ send_paranoid_instructions: "در صورتیکه ایمیل شما در پایگاه داده ما موجود باشد، ایمیلی حاوی لینک تغییر رمز عبور دریافت خواهید کرد."
+ updated: "رمز عبور شما عوض شد. حالا به سیستم وارد شدید."
+ updated_not_active: "رمز عبورتان عوض شد."
+ registrations:
+ destroyed: "بدرود! حساب شما با موفقیت لغو شد. به امید دیدار مجدد شما"
+ signed_up: "خوش آمدید! حساب کاربری با موفقیت ساخته شد."
+ signed_up_but_inactive: "حساب کاربری شما ساخته شد. ولی برای ورود به حساب لازم است آن را فعال کنید."
+ signed_up_but_locked: "حساب شما با موفقیت ساخته شد. ولی در حال حاضر امکان ورود ندارید. حساب شما مسدود شده است."
+ signed_up_but_unconfirmed: "پیامی حاوی لینک فعالسازی حساب به ایمیل شما ارسال شد. لطفا لینک ارسال شده را کلیک کنید تا حساب شما فعال شود."
+ update_needs_confirmation: "تغییرات حساب شما ثبت شد، ولی نیاز است ایمیل جدید خود را تایید کنید. لطفا آن ایمیل را چک کنید و لینک تایید ایمیل ارسال شده به آن را کلیک کنید تا ایمیل جدیدتان تایید شود."
+ updated: "تغییرات پروفایل با موفقیت ثبت شد."
+ sessions:
+ signed_in: "با موفقیت وارد شدید."
+ signed_out: "با موفقیت خارج شدید."
+ already_signed_out: "قبلا از حساب خود خارج شدهاید."
+ unlocks:
+ send_instructions: "ظرف چند دقیقه ایمیلی حاوی نحوه رفع مسدودیت حساب کاربری خود دریافت خواهید کرد."
+ send_paranoid_instructions: "اگر حساب کاربری شما وجود داشته باشد، ایمیلی حاوی روش رفع مسدودیت آن را دریافت خواهید کرد."
+ unlocked: "مسدودیت حساب شما با موفقیت برطرف شد. لطفا به حساب خود وارد شوید."
+ errors:
+ messages:
+ already_confirmed: "حساب کاربری قبلا فعال شده، به حساب کاربری خود وارد شوید."
+ confirmation_period_expired: "فعالسازی حساب میبایست ظرف %{period}، انجام میشد. لطفا درخواست دهید تا مجددا ایمیل فعالسازی ارسال شود."
+ expired: "منقضی شده است، لطفا مجددا درخواست بدهید."
+ not_found: "یافت نشد"
+ not_locked: "مسدود نشده است."
+ not_saved:
+ one: "یک خطا مانع ثبت شدن تغییرات %{resource} شده است.:"
+ other: "%{count} خطا مانع ثبت شدن تغییرات %{resource} شده است:"
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
new file mode 100644
index 000000000..86da2829e
--- /dev/null
+++ b/config/locales/fa.yml
@@ -0,0 +1,25 @@
+fa:
+ hello: "سلام"
+ messages:
+ reset_password_success: سوت! درخواست ریست شدن رمز عبور با موفقیت ارسال شد. ایمیل خود را چک کنید
+ reset_password_failure: اوه نه! کاربری با چنین ایمیلی وجود ندارد
+
+ errors:
+ signup:
+ disposable_email: استفاده از ایمیلهای موقت امکانپذیر نیست
+ invalid_email: ایمیل وارد شده معتبر نیست
+ email_already_exists: "قبلا کاربری با ایمیل %{email} ثبت نام کرده است."
+ failed: ثبت نام ناموفق بود
+
+ conversations:
+ activity:
+ status:
+ resolved: "مکالمه توسط اپراتور %{user_name} حل شده، اعلام شده بود"
+ open: "گفتگو توسط اپراتور %{user_name} مجددا باز شده بود"
+ assignee:
+ assigned: "%{user_name} گفتگو را به %{assignee_name} اختصاص داد"
+ removed: "گفتگو توسط اپراتور %{user_name} به وضعیت اختصاص داده نشده تغییر یافت"
+ templates:
+ greeting_message_body: "%{account_name} معمولا ظرف مدت کوتاهی پاسخ میدهد."
+ ways_to_reach_you_message_body: "راهی برای ارتباط گرفتن تیم با شما قرار دهید"
+ email_input_box_message_body: "پیام جدیدی به این گفتگو اضافه شده است"
diff --git a/config/routes.rb b/config/routes.rb
index 0023e81ad..df64f88d4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -61,8 +61,12 @@ Rails.application.routes.draw do
end
resources :contacts, only: [:index, :show, :update, :create] do
+ collection do
+ get :search
+ end
scope module: :contacts do
resources :conversations, only: [:index]
+ resources :contact_inboxes, only: [:create]
end
end
diff --git a/config/storage.yml b/config/storage.yml
index a44d0381d..ba053d00e 100644
--- a/config/storage.yml
+++ b/config/storage.yml
@@ -28,6 +28,15 @@ microsoft:
storage_access_key: <%= ENV.fetch('AZURE_STORAGE_ACCESS_KEY', '') %>
container: <%= ENV.fetch('AZURE_STORAGE_CONTAINER', '') %>
+# s3 compatible service such as DigitalOcean Spaces, Minio.
+s3_compatible:
+ service: S3
+ access_key_id: <%= ENV.fetch('STORAGE_ACCESS_KEY_ID', '') %>
+ secret_access_key: <%= ENV.fetch('STORAGE_SECRET_ACCESS_KEY', '') %>
+ region: <%= ENV.fetch('STORAGE_REGION', '') %>
+ bucket: <%= ENV.fetch('STORAGE_BUCKET_NAME', '') %>
+ endpoint: <%= ENV.fetch('STORAGE_ENDPOINT', '') %>
+
# mirror:
# service: Mirror
# primary: local
diff --git a/db/migrate/20200627115105_create_api_channel.rb b/db/migrate/20200627115105_create_api_channel.rb
new file mode 100644
index 000000000..3c9e92553
--- /dev/null
+++ b/db/migrate/20200627115105_create_api_channel.rb
@@ -0,0 +1,9 @@
+class CreateApiChannel < ActiveRecord::Migration[6.0]
+ def change
+ create_table :channel_api do |t|
+ t.integer :account_id, null: false
+ t.string :webhook_url, null: false
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20200709145000_remove_multiple_feature_flags.rb b/db/migrate/20200709145000_remove_multiple_feature_flags.rb
new file mode 100644
index 000000000..a7384a529
--- /dev/null
+++ b/db/migrate/20200709145000_remove_multiple_feature_flags.rb
@@ -0,0 +1,21 @@
+class RemoveMultipleFeatureFlags < ActiveRecord::Migration[6.0]
+ def change
+ current_config = InstallationConfig.where(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS').last
+ InstallationConfig.where(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS').where.not(id: current_config.id).destroy_all
+ ConfigLoader.new.process
+ update_existing_accounts
+ end
+
+ def update_existing_accounts
+ feature_config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
+ facebook_config = feature_config.value.find { |value| value['name'] == 'channel_facebook' }
+ twitter_config = feature_config.value.find { |value| value['name'] == 'channel_twitter' }
+ Account.find_in_batches do |account_batch|
+ account_batch.each do |account|
+ account.enable_features('channel_facebook') if facebook_config['enabled']
+ account.enable_features('channel_twitter') if twitter_config['enabled']
+ account.save!
+ end
+ end
+ end
+end
diff --git a/db/migrate/20200715124113_create_email_channel.rb b/db/migrate/20200715124113_create_email_channel.rb
new file mode 100644
index 000000000..ca066aacb
--- /dev/null
+++ b/db/migrate/20200715124113_create_email_channel.rb
@@ -0,0 +1,10 @@
+class CreateEmailChannel < ActiveRecord::Migration[6.0]
+ def change
+ create_table :channel_email do |t|
+ t.integer :account_id, null: false
+ t.string :email, null: false, index: { unique: true }
+ t.string :forward_to_address, null: false, index: { unique: true }
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20200719171437_rename_nick_name_to_display_name.rb b/db/migrate/20200719171437_rename_nick_name_to_display_name.rb
new file mode 100644
index 000000000..35a53a3c1
--- /dev/null
+++ b/db/migrate/20200719171437_rename_nick_name_to_display_name.rb
@@ -0,0 +1,5 @@
+class RenameNickNameToDisplayName < ActiveRecord::Migration[6.0]
+ def change
+ rename_column :users, :nickname, :display_name
+ end
+end
diff --git a/db/migrate/20200725131651_create_email_templates.rb b/db/migrate/20200725131651_create_email_templates.rb
new file mode 100644
index 000000000..00cef28eb
--- /dev/null
+++ b/db/migrate/20200725131651_create_email_templates.rb
@@ -0,0 +1,13 @@
+class CreateEmailTemplates < ActiveRecord::Migration[6.0]
+ def change
+ create_table :email_templates do |t|
+ t.string :name, null: false
+ t.text :body, null: false
+ t.integer :account_id, null: true
+ t.integer :template_type, default: 1
+ t.integer :locale, default: 0, null: false
+ t.timestamps
+ end
+ add_index :email_templates, [:name, :account_id], unique: true
+ end
+end
diff --git a/db/migrate/20200730080242_add_feature_setting_to_website_inbox.rb b/db/migrate/20200730080242_add_feature_setting_to_website_inbox.rb
new file mode 100644
index 000000000..7ffbe401f
--- /dev/null
+++ b/db/migrate/20200730080242_add_feature_setting_to_website_inbox.rb
@@ -0,0 +1,5 @@
+class AddFeatureSettingToWebsiteInbox < ActiveRecord::Migration[6.0]
+ def change
+ add_column :channel_web_widgets, :feature_flags, :integer, default: 3, null: false
+ end
+end
diff --git a/db/migrate/20200802170002_reset_agent_last_seen_at.rb b/db/migrate/20200802170002_reset_agent_last_seen_at.rb
new file mode 100644
index 000000000..b6d4198a7
--- /dev/null
+++ b/db/migrate/20200802170002_reset_agent_last_seen_at.rb
@@ -0,0 +1,7 @@
+class ResetAgentLastSeenAt < ActiveRecord::Migration[6.0]
+ def change
+ # rubocop:disable Rails/SkipsModelValidations
+ ::Conversation.where('agent_last_seen_at > ?', DateTime.now.utc).update_all(agent_last_seen_at: DateTime.now.utc)
+ # rubocop:enable Rails/SkipsModelValidations
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b6337dbcd..0bd83d85a 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: 2020_07_04_173104) do
+ActiveRecord::Schema.define(version: 2020_08_02_170002) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -120,6 +120,23 @@ ActiveRecord::Schema.define(version: 2020_07_04_173104) do
t.datetime "updated_at", null: false
end
+ create_table "channel_api", force: :cascade do |t|
+ t.integer "account_id", null: false
+ t.string "webhook_url", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ create_table "channel_email", force: :cascade do |t|
+ t.integer "account_id", null: false
+ t.string "email", null: false
+ t.string "forward_to_address", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["email"], name: "index_channel_email_on_email", unique: true
+ t.index ["forward_to_address"], name: "index_channel_email_on_forward_to_address", unique: true
+ end
+
create_table "channel_facebook_pages", id: :serial, force: :cascade do |t|
t.string "page_id", null: false
t.string "user_access_token", null: false
@@ -161,6 +178,7 @@ ActiveRecord::Schema.define(version: 2020_07_04_173104) do
t.string "widget_color", default: "#1f93ff"
t.string "welcome_title"
t.string "welcome_tagline"
+ t.integer "feature_flags", default: 3, null: false
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
end
@@ -213,6 +231,17 @@ ActiveRecord::Schema.define(version: 2020_07_04_173104) do
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
end
+ create_table "email_templates", force: :cascade do |t|
+ t.string "name", null: false
+ t.text "body", null: false
+ t.integer "account_id"
+ t.integer "template_type", default: 1
+ t.integer "locale", default: 0, null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true
+ end
+
create_table "events", force: :cascade do |t|
t.string "name"
t.float "value"
@@ -407,7 +436,7 @@ ActiveRecord::Schema.define(version: 2020_07_04_173104) do
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.string "name", null: false
- t.string "nickname"
+ t.string "display_name"
t.string "email"
t.json "tokens"
t.datetime "created_at", null: false
diff --git a/docker-compose.production.yaml b/docker-compose.production.yaml
index ababfe265..129c93ff4 100644
--- a/docker-compose.production.yaml
+++ b/docker-compose.production.yaml
@@ -2,20 +2,11 @@ version: '3'
services:
base: &base
- build:
- context: .
- dockerfile: ./docker/Dockerfile
- args:
- BUNDLE_WITHOUT: 'development:test'
- EXECJS_RUNTIME: Disabled
- RAILS_ENV: 'production'
- RAILS_SERVE_STATIC_FILES: 'true'
- image: chatwoot:latest
+ image: chatwoot/chatwoot:latest
env_file: .env ## Change this file for customized env variables
rails:
<<: *base
- image: chatwoot:latest
depends_on:
- postgres
- redis
@@ -29,7 +20,6 @@ services:
sidekiq:
<<: *base
- image: chatwoot:latest
depends_on:
- postgres
- redis
@@ -44,23 +34,17 @@ services:
ports:
- '5432:5432'
volumes:
- - postgres:/data/postgres
+ - /data/postgres:/var/lib/postgresql/data
environment:
- POSTGRES_DB=chatwoot
- POSTGRES_USER=postgres
+ # Please provide your own password.
- POSTGRES_PASSWORD=
redis:
image: redis:alpine
restart: always
volumes:
- - redis:/data/redis
+ - /data/redis:/data
ports:
- '6379:6379'
-
-volumes:
- postgres:
- redis:
- bundle:
- packs:
- node_modules_rails:
diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml
new file mode 100644
index 000000000..ababfe265
--- /dev/null
+++ b/docker-compose.test.yaml
@@ -0,0 +1,66 @@
+version: '3'
+
+services:
+ base: &base
+ build:
+ context: .
+ dockerfile: ./docker/Dockerfile
+ args:
+ BUNDLE_WITHOUT: 'development:test'
+ EXECJS_RUNTIME: Disabled
+ RAILS_ENV: 'production'
+ RAILS_SERVE_STATIC_FILES: 'true'
+ image: chatwoot:latest
+ env_file: .env ## Change this file for customized env variables
+
+ rails:
+ <<: *base
+ image: chatwoot:latest
+ depends_on:
+ - postgres
+ - redis
+ ports:
+ - 3000:3000
+ environment:
+ - NODE_ENV=production
+ - RAILS_ENV=production
+ entrypoint: docker/entrypoints/rails.sh
+ command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0']
+
+ sidekiq:
+ <<: *base
+ image: chatwoot:latest
+ depends_on:
+ - postgres
+ - redis
+ environment:
+ - NODE_ENV=production
+ - RAILS_ENV=production
+ command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
+
+ postgres:
+ image: postgres:12
+ restart: always
+ ports:
+ - '5432:5432'
+ volumes:
+ - postgres:/data/postgres
+ environment:
+ - POSTGRES_DB=chatwoot
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=
+
+ redis:
+ image: redis:alpine
+ restart: always
+ volumes:
+ - redis:/data/redis
+ ports:
+ - '6379:6379'
+
+volumes:
+ postgres:
+ redis:
+ bundle:
+ packs:
+ node_modules_rails:
diff --git a/docs/README.md b/docs/README.md
index 879451bfa..7f2b5d20c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,12 +5,13 @@ path: '/docs'
This guide will help you get started with Chatwoot!
-### Environment Setup
+### Environment Setup for local development
* [Mac](/docs/installation-guide-mac-os)
* [Ubuntu](/docs/installation-guide-ubuntu)
* [Windows](/docs/installation-guide-windows)
* [Docker](/docs/installation-guide-docker)
+*
### Project Setup
@@ -19,7 +20,7 @@ This guide will help you get started with Chatwoot!
* [Conversation Continuity with Email](/docs/conversation-continuity)
* [Common Errors](/docs/common-errors)
-### Deployment
+### Deploying Chatwoot in Production
* [Architecture](/docs/deployment/architecture)
* [Heroku](/docs/deployment/deploy-chatwoot-with-heroku) (recommended)
diff --git a/docs/channels/facebook-channel.md b/docs/channels/facebook-channel.md
index 08f11d0aa..87c68b44f 100644
--- a/docs/channels/facebook-channel.md
+++ b/docs/channels/facebook-channel.md
@@ -1,9 +1,9 @@
---
-path: "/docs/channels/facebook"
-title: "How to create Facebook channel?"
+path: '/docs/channels/facebook'
+title: 'How to create Facebook channel?'
---
-If you are using self-hosted Chatwoot installation, please configure the Facebook app as described in the [guide to setup Facebook app](/docs/facebook-setup)
+If you are using a self-hosted Chatwoot installation, please configure the Facebook app as described in the [guide to setup Facebook app](/docs/facebook-setup)
**Step 1**. Click on "Add Inbox" button from Settings > Inboxes page.
@@ -17,7 +17,7 @@ If you are using self-hosted Chatwoot installation, please configure the Faceboo

-**Step 4**. Authenticate with Facebook and select the page you want connect, enable all permissions shown in the list, otherwise the app might not work.
+**Step 4**. Authenticate with Facebook and select the page you want to connect. Enable all permissions shown in the list, otherwise the app might not work.

diff --git a/docs/channels/images/sdk/expanded-bubble.gif b/docs/channels/images/sdk/expanded-bubble.gif
new file mode 100644
index 000000000..4db6fdf3c
Binary files /dev/null and b/docs/channels/images/sdk/expanded-bubble.gif differ
diff --git a/docs/channels/images/sdk/standard-bubble.gif b/docs/channels/images/sdk/standard-bubble.gif
new file mode 100644
index 000000000..e0541dff7
Binary files /dev/null and b/docs/channels/images/sdk/standard-bubble.gif differ
diff --git a/docs/channels/supported-features.md b/docs/channels/supported-features.md
new file mode 100644
index 000000000..d8f03d7a1
--- /dev/null
+++ b/docs/channels/supported-features.md
@@ -0,0 +1,65 @@
+---
+path: "/docs/channels/feature-comparison"
+title: "Supported features on channels"
+---
+
+
+### Supported message type
+
+
+
+| Channel | Incoming/Outgoing message | Activity message | Template message |
+| -- | -- | -- | -- |
+| Website | Yes | Yes | Yes |
+| Facebook | Yes | Yes | No |
+| Twitter DM | Yes | Yes | No |
+| Tweets | Yes | Yes | No |
+| SMS | Yes | Yes | No |
+| Whatsapp | Yes | Yes | No |
+
+
+
+### Maximum message size (number of characters)
+
+
+
+| Channel | Maximum message size |
+| -- | -- |
+| Website | 10,000 |
+| Facebook | 640 |
+| Twitter DM | 10,000 |
+| Tweets | 280 |
+| SMS | 160 |
+| Whatsapp | 10,000 |
+
+
+
+### Outgoing message restriction
+
+
+
+| Channel | Restriction |
+| -- | -- |
+| Website | No restriction |
+| Facebook | Cannot send promotional messages 24 hours after the last incoming message |
+| Twitter DM | No restriction |
+| Tweets | No restriction |
+| SMS | No restriction |
+| Whatsapp | Cannot send any message other than Whatsapp approved template messages 24 hours after the last incoming message |
+
+
+
+### Available features
+
+
+
+| Channel | Channel greeting | Attachments | Agent Auto assignment | Slack |
+| -- | -- | -- | -- | -- |
+| Website | Yes | Yes | Yes | Yes |
+| Facebook | Yes | Yes | Yes | No |
+| Twitter DM | Yes | No | Yes | No |
+| Tweets | Yes | No | Yes | No |
+| SMS | Yes | No | Yes | No |
+| Whatsapp | Yes | Yes | Yes | No |
+
+
diff --git a/docs/channels/twitter-channel.md b/docs/channels/twitter-channel.md
index db65060fe..d81ac5096 100644
--- a/docs/channels/twitter-channel.md
+++ b/docs/channels/twitter-channel.md
@@ -1,6 +1,6 @@
---
-path: "/docs/channels/twitter"
-title: "How to create Twitter channel?"
+path: '/docs/channels/twitter'
+title: 'How to create Twitter channel?'
---
**Step 1**. Click on "Add Inbox" button from Settings > Inboxes page.
@@ -11,11 +11,11 @@ title: "How to create Twitter channel?"

-**Step 3**. Click on "Sign in with Twitter" button
+**Step 3**. Click on "Sign in with Twitter" button.

-**Step 4**. You will be redirected to Twitter, Click on "Authorize app" button.
+**Step 4**. You will be redirected to Twitter. Click on "Authorize app" button.

@@ -23,10 +23,10 @@ title: "How to create Twitter channel?"

-**Step 6**. Hooray! You have successfully created a Twitter inbox. You will be able to manage Twitter DMs and tweets mentioning you in the Chatwoot Inbox.
+**Step 6**. Hooray! You have successfully created a Twitter inbox. You will be able to manage Twitter DMs and tweets mentioning you in the Chatwoot Inbox.

- **Step 7**. If you want to update the agents who have access to the inbox, you can go to Settings > Inboxes.
+**Step 7**. If you want to update the agents who have access to the inbox, you can go to Settings > Inboxes.

diff --git a/docs/channels/website-channel.md b/docs/channels/website-channel.md
index 52cbb3736..09e5ce1f8 100644
--- a/docs/channels/website-channel.md
+++ b/docs/channels/website-channel.md
@@ -1,6 +1,6 @@
---
-path: "/docs/channels/website"
-title: "How to create website channel?"
+path: '/docs/channels/website'
+title: 'How to create website channel?'
---
**Step 1**. Click on "Add Inbox" button from Settings > Inboxes page.
@@ -23,6 +23,6 @@ title: "How to create website channel?"

-**Step 6**. If you want to update the agents who have access to the inbox, you can go to Settings > Inboxes. You will be able to see your website inbox listed there. Click on Settings. You will be able to Code as well the list of agents who have access to the inbox.
+**Step 6**. If you want to update the agents who have access to the inbox, you can go to Settings > Inboxes. You will be able to see your website inbox listed there. Click on Settings. You will be able to see the code as well as the list of agents who have access to the inbox.

diff --git a/docs/channels/website-sdk.md b/docs/channels/website-sdk.md
index be59f8f69..b6d0497d2 100644
--- a/docs/channels/website-sdk.md
+++ b/docs/channels/website-sdk.md
@@ -1,27 +1,46 @@
---
-path: "/docs/website-sdk"
-title: "Sending Information into Chatwoot"
+path: '/docs/website-sdk'
+title: 'Sending Information into Chatwoot'
---
+Additional information about a contact is always useful. The Chatwoot Website SDK ensures that you can send additional information that you have about the user.
-Additional information about a contact is always useful, Chatwoot website SDK ensures that you can send the additional info you have about the user.
+If you have installed our code on your website, the SDK would expose `window.$chatwoot` object.
-If you have installed our code on your website, SDK would expose `window.$chatwoot` object.
+In order to make sure that the SDK has been loaded completely, please make sure that you listen to `chatwoot:ready` event as follows:
-To hide the bubble, you can use the following setting. Please not if you use this, then you have to trigger the widget by yourself.
+```js
+window.addEventListener('chatwoot:ready', function () {
+ // Use window.$chatwoot here
+ // ...
+});
+```
+
+To hide the bubble, you can use the setting mentioned below. **Note**: If you use this, then you have to trigger the widget by yourself.
```js
window.chatwootSettings = {
hideMessageBubble: false,
position: 'left', // This can be left or right
locale: 'en', // Language to be set
-}
+ type: 'standard', // [standard, expanded_bubble]
+};
```
+Chatwoot support 2 designs for for the widget
+
+1. Standard (default)
+
+
+
+2. Expanded bubble
+
+
+
### To trigger widget without displaying bubble
```js
-window.$chatwoot.toggle()
+window.$chatwoot.toggle();
```
### To set the user in the widget
@@ -31,33 +50,33 @@ window.$chatwoot.setUser('identifier_key', {
email: 'email@example.com',
name: 'name',
avatar_url: '',
-})
+});
```
-`setUser` accepts an identifier which can be a `user_id` in your database or any unique parameter which represents a user. You can pass email, name, avatar_url as params, support for additional parameters are in progress.
+`setUser` accepts an identifier which can be a `user_id` in your database or any unique parameter which represents a user. You can pass email, name, avatar_url as params. Support for additional parameters is in progress.
-Make sure that you reset the session when the user logouts of your app.
+Make sure that you reset the session when the user logs out of your app.
### To set language manually
```js
-window.$chatwoot.setLocale('en')
+window.$chatwoot.setLocale('en');
```
-To set the language manually use the setLocale function.
+To set the language manually, use the `setLocale` function.
### To set labels on the conversation
-Please note that the labels will be set on a conversation, if the user has not started a conversation, then the following items will not have any effect.
+Please note that the labels will be set on a conversation if the user has not started a conversation. In that case, the following items will not have any effect:
```js
-window.$chatwoot.addLabel('support-ticket')
+window.$chatwoot.addLabel('support-ticket');
-window.$chatwoot.removeLabel('support-ticket')
+window.$chatwoot.removeLabel('support-ticket');
```
-### To refresh the session (use this while you logout user from your app)
+### To refresh the session (use this while you logout the user from your app)
```js
-window.$chatwoot.reset()
+window.$chatwoot.reset();
```
diff --git a/docs/channels/whatsapp-sms-twilio.md b/docs/channels/whatsapp-sms-twilio.md
index 96a036a72..51dd1d3d0 100644
--- a/docs/channels/whatsapp-sms-twilio.md
+++ b/docs/channels/whatsapp-sms-twilio.md
@@ -1,6 +1,6 @@
---
-path: "/docs/channels/whatsapp-sms-twilio"
-title: "How to create a Whatsapp/SMS channel with Twilio?"
+path: '/docs/channels/whatsapp-sms-twilio'
+title: 'How to create a Whatsapp/SMS channel with Twilio?'
---
**Step 1**. Click on "Add Inbox" button from Settings > Inboxes page.
@@ -13,17 +13,17 @@ title: "How to create a Whatsapp/SMS channel with Twilio?"
**Step 3**. Configure the inbox.
-These are input required to create this channel
+These are the inputs required to create this channel:
-| Input | Description | Where can I find it |
-| -- | -- | -- |
-| Channel Name | This is the name inbox, this will used across the application | N/A |
-| Channel Type | Select SMS if you are integrating SMS channel, select Whatsapp if you have a verified Whatsapp number in Twilio | N/A |
-| Phone Number | This is number you will be using to communicate with you customer, this has to be verified in Twilio | Enter your number as in Twilio Dashboard |
-| Account SID | Account SID in Twilio Console | Login to Twilio Console, you would be able to see Account SID and Auth Token |
-| Auth Token | Auth token for the account | Login to Twilio Console, you would be able to see Account SID and Auth Token |
+| Input | Description | Where can I find it |
+| ------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
+| Channel Name | This is the name inbox, this will be used across the application. | N/A |
+| Channel Type | Select SMS, if you are integrating an SMS channel. Select Whatsapp, if you have a verified Whatsapp number in Twilio. | N/A |
+| Phone Number | This is the number you will be using to communicate with your customer. This has to be verified in Twilio. | Enter your number as in the Twilio Dashboard |
+| Account SID | Account SID in Twilio Console | Login to Twilio Console. Here, you would be able to see the Account SID and the Auth Token |
+| Auth Token | Auth token for the account | Login to the Twilio Console. Here, you would be able to see the Account SID and the Auth Token |
@@ -37,19 +37,18 @@ These are input required to create this channel

-If it is an SMS Channel then you don't need to do anything else, you will start receiving the messages in the dashboard whenever a customer sends you one.
+If it is an SMS Channel, then you don't need to do anything else. You will start receiving the messages in the dashboard whenever a customer sends you one.
-If you are connecting a **Whatsapp** channel, you have configure a callback URL in the Twilio inbox.
+If you are connecting a **Whatsapp** channel, you have to configure a callback URL in the Twilio inbox:
- Login to your Twilio Console.
-- Go to `Programmable SMS > Whatsapp > Senders`
-- You will be able to see your phone number. Click on it, it will display a field like the one below.
+- Go to `Programmable SMS > Whatsapp > Senders`.
+- You will be able to see your phone number. Click on it, it will display a field like the one shown below.

- Provide `https://app.chatwoot.com/twilio/callback` as the value for `WHEN A MESSAGE COMES IN` input.
-
**Step 7**. If you want to update the agents who have access to the inbox, you can go to Settings > Inboxes.

diff --git a/docs/deployment/production/linux-vm.md b/docs/deployment/production/linux-vm.md
index 79e940074..3ae5da258 100644
--- a/docs/deployment/production/linux-vm.md
+++ b/docs/deployment/production/linux-vm.md
@@ -32,6 +32,7 @@ server {
server_name yourdomain.com;
# where rails app is running
set $upstream 127.0.0.1:3000;
+ underscores_in_headers on;
# Here we define the web-root for our SSL proof
location /.well-known {
diff --git a/docs/development/environment-setup/ubuntu.md b/docs/development/environment-setup/ubuntu.md
index 3975f98e7..7e6570f47 100644
--- a/docs/development/environment-setup/ubuntu.md
+++ b/docs/development/environment-setup/ubuntu.md
@@ -3,7 +3,7 @@ path: "/docs/installation-guide-ubuntu"
title: "Ubuntu installation guide"
---
-Open a terminal and run the following commands
+Open a terminal and run the following commands:
```bash
sudo apt-get update
@@ -29,17 +29,17 @@ sudo apt-get update
sudo apt-get install rvm
```
-Enable `Run command as a login shell` in terminal `Preferences`. Restart your computer
+Enable `Run command as a login shell` in terminal `Preferences`. Restart your computer.
### Install Ruby
-Chatwoot APIs are built on Ruby on Rails, you need install ruby 2.7.1
+Chatwoot APIs are built on Ruby on Rails. You need to install ruby 2.7.1:
```bash
rvm install ruby-2.7.1
```
-Use ruby 2.7.1 as default
+Use ruby 2.7.1 as default:
```bash
rvm use 2.7.1 --default
@@ -47,7 +47,7 @@ rvm use 2.7.1 --default
### Install Node.js
-Install Node.js from NodeSoure using the following commands
+Install Node.js from NodeSource using the following commands:
```bash
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
@@ -56,7 +56,7 @@ sudo apt-get install -y nodejs
### Install yarn
-We use `yarn` as package manager
+We use `yarn` as the package manager:
```bash
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
@@ -69,19 +69,19 @@ sudo apt-get update && sudo apt-get install yarn
### Install postgres
-The database used in Chatwoot is PostgreSQL. Use the following commands to install postgres.
+The database used in Chatwoot is PostgreSQL. Use the following commands to install postgres:
```bash
sudo apt install postgresql postgresql-contrib
```
-The installation procedure created a user account called postgres that is associated with the default Postgres role. In order to use Postgres, you can log into that account.
+The installation procedure creates a user account called postgres that is associated with the default Postgres role. In order to use Postgres, you can log into that account:
```bash
sudo -u postgres psql
```
-Install `libpg-dev` dependencies for ubuntu
+Install `libpg-dev` dependencies for ubuntu:
```bash
sudo apt-get install libpq-dev
@@ -89,13 +89,13 @@ sudo apt-get install libpq-dev
### Install redis-server
-Chatwoot uses Redis server in agent assignments and reporting. To install `redis-server`
+Chatwoot uses Redis server in agent assignments and reporting. You need to install `redis-server`:
```bash
sudo apt-get install redis-server
```
-Enable Redis to start on system boot.
+Next, enable Redis to start on system boot:
```bash
sudo systemctl enable redis-server.service
diff --git a/docs/development/environment-setup/windows.md b/docs/development/environment-setup/windows.md
index c1f379b5a..25e7c0121 100644
--- a/docs/development/environment-setup/windows.md
+++ b/docs/development/environment-setup/windows.md
@@ -5,17 +5,17 @@ title: 'Windows 10 Installation Guide'
### Requirements
-You need to install Linux Subsystem for Windows.
+You need to install the Linux Subsystem for Windows.
-1. The first step is to enable "Developer mode" in Windows. You can do this by opening up Settings and navigating to Update & Security, then "For Developers". Click the "Developer mode" option to enable it.
+1. The first step is to enable "Developer mode" in Windows. You can do this by opening up Settings and navigating to "Update & Security". In there, choose the tab on the left that reads "For Developers". Turn the "Developer mode" toggle on to enable it.
-2. Enable Windows Subsystem for Linux by opening Control Panel, going to Programs, and then clicking "Turn Windows Features On or Off". Looking for the "Windows Subsystem for Linux" option and check the box next to it.
+2. Next you have to enable the Windows Subsystem for Linux. Open the "Control Panel" and go to "Programs and Features". Click on the link on the left "Turn Windows features on or off". Look for the "Windows Subsystem for Linux" option and select the checkbox next to it.
-3. Once that's complete, you can open up the Start menu again and search for "Bash". This time it will have the Ubuntu logo.
+3. Once that's complete, you can open up the Start Menu again and search for "Bash". This time it will have the Ubuntu logo.
### Installing RVM & Ruby
@@ -40,7 +40,7 @@ ruby -v
### Install Node.js
-Install Node.js from NodeSoure using the following commands
+Install `Node.js` from NodeSource using the following commands
```bash
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
@@ -49,7 +49,7 @@ sudo apt-get install -y nodejs
### Install yarn
-We use `yarn` as package manager
+We use `yarn` as the package manager
```bash
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
@@ -62,7 +62,7 @@ sudo apt-get update && sudo apt-get install yarn
### Install postgres
-The database used in Chatwoot is PostgreSQL. Use the following commands to install postgres.
+The database used in Chatwoot is PostgreSQL. Use the following commands to install postgres:
```bash
sudo apt install postgresql postgresql-contrib
@@ -74,7 +74,7 @@ The installation procedure created a user account called postgres that is associ
sudo -u postgres psql
```
-Install `libpg-dev` dependencies for ubuntu
+Install `libpg-dev` dependencies for Ubuntu
```bash
sudo apt-get install libpq-dev
diff --git a/docs/development/project-setup/configuring-cloud-storage.md b/docs/development/project-setup/configuring-cloud-storage.md
index ccdcf7da4..d0390eb4b 100644
--- a/docs/development/project-setup/configuring-cloud-storage.md
+++ b/docs/development/project-setup/configuring-cloud-storage.md
@@ -56,3 +56,19 @@ AZURE_STORAGE_ACCOUNT_NAME=
AZURE_STORAGE_ACCESS_KEY=
AZURE_STORAGE_CONTAINER=
```
+
+
+### Using Amazon S3 Compatible Service
+
+Use s3 compatible service such as [DigitalOcean Spaces](https://www.digitalocean.com/docs/spaces/resources/s3-sdk-examples/#configure-a-client), Minio.
+
+Configure the following env variables.
+
+```bash
+ACTIVE_STORAGE_SERVICE='s3_compatible'
+STORAGE_BUCKET_NAME=
+STORAGE_ACCESS_KEY_ID=
+STORAGE_SECRET_ACCESS_KEY=
+STORAGE_REGION=nyc3
+STORAGE_ENDPOINT=https://nyc3.digitaloceanspaces.com
+```
diff --git a/docs/development/project-setup/quick-setup.md b/docs/development/project-setup/quick-setup.md
index 1f8fd946b..2c802bead 100644
--- a/docs/development/project-setup/quick-setup.md
+++ b/docs/development/project-setup/quick-setup.md
@@ -81,7 +81,21 @@ docker-compose run -rm server bundle exec rake db:reset
This command essentially runs postgres and redis containers and then run the rake command inside the chatwoot server container.
-### Docker for production
+### Running Cypress Tests
+
+Refer the docs to learn how to write cypress specs
+https://github.com/shakacode/cypress-on-rails
+https://docs.cypress.io/guides/overview/why-cypress.html
+
+```
+# in terminal tab1
+overmind start -f Procfile.test
+# in terminal tab2
+yarn cypress open --project ./test
+```
+
+
+### Debugging Docker for production
You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot)
diff --git a/lib/action_view/template/handlers/liquid.rb b/lib/action_view/template/handlers/liquid.rb
new file mode 100644
index 000000000..65c0b1b3e
--- /dev/null
+++ b/lib/action_view/template/handlers/liquid.rb
@@ -0,0 +1,62 @@
+# Code inspired by
+# http://royvandermeij.com/blog/2011/09/21/create-a-liquid-handler-for-rails-3-dot-1/
+# https://github.com/chamnap/liquid-rails/blob/master/lib/liquid-rails/template_handler.rb
+
+class ActionView::Template::Handlers::Liquid
+ def self.call(template, _source)
+ "ActionView::Template::Handlers::Liquid.new(self).render(#{template.source.inspect}, local_assigns)"
+ end
+
+ def initialize(view)
+ @view = view
+ @controller = @view.controller
+ @helper = ActionController::Base.helpers
+ end
+
+ def render(template, local_assigns = {})
+ assigns = drops
+ assigns['content_for_layout'] = @view.content_for(:layout) if @view.content_for?(:layout)
+ assigns.merge!(local_assigns)
+ assigns.merge!(locals)
+
+ liquid = Liquid::Template.parse(template)
+ liquid.send(render_method, assigns.stringify_keys, filters: filters, registers: registers.stringify_keys)
+ end
+
+ def locals
+ if @controller.respond_to?(:liquid_locals, true)
+ @controller.send(:liquid_locals)
+ else
+ {}
+ end
+ end
+
+ def drops
+ droppables = @controller.send(:liquid_droppables) if @controller.respond_to?(:liquid_droppables, true)
+ droppables.update(droppables) { |_, obj| obj.try(:to_drop) || nil }
+ end
+
+ def filters
+ if @controller.respond_to?(:liquid_filters, true)
+ @controller.send(:liquid_filters)
+ else
+ []
+ end
+ end
+
+ def registers
+ if @controller.respond_to?(:liquid_registers, true)
+ @controller.send(:liquid_registers)
+ else
+ {}
+ end
+ end
+
+ def compilable?
+ false
+ end
+
+ def render_method
+ ::Rails.env.development? || ::Rails.env.test? ? :render! : :render
+ end
+end
diff --git a/lib/config_loader.rb b/lib/config_loader.rb
index a313bb190..e880d3dd1 100644
--- a/lib/config_loader.rb
+++ b/lib/config_loader.rb
@@ -74,11 +74,11 @@ class ConfigLoader
def compare_and_save_feature(config)
features = if @reconcile_only_new
# leave the existing feature flag values as it is and add new feature flags with default values
- (account_features + config.value).uniq { |h| h['name'] }
+ (config.value + account_features).uniq { |h| h['name'] }
else
# update the existing feature flag values with default values and add new feature flags with default values
- (config.value + account_features).uniq { |h| h['name'] }
+ (account_features + config.value).uniq { |h| h['name'] }
end
- save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features })
+ config.update({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features })
end
end
diff --git a/lib/integrations/facebook/message_creator.rb b/lib/integrations/facebook/message_creator.rb
index abf6c93e2..2669b840a 100644
--- a/lib/integrations/facebook/message_creator.rb
+++ b/lib/integrations/facebook/message_creator.rb
@@ -9,10 +9,10 @@ class Integrations::Facebook::MessageCreator
def perform
# begin
- if outgoing_message_via_echo?
- create_outgoing_message
+ if agent_message_via_echo?
+ create_agent_message
else
- create_incoming_message
+ create_contact_message
end
# rescue => e
# Raven.capture_exception(e)
@@ -21,22 +21,22 @@ class Integrations::Facebook::MessageCreator
private
- def outgoing_message_via_echo?
+ def agent_message_via_echo?
response.echo? && !response.sent_from_chatwoot_app?
- # this means that it is an outgoing message from page, but not sent from chatwoot.
- # User can send from fb page directly on mobile messenger, so this case should be handled as outgoing message
+ # this means that it is an agent message from page, but not sent from chatwoot.
+ # User can send from fb page directly on mobile / web messenger, so this case should be handled as agent message
end
- def create_outgoing_message
+ def create_agent_message
Channel::FacebookPage.where(page_id: response.sender_id).each do |page|
- mb = Messages::Outgoing::EchoBuilder.new(response, page.inbox, true)
+ mb = Messages::Facebook::MessageBuilder.new(response, page.inbox, true)
mb.perform
end
end
- def create_incoming_message
+ def create_contact_message
Channel::FacebookPage.where(page_id: response.recipient_id).each do |page|
- mb = Messages::IncomingMessageBuilder.new(response, page.inbox)
+ mb = Messages::Facebook::MessageBuilder.new(response, page.inbox)
mb.perform
end
end
diff --git a/lib/tasks/db_enhancements.rake b/lib/tasks/db_enhancements.rake
new file mode 100644
index 000000000..de2c24838
--- /dev/null
+++ b/lib/tasks/db_enhancements.rake
@@ -0,0 +1,5 @@
+# We are hooking config loader to run automatically everytime migration is executed
+Rake::Task['db:migrate'].enhance do
+ puts 'Loading Installation config'
+ ConfigLoader.new.process
+end
diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb
index 57020e299..efa555845 100644
--- a/lib/webhooks/trigger.rb
+++ b/lib/webhooks/trigger.rb
@@ -1,6 +1,6 @@
class Webhooks::Trigger
def self.execute(url, payload)
- RestClient.post(url, payload)
+ RestClient.post(url, payload.to_json, { content_type: :json, accept: :json })
rescue StandardError => e
Raven.capture_exception(e)
end
diff --git a/package.json b/package.json
index 2a1c53856..ad69ca804 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,6 @@
"url-loader": "^2.0.0",
"v-tooltip": "~2.0.2",
"vue": "^2.6.0",
- "vue-aplayer": "~0.1.1",
"vue-audio": "~0.0.7",
"vue-axios": "~1.2.2",
"vue-chartjs": "^3.4.2",
@@ -63,6 +62,7 @@
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^25.3.0",
"babel-loader": "^8.1.0",
+ "cypress": "^4.10.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^4.0.0",
diff --git a/public/assets/dashboard/integrations/cable.svg b/public/dashboard/images/integrations/cable.svg
similarity index 100%
rename from public/assets/dashboard/integrations/cable.svg
rename to public/dashboard/images/integrations/cable.svg
diff --git a/public/assets/dashboard/integrations/slack.png b/public/dashboard/images/integrations/slack.png
similarity index 100%
rename from public/assets/dashboard/integrations/slack.png
rename to public/dashboard/images/integrations/slack.png
diff --git a/semantic.yml b/semantic.yml
new file mode 100644
index 000000000..54040a62c
--- /dev/null
+++ b/semantic.yml
@@ -0,0 +1,12 @@
+titleOnly: true
+types:
+ - Feature
+ - Fix
+ - Docs
+ - Style
+ - Refactor
+ - Perf
+ - Test
+ - Build
+ - Chore
+ - Revert
diff --git a/spec/builders/messages/incoming_message_builder_spec.rb b/spec/builders/messages/facebook/message_builder_spec.rb
similarity index 95%
rename from spec/builders/messages/incoming_message_builder_spec.rb
rename to spec/builders/messages/facebook/message_builder_spec.rb
index 0e91d3ae6..52edd0413 100644
--- a/spec/builders/messages/incoming_message_builder_spec.rb
+++ b/spec/builders/messages/facebook/message_builder_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe ::Messages::IncomingMessageBuilder do
+describe ::Messages::Facebook::MessageBuilder do
subject(:message_builder) { described_class.new(incoming_fb_text_message, facebook_channel.inbox).perform }
let!(:facebook_channel) { create(:channel_facebook_page) }
diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/message_builder_spec.rb
new file mode 100644
index 000000000..6227f2a48
--- /dev/null
+++ b/spec/builders/messages/message_builder_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+describe ::Messages::MessageBuilder do
+ subject(:message_builder) { described_class.new(user, conversation, params).perform }
+
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account: account) }
+ let(:inbox) { create(:inbox, account: account) }
+ let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) }
+ let(:conversation) { create(:conversation, inbox: inbox, account: account) }
+ let(:params) do
+ ActionController::Parameters.new({
+ content: 'test'
+ })
+ end
+
+ describe '#perform' do
+ it 'creates a message' do
+ message = message_builder
+ expect(message.content).to eq params[:content]
+ end
+ end
+
+ describe '#perform when message_type is incoming' do
+ context 'when channel is not api' do
+ let(:params) do
+ ActionController::Parameters.new({
+ content: 'test',
+ message_type: 'incoming'
+ })
+ end
+
+ it 'creates throws error when channel is not api' do
+ expect { message_builder }.to raise_error 'Incoming messages are only allowed in Api inboxes'
+ end
+ end
+
+ context 'when channel is api' do
+ let(:channel_api) { create(:channel_api, account: account) }
+ let(:conversation) { create(:conversation, inbox: channel_api.inbox, account: account) }
+ let(:params) do
+ ActionController::Parameters.new({
+ content: 'test',
+ message_type: 'incoming'
+ })
+ end
+
+ it 'creates message when channel is api' do
+ message = message_builder
+ expect(message.message_type).to eq params[:message_type]
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb
new file mode 100644
index 000000000..5d2288aeb
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/contacts/contact_inboxes_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'rails_helper'
+
+RSpec.describe '/api/v1/accounts/{account.id}/contacts/:id/contact_inboxes', type: :request do
+ let(:account) { create(:account) }
+ let(:contact) { create(:contact, account: account) }
+ let(:inbox_1) { create(:inbox, account: account) }
+ let(:channel_api) { create(:channel_api, account: account) }
+ let(:user) { create(:user, account: account) }
+
+ describe 'GET /api/v1/accounts/{account.id}/contacts/:id/contact_inboxes' do
+ context 'when unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when user is logged in' do
+ it 'creates a contact inbox' do
+ expect do
+ post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes",
+ params: { inbox_id: channel_api.inbox.id },
+ headers: user.create_new_auth_token,
+ as: :json
+ end.to change(ContactInbox, :count).by(1)
+
+ expect(response).to have_http_status(:success)
+ expect(contact.reload.contact_inboxes.map(&:inbox_id)).to include(channel_api.inbox.id)
+ end
+
+ it 'throws error when its not an api inbox' do
+ expect do
+ post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contact_inboxes",
+ params: { inbox_id: inbox_1.id },
+ headers: user.create_new_auth_token,
+ as: :json
+ end.to change(ContactInbox, :count).by(0)
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
index 1a1b82442..c1dfc83e0 100644
--- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb
@@ -15,14 +15,44 @@ RSpec.describe 'Contacts API', type: :request do
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let!(:contact) { create(:contact, account: account) }
+ let!(:contact_inbox) { create(:contact_inbox, contact: contact) }
- it 'returns all contacts' do
+ it 'returns all contacts with contact inboxes' do
get "/api/v1/accounts/#{account.id}/contacts",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact.email)
+ expect(response.body).to include(contact_inbox.source_id)
+ expect(response.body).to include(contact_inbox.inbox.name)
+ end
+ end
+ end
+
+ describe 'GET /api/v1/accounts/{account.id}/contacts/search' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get "/api/v1/accounts/#{account.id}/contacts/search"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:contact1) { create(:contact, account: account) }
+ let!(:contact2) { create(:contact, account: account, email: 'test@test.com') }
+
+ it 'returns all contacts with contact inboxes' do
+ get "/api/v1/accounts/#{account.id}/contacts/search",
+ params: { q: contact2.email },
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(response.body).to include(contact2.email)
+ expect(response.body).not_to include(contact1.email)
end
end
end
@@ -65,6 +95,7 @@ RSpec.describe 'Contacts API', type: :request do
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
+ let(:inbox) { create(:inbox, account: account) }
it 'creates the contact' do
expect do
@@ -74,6 +105,15 @@ RSpec.describe 'Contacts API', type: :request do
expect(response).to have_http_status(:success)
end
+
+ it 'creates the contact identifier when inbox id is passed' do
+ expect do
+ post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
+ params: valid_params.merge({ inbox_id: inbox.id })
+ end.to change(ContactInbox, :count).by(1)
+
+ expect(response).to have_http_status(:success)
+ end
end
end
diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
index af9eeaa4f..17ad27118 100644
--- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
@@ -177,15 +177,12 @@ RSpec.describe 'Conversations API', type: :request do
let(:agent) { create(:user, account: account, role: :agent) }
it 'updates last seen' do
- params = { agent_last_seen_at: '-1' }
-
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
- params: params,
as: :json
expect(response).to have_http_status(:success)
- expect(conversation.reload.agent_last_seen_at).to eq(DateTime.strptime(params[:agent_last_seen_at].to_s, '%s'))
+ expect(conversation.reload.agent_last_seen_at).not_to eq nil
end
end
end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 17eea3043..88cae6a82 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -15,9 +15,9 @@ RSpec.describe 'Accounts API', type: :request do
end
it 'calls account builder' do
- allow(account_builder).to receive(:perform).and_return(user)
+ allow(account_builder).to receive(:perform).and_return([user, account])
- params = { account_name: 'test', email: email }
+ params = { account_name: 'test', email: email, user: nil }
post api_v1_accounts_url,
params: params,
@@ -31,7 +31,7 @@ RSpec.describe 'Accounts API', type: :request do
it 'renders error response on invalid params' do
allow(account_builder).to receive(:perform).and_return(nil)
- params = { account_name: nil, email: nil }
+ params = { account_name: nil, email: nil, user: nil }
post api_v1_accounts_url,
params: params,
@@ -46,7 +46,7 @@ RSpec.describe 'Accounts API', type: :request do
it 'ignores confirmed param when called with out super admin token' do
allow(account_builder).to receive(:perform).and_return(nil)
- params = { account_name: 'test', email: email, confirmed: true }
+ params = { account_name: 'test', email: email, confirmed: true, user: nil }
post api_v1_accounts_url,
params: params,
@@ -169,8 +169,7 @@ RSpec.describe 'Accounts API', type: :request do
name: 'New Name',
locale: 'en',
domain: 'example.com',
- support_email: 'care@example.com',
- domain_emails_enabled: true
+ support_email: 'care@example.com'
}
it 'modifies an account' do
@@ -183,7 +182,6 @@ RSpec.describe 'Accounts API', type: :request do
expect(account.reload.name).to eq(params[:name])
expect(account.reload.locale).to eq(params[:locale])
expect(account.reload.domain).to eq(params[:domain])
- expect(account.reload.domain_emails_enabled).to eq(params[:domain_emails_enabled])
expect(account.reload.support_email).to eq(params[:support_email])
end
end
diff --git a/spec/controllers/api/v1/widget/events_controller_spec.rb b/spec/controllers/api/v1/widget/events_controller_spec.rb
index a1b8e4af9..e2c45ab6e 100644
--- a/spec/controllers/api/v1/widget/events_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/events_controller_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe '/api/v1/widget/events', type: :request do
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(params[:name], anything, contact_inbox: contact_inbox,
- event_info: { browser_language: nil, widget_language: nil })
+ event_info: { browser_language: nil, widget_language: nil, browser: anything })
end
end
end
diff --git a/spec/cypress.json b/spec/cypress.json
new file mode 100644
index 000000000..7b1481225
--- /dev/null
+++ b/spec/cypress.json
@@ -0,0 +1,4 @@
+{
+ "baseUrl": "http://localhost:5050",
+ "defaultCommandTimeout": 10000
+}
diff --git a/spec/cypress/app_commands/activerecord_fixtures.rb b/spec/cypress/app_commands/activerecord_fixtures.rb
new file mode 100644
index 000000000..a891e182b
--- /dev/null
+++ b/spec/cypress/app_commands/activerecord_fixtures.rb
@@ -0,0 +1,22 @@
+# you can delete this file if you don't use Rails Test Fixtures
+
+fixtures_dir = command_options.try(:[], 'fixtures_dir')
+fixture_files = command_options.try(:[], 'fixtures')
+
+if defined?(ActiveRecord)
+ require 'active_record/fixtures'
+
+ fixtures_dir ||= ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ fixture_files ||= Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] }
+
+ logger.debug "loading fixtures: { dir: #{fixtures_dir}, files: #{fixture_files} }"
+ ActiveRecord::FixtureSet.reset_cache
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
+ 'Fixtures Done' # this gets returned
+else # this else part can be removed
+ logger.error 'Looks like activerecord_fixtures has to be modified to suite your need'
+ Post.create(title: 'MyCypressFixtures')
+ Post.create(title: 'MyCypressFixtures2')
+ Post.create(title: 'MyRailsFixtures')
+ Post.create(title: 'MyRailsFixtures2')
+end
diff --git a/spec/cypress/app_commands/clean.rb b/spec/cypress/app_commands/clean.rb
new file mode 100644
index 000000000..f35135ab2
--- /dev/null
+++ b/spec/cypress/app_commands/clean.rb
@@ -0,0 +1,10 @@
+if defined?(DatabaseCleaner)
+ # cleaning the database using database_cleaner
+ DatabaseCleaner.strategy = :truncation
+ DatabaseCleaner.clean
+else
+ logger.warn 'add database_cleaner or update cypress/app_commands/clean.rb'
+ Post.delete_all if defined?(Post)
+end
+
+Rails.logger.info 'APPCLEANED' # used by log_fail.rb
diff --git a/spec/cypress/app_commands/eval.rb b/spec/cypress/app_commands/eval.rb
new file mode 100644
index 000000000..3a39bf3e2
--- /dev/null
+++ b/spec/cypress/app_commands/eval.rb
@@ -0,0 +1 @@
+Kernel.eval(command_options) unless command_options.nil?
diff --git a/spec/cypress/app_commands/factory_bot.rb b/spec/cypress/app_commands/factory_bot.rb
new file mode 100644
index 000000000..389151601
--- /dev/null
+++ b/spec/cypress/app_commands/factory_bot.rb
@@ -0,0 +1,12 @@
+Array.wrap(command_options).map do |factory_options|
+ factory_method = factory_options.shift
+ begin
+ logger.debug "running #{factory_method}, #{factory_options}"
+ CypressOnRails::SmartFactoryWrapper.public_send(factory_method, *factory_options)
+ rescue StandardError => e
+ logger.error "#{e.class}: #{e.message}"
+ logger.error e.backtrace.join("\n")
+ logger.error e.record.inspect.to_s if e.is_a?(ActiveRecord::RecordInvalid)
+ raise e
+ end
+end
diff --git a/spec/cypress/app_commands/load_seed.rb b/spec/cypress/app_commands/load_seed.rb
new file mode 100644
index 000000000..a654ab653
--- /dev/null
+++ b/spec/cypress/app_commands/load_seed.rb
@@ -0,0 +1 @@
+Rails.application.load_seed
diff --git a/spec/cypress/app_commands/log_fail.rb b/spec/cypress/app_commands/log_fail.rb
new file mode 100644
index 000000000..8c64be394
--- /dev/null
+++ b/spec/cypress/app_commands/log_fail.rb
@@ -0,0 +1,23 @@
+# This file is called when a cypress spec fails and allows for extra logging to be captured
+filename = command_options.fetch('runnable_full_title', 'no title').gsub(/[^[:print:]]/, '')
+
+# grab last lines until "APPCLEANED" (Make sure in clean.rb to log the text "APPCLEANED")
+system "tail -n 10000 -r log/#{Rails.env}.log | sed \"/APPCLEANED/ q\" | sed 'x;1!H;$!d;x' > 'log/#{filename}.log'"
+
+# create a json debug file for server debugging
+json_result = {}
+json_result['error'] = command_options.fetch('error_message', 'no error message')
+
+if defined?(ActiveRecord::Base)
+ json_result['records'] =
+ ActiveRecord::Base.descendants.each_with_object({}) do |record_class, records|
+ records[record_class.to_s] = record_class.limit(100).map(&:attributes)
+ rescue StandardError => e
+ Rails.logger.info e.message
+ end
+end
+
+filename = command_options.fetch('runnable_full_title', 'no title').gsub(/[^[:print:]]/, '')
+File.open(Rails.root.join("log/#{filename}.json"), 'w+') do |file|
+ file << JSON.pretty_generate(json_result)
+end
diff --git a/spec/cypress/app_commands/scenarios/default.rb b/spec/cypress/app_commands/scenarios/default.rb
new file mode 100644
index 000000000..a654ab653
--- /dev/null
+++ b/spec/cypress/app_commands/scenarios/default.rb
@@ -0,0 +1 @@
+Rails.application.load_seed
diff --git a/spec/cypress/cypress_helper.rb b/spec/cypress/cypress_helper.rb
new file mode 100644
index 000000000..84a820c5a
--- /dev/null
+++ b/spec/cypress/cypress_helper.rb
@@ -0,0 +1,33 @@
+# This is loaded once before the first command is executed
+
+begin
+ require 'database_cleaner'
+rescue LoadError => e
+ puts e.message
+end
+
+begin
+ require 'factory_bot_rails'
+rescue LoadError => e
+ puts e.message
+ begin
+ require 'factory_girl_rails'
+ rescue LoadError => e
+ puts e.message
+ end
+end
+
+require 'cypress_on_rails/smart_factory_wrapper'
+
+factory = CypressOnRails::SimpleRailsFactory
+factory = FactoryBot if defined?(FactoryBot)
+factory = FactoryGirl if defined?(FactoryGirl)
+
+CypressOnRails::SmartFactoryWrapper.configure(
+ always_reload: !Rails.configuration.cache_classes,
+ factory: factory,
+ files: [
+ Rails.root.join('spec/factories.rb'),
+ Rails.root.join('spec/factories/**/*.rb')
+ ]
+)
diff --git a/spec/cypress/integration/happy_paths/admin_dashboard_authentication.js b/spec/cypress/integration/happy_paths/admin_dashboard_authentication.js
new file mode 100644
index 000000000..20ec5f63d
--- /dev/null
+++ b/spec/cypress/integration/happy_paths/admin_dashboard_authentication.js
@@ -0,0 +1,20 @@
+describe('AdminDashboardAuthentication', function() {
+ before(() => {
+ cy.app('clean');
+ cy.appScenario('default')
+ });
+
+ it('authenticates an admin ', function() {
+ cy.visit('/');
+
+ cy.get("[data-testid='email_input']")
+ .clear()
+ .type('john@acme.inc');
+ cy.get("[data-testid='password_input']")
+ .clear()
+ .type('123456');
+
+ cy.get("[data-testid='submit_button']").click();
+ cy.contains('Conversations');
+ });
+});
diff --git a/spec/cypress/plugins/index.js b/spec/cypress/plugins/index.js
new file mode 100644
index 000000000..aa9918d21
--- /dev/null
+++ b/spec/cypress/plugins/index.js
@@ -0,0 +1,21 @@
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+}
diff --git a/spec/cypress/support/commands.js b/spec/cypress/support/commands.js
new file mode 100644
index 000000000..c1f5a772e
--- /dev/null
+++ b/spec/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This is will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
diff --git a/spec/cypress/support/index.js b/spec/cypress/support/index.js
new file mode 100644
index 000000000..edba3c8a9
--- /dev/null
+++ b/spec/cypress/support/index.js
@@ -0,0 +1,21 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+import './on-rails'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/spec/cypress/support/on-rails.js b/spec/cypress/support/on-rails.js
new file mode 100644
index 000000000..068d052ca
--- /dev/null
+++ b/spec/cypress/support/on-rails.js
@@ -0,0 +1,54 @@
+// CypressOnRails: dont remove these command
+Cypress.Commands.add('appCommands', function (body) {
+ cy.log("APP: " + JSON.stringify(body))
+ return cy.request({
+ method: 'POST',
+ url: "/__cypress__/command",
+ body: JSON.stringify(body),
+ log: true,
+ failOnStatusCode: true
+ }).then((response) => {
+ return response.body
+ });
+});
+
+Cypress.Commands.add('app', function (name, command_options) {
+ return cy.appCommands({name: name, options: command_options}).then((body) => {
+ return body[0]
+ });
+});
+
+Cypress.Commands.add('appScenario', function (name, options = {}) {
+ return cy.app('scenarios/' + name, options)
+});
+
+Cypress.Commands.add('appEval', function (code) {
+ return cy.app('eval', code)
+});
+
+Cypress.Commands.add('appFactories', function (options) {
+ return cy.app('factory_bot', options)
+});
+
+Cypress.Commands.add('appFixtures', function (options) {
+ cy.app('activerecord_fixtures', options)
+});
+// CypressOnRails: end
+
+// The next is optional
+// beforeEach(() => {
+// cy.app('clean') // have a look at cypress/app_commands/clean.rb
+// });
+
+// comment this out if you do not want to attempt to log additional info on test fail
+Cypress.on('fail', (err, runnable) => {
+ // allow app to generate additional logging data
+ Cypress.$.ajax({
+ url: '/__cypress__/command',
+ data: JSON.stringify({name: 'log_fail', options: {error_message: err.message, runnable_full_title: runnable.fullTitle() }}),
+ async: false,
+ method: 'POST'
+ });
+
+ throw err;
+});
diff --git a/spec/factories/accounts.rb b/spec/factories/accounts.rb
index 25b6c2f4b..aa8a9b7e6 100644
--- a/spec/factories/accounts.rb
+++ b/spec/factories/accounts.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :account do
sequence(:name) { |n| "Account #{n}" }
- domain_emails_enabled { false }
+ custom_email_domain_enabled { false }
domain { 'test.com' }
support_email { 'support@test.com' }
end
diff --git a/spec/factories/channel/channel_api.rb b/spec/factories/channel/channel_api.rb
new file mode 100644
index 000000000..7a01b5355
--- /dev/null
+++ b/spec/factories/channel/channel_api.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :channel_api, class: 'Channel::Api' do
+ webhook_url { 'http://example.com' }
+ account
+ after(:create) do |channel_api|
+ create(:inbox, channel: channel_api, account: channel_api.account)
+ end
+ end
+end
diff --git a/spec/factories/email_template.rb b/spec/factories/email_template.rb
new file mode 100644
index 000000000..394eb6590
--- /dev/null
+++ b/spec/factories/email_template.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :email_template do
+ name { 'MyString' }
+ end
+end
diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb
index 236095a0a..ed9574e8b 100644
--- a/spec/factories/messages.rb
+++ b/spec/factories/messages.rb
@@ -2,16 +2,16 @@
FactoryBot.define do
factory :message do
- content { 'Message' }
+ content { 'Incoming Message' }
status { 'sent' }
message_type { 'incoming' }
content_type { 'text' }
account { create(:account) }
after(:build) do |message|
- message.sender ||= create(:user, account: message.account)
- message.conversation ||= create(:conversation, account: message.account)
+ message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account)
message.inbox ||= create(:inbox, account: message.account)
+ message.conversation ||= create(:conversation, account: message.account, inbox: message.inbox)
end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 5112a8a51..bbe1e6acd 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -12,8 +12,8 @@ FactoryBot.define do
provider { 'email' }
uid { SecureRandom.uuid }
name { Faker::Name.name }
- nickname { Faker::Name.first_name }
- email { nickname + "@#{SecureRandom.uuid}.com" }
+ display_name { Faker::Name.first_name }
+ email { display_name + "@#{SecureRandom.uuid}.com" }
password { 'password' }
after(:build) do |user, evaluator|
diff --git a/spec/fixtures/files/reply.eml b/spec/fixtures/files/reply.eml
index 82ec19fab..5eb78f1e1 100644
--- a/spec/fixtures/files/reply.eml
+++ b/spec/fixtures/files/reply.eml
@@ -4,7 +4,7 @@ Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE
Subject: Discussion: Let's debate these attachments
Date: Tue, 20 Apr 2020 04:20:20 -0400
In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
-To: "Replies"
+To: "Replies"
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>
X-Mailer: Apple Mail (2.1244.3)
diff --git a/spec/lib/email_templates/db_resolver_service_spec.rb b/spec/lib/email_templates/db_resolver_service_spec.rb
new file mode 100644
index 000000000..7bbb5c508
--- /dev/null
+++ b/spec/lib/email_templates/db_resolver_service_spec.rb
@@ -0,0 +1,80 @@
+require 'rails_helper'
+
+describe ::EmailTemplates::DbResolverService do
+ subject(:resolver) { described_class.using(EmailTemplate, {}) }
+
+ describe '#find_templates' do
+ context 'when template does not exist in db' do
+ it 'return empty array' do
+ expect(resolver.find_templates('test', '', false, [])).to eq([])
+ end
+ end
+
+ context 'when installation template exist in db' do
+ it 'return installation template' do
+ email_template = create(:email_template, name: 'test', body: 'test')
+ handler = ActionView::Template.registered_template_handler(:liquid)
+ template_details = {
+ format: Mime['html'].to_sym,
+ updated_at: email_template.updated_at,
+ virtual_path: 'test'
+ }
+
+ expect(
+ resolver.find_templates('test', '', false, []).first.to_json
+ ).to eq(
+ ActionView::Template.new(
+ email_template.body,
+ "DB Template - #{email_template.id}", handler, template_details
+ ).to_json
+ )
+ end
+ end
+
+ context 'when account template exists in db' do
+ let(:account) { create(:account) }
+ let(:installation_template) { create(:email_template, name: 'test', body: 'test') }
+ let(:account_template) { create(:email_template, name: 'test', body: 'test2', account: account) }
+
+ it 'return account template for current account' do
+ Current.account = account
+ handler = ActionView::Template.registered_template_handler(:liquid)
+ template_details = {
+ format: Mime['html'].to_sym,
+ updated_at: account_template.updated_at,
+ virtual_path: 'test'
+ }
+
+ expect(
+ resolver.find_templates('test', '', false, []).first.to_json
+ ).to eq(
+ ActionView::Template.new(
+ account_template.body,
+ "DB Template - #{account_template.id}", handler, template_details
+ ).to_json
+ )
+ Current.account = nil
+ end
+
+ it 'return installation template when current account dont have template' do
+ Current.account = create(:account)
+ handler = ActionView::Template.registered_template_handler(:liquid)
+ template_details = {
+ format: Mime['html'].to_sym,
+ updated_at: installation_template.updated_at,
+ virtual_path: 'test'
+ }
+
+ expect(
+ resolver.find_templates('test', '', false, []).first.to_json
+ ).to eq(
+ ActionView::Template.new(
+ installation_template.body,
+ "DB Template - #{installation_template.id}", handler, template_details
+ ).to_json
+ )
+ Current.account = nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/integrations/slack/send_on_slack_service_spec.rb b/spec/lib/integrations/slack/send_on_slack_service_spec.rb
index a709c2ce0..0fd8270fd 100644
--- a/spec/lib/integrations/slack/send_on_slack_service_spec.rb
+++ b/spec/lib/integrations/slack/send_on_slack_service_spec.rb
@@ -20,7 +20,7 @@ describe Integrations::Slack::SendOnSlackService do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,
text: message.content,
- username: "Agent: #{message.sender.name}",
+ username: "Contact: #{message.sender.name}",
thread_ts: conversation.identifier,
icon_url: anything
)
diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb
index 17564510e..7608634ac 100644
--- a/spec/lib/webhooks/trigger_spec.rb
+++ b/spec/lib/webhooks/trigger_spec.rb
@@ -5,10 +5,10 @@ describe Webhooks::Trigger do
describe '#execute' do
it 'triggers webhook' do
- params = { hello: 'hello' }
- url = 'htpps://test.com'
+ params = { hello: :hello }
+ url = 'https://test.com'
- expect(RestClient).to receive(:post).with(url, params).once
+ expect(RestClient).to receive(:post).with(url, params.to_json, { accept: :json, content_type: :json }).once
trigger.execute(url, params)
end
end
diff --git a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
index 7f7227879..2fb72d141 100644
--- a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
+++ b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :maile
let(:mail) { described_class.conversation_creation(conversation, agent).deliver_now }
it 'renders the subject' do
- expect(mail.subject).to eq("#{agent.name}, A new conversation [ID - #{conversation
+ expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation
.display_id}] has been created in #{conversation.inbox&.name}.")
end
@@ -29,7 +29,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :maile
let(:mail) { described_class.conversation_assignment(conversation, agent).deliver_now }
it 'renders the subject' do
- expect(mail.subject).to eq("#{agent.name}, A new conversation [ID - #{conversation.display_id}] has been assigned to you.")
+ expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation.display_id}] has been assigned to you.")
end
it 'renders the receiver email' do
diff --git a/spec/mailers/confirmation_instructions_spec.rb b/spec/mailers/confirmation_instructions_spec.rb
index f6d908e2d..9914c1585 100644
--- a/spec/mailers/confirmation_instructions_spec.rb
+++ b/spec/mailers/confirmation_instructions_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe 'Confirmation Instructions', type: :mailer do
expect(mail.body).to match(
"#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(inviter_val.account.name)}, has invited you to try out Chatwoot!"
)
+ Current.account = nil
end
end
end
diff --git a/spec/mailers/conversation_reply_mailer_spec.rb b/spec/mailers/conversation_reply_mailer_spec.rb
index 61c3c8f98..3761f02b9 100644
--- a/spec/mailers/conversation_reply_mailer_spec.rb
+++ b/spec/mailers/conversation_reply_mailer_spec.rb
@@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe ConversationReplyMailer, type: :mailer do
- describe 'reply_with_summary' do
+ describe 'reply' do
let!(:account) { create(:account) }
let!(:agent) { create(:user, email: 'agent1@example.com', account: account) }
let(:class_instance) { described_class.new }
@@ -13,7 +13,7 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(true)
end
- context 'with all mails' do
+ context 'with summary' do
let(:conversation) { create(:conversation, assignee: agent) }
let(:message) { create(:message, conversation: conversation) }
let(:private_message) { create(:message, content: 'This is a private message', conversation: conversation) }
@@ -33,12 +33,57 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
end
end
+ context 'without assignee' do
+ let(:conversation) { create(:conversation, assignee: nil) }
+ let(:message) { create(:message, conversation: conversation) }
+ let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now }
+
+ it 'has correct name' do
+ expect(mail[:from].display_names).to eq(['Notifications'])
+ end
+ end
+
+ context 'without summary' do
+ let(:conversation) { create(:conversation, assignee: agent, account: account).reload }
+ let(:message_1) { create(:message, conversation: conversation, account: account, content: 'Outgoing Message 1').reload }
+ let(:message_2) { build(:message, conversation: conversation, account: account, message_type: 'outgoing', content: 'Outgoing Message 2') }
+ let(:private_message) do
+ create(:message,
+ content: 'This is a private message',
+ conversation: conversation,
+ account: account,
+ message_type: 'outgoing').reload
+ end
+ let(:mail) { described_class.reply_without_summary(message_1.conversation, Time.zone.now - 1.minute).deliver_now }
+
+ before do
+ message_2.save
+ end
+
+ it 'renders the subject' do
+ expect(mail.subject).to eq("[##{message_2.conversation.display_id}] New messages on this conversation")
+ end
+
+ it 'not have private notes' do
+ # make the message private
+ private_message.private = true
+ private_message.save
+ expect(mail.body.decoded).not_to include(private_message.content)
+ end
+
+ it 'onlies have the messages sent by the agent' do
+ expect(mail.body.decoded).not_to include(message_1.content)
+ expect(mail.body.decoded).to include(message_2.content)
+ end
+ end
+
context 'when custom domain and email is not enabled' do
let(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, user: agent, inbox: inbox) }
let(:conversation) { create(:conversation, assignee: agent, inbox: inbox_member.inbox, account: account) }
let!(:message) { create(:message, conversation: conversation, account: account) }
let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now }
+ let(:domain) { account.domain || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN', false) }
it 'renders the receiver email' do
expect(mail.to).to eq([message&.conversation&.contact&.email])
@@ -49,33 +94,37 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
end
it 'sets the correct custom message id' do
- expect(mail.message_id).to eq("")
+ expect(mail.message_id).to eq("conversation/#{conversation.uuid}/messages/#{message.id}@#{domain}")
end
it 'sets the correct in reply to id' do
- expect(mail.in_reply_to).to eq("")
+ expect(mail.in_reply_to).to eq("account/#{conversation.account.id}/conversation/#{conversation.uuid}@#{domain}")
end
end
- context 'when the cutsom domain emails are enabled' do
- let(:conversation) { create(:conversation, assignee: agent) }
- let(:message) { create(:message, conversation: conversation) }
+ context 'when the custom domain emails are enabled' do
+ let(:account) { create(:account) }
+ let(:conversation) { create(:conversation, assignee: agent, account: account).reload }
+ let(:message) { create(:message, conversation: conversation, account: account, inbox: conversation.inbox) }
let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now }
before do
account = conversation.account
account.domain = 'example.com'
account.support_email = 'support@example.com'
- account.domain_emails_enabled = true
- account.save
+ account.enable_features('inbound_emails')
+ account.save!
end
it 'sets reply to email to be based on the domain' do
- reply_to_email = "reply+to+#{message.conversation.uuid}@#{conversation.account.domain}"
+ reply_to_email = "reply+#{message.conversation.uuid}@#{conversation.account.domain}"
+ reply_to = "#{agent.available_name} <#{reply_to_email}>"
+ expect(mail['REPLY-TO'].value).to eq(reply_to)
expect(mail.reply_to).to eq([reply_to_email])
end
it 'sets the from email to be the support email' do
+ expect(mail['FROM'].value).to eq("#{agent.available_name} <#{conversation.account.support_email}>")
expect(mail.from).to eq([conversation.account.support_email])
end
diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb
index 1c5cc2939..b1e298b3d 100644
--- a/spec/models/conversation_spec.rb
+++ b/spec/models/conversation_spec.rb
@@ -88,8 +88,8 @@ RSpec.describe Conversation, type: :model do
it 'creates conversation activities' do
# create_activity
- expect(conversation.messages.pluck(:content)).to include("Conversation was marked resolved by #{old_assignee.name}")
- expect(conversation.messages.pluck(:content)).to include("Assigned to #{new_assignee.name} by #{old_assignee.name}")
+ expect(conversation.messages.pluck(:content)).to include("Conversation was marked resolved by #{old_assignee.available_name}")
+ expect(conversation.messages.pluck(:content)).to include("Assigned to #{new_assignee.available_name} by #{old_assignee.available_name}")
end
end
@@ -315,6 +315,7 @@ RSpec.describe Conversation, type: :model do
inbox_id: conversation.inbox_id,
status: conversation.status,
timestamp: conversation.created_at.to_i,
+ can_reply: true,
channel: 'Channel::WebWidget',
user_last_seen_at: conversation.user_last_seen_at.to_i,
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
@@ -347,4 +348,45 @@ RSpec.describe Conversation, type: :model do
expect(conversation.status).to eq('bot')
end
end
+
+ describe '#can_reply?' do
+ describe 'on channels without 24 hour restriction' do
+ let(:conversation) { create(:conversation) }
+
+ it 'returns true' do
+ expect(conversation.can_reply?).to eq true
+ end
+ end
+
+ describe 'on channels with 24 hour restriction' do
+ let!(:facebook_channel) { create(:channel_facebook_page) }
+ let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: facebook_channel.account) }
+ let!(:conversation) { create(:conversation, inbox: facebook_inbox, account: facebook_channel.account) }
+
+ it 'returns false if there are no incoming messages' do
+ expect(conversation.can_reply?).to eq false
+ end
+
+ it 'return false if last incoming message is outside of 24 hour window' do
+ create(
+ :message,
+ account: conversation.account,
+ inbox: facebook_inbox,
+ conversation: conversation,
+ created_at: Time.now - 25.hours
+ )
+ expect(conversation.can_reply?).to eq false
+ end
+
+ it 'return true if last incoming message is inside 24 hour window' do
+ create(
+ :message,
+ account: conversation.account,
+ inbox: facebook_inbox,
+ conversation: conversation
+ )
+ expect(conversation.can_reply?).to eq true
+ end
+ end
+ end
end
diff --git a/spec/presenters/conversations/event_data_presenter_spec.rb b/spec/presenters/conversations/event_data_presenter_spec.rb
index 76326b616..dea4ae6b8 100644
--- a/spec/presenters/conversations/event_data_presenter_spec.rb
+++ b/spec/presenters/conversations/event_data_presenter_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Conversations::EventDataPresenter do
messages: [],
inbox_id: conversation.inbox_id,
status: conversation.status,
+ can_reply: conversation.can_reply?,
channel: conversation.inbox.channel_type,
timestamp: conversation.created_at.to_i,
user_last_seen_at: conversation.user_last_seen_at.to_i,
diff --git a/spec/services/facebook/send_on_facebook_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb
index 742044abd..25a84f9e7 100644
--- a/spec/services/facebook/send_on_facebook_service_spec.rb
+++ b/spec/services/facebook/send_on_facebook_service_spec.rb
@@ -6,6 +6,7 @@ describe Facebook::SendOnFacebookService do
before do
allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true)
allow(bot).to receive(:deliver)
+ create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
end
let!(:account) { create(:account) }
@@ -20,39 +21,43 @@ describe Facebook::SendOnFacebookService do
describe '#perform' do
context 'without reply' do
it 'if message is private' do
- create(:message, message_type: 'outgoing', private: true, inbox: facebook_inbox, account: account)
+ message = create(:message, message_type: 'outgoing', private: true, inbox: facebook_inbox, account: account)
+ ::Facebook::SendOnFacebookService.new(message: message).perform
expect(bot).not_to have_received(:deliver)
end
it 'if inbox channel is not facebook page' do
- create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
+ message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
+ expect { ::Facebook::SendOnFacebookService.new(message: message).perform }.to raise_error 'Invalid channel service was called'
expect(bot).not_to have_received(:deliver)
end
it 'if message is not outgoing' do
- create(:message, message_type: 'incoming', inbox: facebook_inbox, account: account)
+ message = create(:message, message_type: 'incoming', inbox: facebook_inbox, account: account)
+ ::Facebook::SendOnFacebookService.new(message: message).perform
expect(bot).not_to have_received(:deliver)
end
it 'if message has an FB ID' do
- create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, source_id: SecureRandom.uuid)
+ message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, source_id: SecureRandom.uuid)
+ ::Facebook::SendOnFacebookService.new(message: message).perform
expect(bot).not_to have_received(:deliver)
end
end
context 'with reply' do
it 'if message is sent from chatwoot and is outgoing' do
- create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
- create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
+ message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
+ ::Facebook::SendOnFacebookService.new(message: message).perform
expect(bot).to have_received(:deliver)
end
it 'if message with attachment is sent from chatwoot and is outgoing' do
- create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
message = build(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
message.save!
+ ::Facebook::SendOnFacebookService.new(message: message).perform
expect(bot).to have_received(:deliver)
end
end
diff --git a/spec/services/twilio/send_on_twilio_service_spec.rb b/spec/services/twilio/send_on_twilio_service_spec.rb
index 56a8283c3..3f114a00b 100644
--- a/spec/services/twilio/send_on_twilio_service_spec.rb
+++ b/spec/services/twilio/send_on_twilio_service_spec.rb
@@ -25,22 +25,26 @@ describe Twilio::SendOnTwilioService do
describe '#perform' do
context 'without reply' do
it 'if message is private' do
- create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account)
+ message = create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account)
+ ::Twilio::SendOnTwilioService.new(message: message).perform
expect(twilio_client).not_to have_received(:messages)
end
- it 'if inbox channel is not facebook page' do
- create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
+ it 'if inbox channel is not twilio' do
+ message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
+ expect { ::Twilio::SendOnTwilioService.new(message: message).perform }.to raise_error 'Invalid channel service was called'
expect(twilio_client).not_to have_received(:messages)
end
it 'if message is not outgoing' do
- create(:message, message_type: 'incoming', inbox: twilio_inbox, account: account)
+ message = create(:message, message_type: 'incoming', inbox: twilio_inbox, account: account)
+ ::Twilio::SendOnTwilioService.new(message: message).perform
expect(twilio_client).not_to have_received(:messages)
end
it 'if message has an source id' do
- create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, source_id: SecureRandom.uuid)
+ message = create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, source_id: SecureRandom.uuid)
+ ::Twilio::SendOnTwilioService.new(message: message).perform
expect(twilio_client).not_to have_received(:messages)
end
end
@@ -53,6 +57,7 @@ describe Twilio::SendOnTwilioService do
outgoing_message = create(
:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation
)
+ ::Twilio::SendOnTwilioService.new(message: outgoing_message).perform
expect(outgoing_message.reload.source_id).to eq('1234')
end
@@ -69,6 +74,8 @@ describe Twilio::SendOnTwilioService do
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
message.save!
+
+ ::Twilio::SendOnTwilioService.new(message: message).perform
end
end
end
diff --git a/spec/services/twitter/send_on_twitter_service_spec.rb b/spec/services/twitter/send_on_twitter_service_spec.rb
index 017fcf27c..4e0f4e91e 100644
--- a/spec/services/twitter/send_on_twitter_service_spec.rb
+++ b/spec/services/twitter/send_on_twitter_service_spec.rb
@@ -41,22 +41,26 @@ describe Twitter::SendOnTwitterService do
describe '#perform' do
context 'without reply' do
it 'if inbox channel is not twitter profile' do
- create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
+ message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
+ expect { ::Twitter::SendOnTwitterService.new(message: message).perform }.to raise_error 'Invalid channel service was called'
expect(twitter_client).not_to have_received(:send_direct_message)
end
it 'if message is private' do
- create(:message, message_type: 'outgoing', private: true, inbox: twitter_inbox, account: account)
+ message = create(:message, message_type: 'outgoing', private: true, inbox: twitter_inbox, account: account)
+ ::Twitter::SendOnTwitterService.new(message: message).perform
expect(twitter_client).not_to have_received(:send_direct_message)
end
it 'if message has source_id' do
- create(:message, message_type: 'outgoing', source_id: '123', inbox: widget_inbox, account: account)
+ message = create(:message, message_type: 'outgoing', source_id: '123', inbox: twitter_inbox, account: account)
+ ::Twitter::SendOnTwitterService.new(message: message).perform
expect(twitter_client).not_to have_received(:send_direct_message)
end
it 'if message is not outgoing' do
- create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account)
+ message = create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account)
+ ::Twitter::SendOnTwitterService.new(message: message).perform
expect(twitter_client).not_to have_received(:send_direct_message)
end
end
@@ -64,15 +68,17 @@ describe Twitter::SendOnTwitterService do
context 'with reply' do
it 'if conversation is a direct message' do
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: dm_conversation)
- create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: dm_conversation)
+ message = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: dm_conversation)
+ ::Twitter::SendOnTwitterService.new(message: message).perform
expect(twitter_client).to have_received(:send_direct_message)
end
it 'if conversation is a tweet' do
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: tweet_conversation)
- tweet = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: tweet_conversation)
+ message = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: tweet_conversation)
+ ::Twitter::SendOnTwitterService.new(message: message).perform
expect(twitter_client).to have_received(:send_tweet_reply)
- expect(tweet.reload.source_id).to eq '12345'
+ expect(message.reload.source_id).to eq '12345'
end
end
end
diff --git a/spec/workers/conversation_reply_email_worker_spec.rb b/spec/workers/conversation_reply_email_worker_spec.rb
index 0ebcb58d3..11be19b3f 100644
--- a/spec/workers/conversation_reply_email_worker_spec.rb
+++ b/spec/workers/conversation_reply_email_worker_spec.rb
@@ -5,6 +5,7 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do
let(:perform_at) { (Time.zone.today + 6.hours).to_datetime }
let(:scheduled_job) { described_class.perform_at(perform_at, 1, Time.zone.now) }
let(:conversation) { build(:conversation, display_id: nil) }
+ let(:message) { build(:message, conversation: conversation, content_type: 'incoming_email', inbox: conversation.inbox) }
describe 'testing ConversationSummaryEmailWorker' do
before do
@@ -12,6 +13,7 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do
allow(Conversation).to receive(:find).and_return(conversation)
mailer = double
allow(ConversationReplyMailer).to receive(:reply_with_summary).and_return(mailer)
+ allow(ConversationReplyMailer).to receive(:reply_without_summary).and_return(mailer)
allow(mailer).to receive(:deliver_later).and_return(true)
end
@@ -28,10 +30,16 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do
end
context 'with actions performed by the worker' do
- it 'calls ConversationSummaryMailer' do
+ it 'calls ConversationSummaryMailer#reply_with_summary when last incoming message was not email' do
described_class.new.perform(1, Time.zone.now)
expect(ConversationReplyMailer).to have_received(:reply_with_summary)
end
+
+ it 'calls ConversationSummaryMailer#reply_without_summary when last incoming message was from email' do
+ message.save
+ described_class.new.perform(1, Time.zone.now)
+ expect(ConversationReplyMailer).to have_received(:reply_without_summary)
+ end
end
end
end
diff --git a/swagger/definitions/resource/user.yml b/swagger/definitions/resource/user.yml
index 049e2478e..ecdcbe30a 100644
--- a/swagger/definitions/resource/user.yml
+++ b/swagger/definitions/resource/user.yml
@@ -15,5 +15,5 @@ properties:
enum: ['agent', 'administrator']
confirmed:
type: boolean
- nickname:
+ display_name:
type: string
diff --git a/swagger/swagger.json b/swagger/swagger.json
index 2ac8cc1f2..9fc9a31df 100644
--- a/swagger/swagger.json
+++ b/swagger/swagger.json
@@ -1087,7 +1087,7 @@
"confirmed": {
"type": "boolean"
},
- "nickname": {
+ "display_name": {
"type": "string"
}
}
diff --git a/yarn.lock b/yarn.lock
index d78e760f4..9d485c69a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -812,6 +812,50 @@
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
+"@cypress/listr-verbose-renderer@0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a"
+ integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=
+ dependencies:
+ chalk "^1.1.3"
+ cli-cursor "^1.0.2"
+ date-fns "^1.27.2"
+ figures "^1.7.0"
+
+"@cypress/request@2.88.5":
+ version "2.88.5"
+ resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7"
+ integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.5.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+"@cypress/xvfb@1.2.4":
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a"
+ integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==
+ dependencies:
+ debug "^3.1.0"
+ lodash.once "^4.1.1"
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
@@ -1150,6 +1194,16 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
+"@types/sinonjs__fake-timers@6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
+ integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
+
+"@types/sizzle@2.3.2":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
+ integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -1531,20 +1585,16 @@ anymatch@^3.0.3:
normalize-path "^3.0.0"
picomatch "^2.0.4"
-aplayer@^1.5.8:
- version "1.10.1"
- resolved "https://registry.yarnpkg.com/aplayer/-/aplayer-1.10.1.tgz#318289206107452cc39e8f552fa6cc6cb459a90c"
- integrity sha512-HAfyxgCUTLAqtYlxzzK9Fyqg6y+kZ9CqT1WfeWE8FSzwspT6oBqWOZHANPHF3RGTtC33IsyEgrfthPDzU5r9kQ==
- dependencies:
- balloon-css "^0.5.0"
- promise-polyfill "7.1.0"
- smoothscroll "0.4.0"
-
aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+arch@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf"
+ integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==
+
are-we-there-yet@~1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
@@ -1708,6 +1758,11 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
+async@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
+ integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
+
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -1957,11 +2012,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
-balloon-css@^0.5.0:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/balloon-css/-/balloon-css-0.5.2.tgz#9e2163565a136c9d4aa20e8400772ce3b738d3ff"
- integrity sha512-zheJpzwyNrG4t39vusA67v3BYg1HTVXOF8cErPEHzWK88PEOFwgo6Ea9VHOgOWNMgeuOtFVtB73NE2NWl9uDyQ==
-
base64-js@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -2016,15 +2066,15 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
-bluebird@^3.1.1, bluebird@^3.5.5:
+bluebird@3.7.2, bluebird@^3.1.1, bluebird@^3.5.5:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
- version "4.11.8"
- resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
- integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+ version "4.11.9"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
+ integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
body-parser@1.19.0:
version "1.19.0"
@@ -2187,6 +2237,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
+buffer-crc32@~0.2.3:
+ version "0.2.13"
+ resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+ integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -2286,6 +2341,11 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
+cachedir@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
+ integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
+
caller-callsite@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
@@ -2421,6 +2481,11 @@ chartjs-color@^2.0.0:
chartjs-color-string "^0.6.0"
color-convert "^1.9.3"
+check-more-types@2.24.0:
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
+ integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
+
chokidar@^2.0.2, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2485,6 +2550,13 @@ clean-stack@^2.0.0:
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+cli-cursor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+ integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=
+ dependencies:
+ restore-cursor "^1.0.1"
+
cli-cursor@^2.0.0, cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
@@ -2499,6 +2571,16 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
+cli-table3@0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
+ integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
+ dependencies:
+ object-assign "^4.1.0"
+ string-width "^2.1.1"
+ optionalDependencies:
+ colors "^1.1.2"
+
cli-truncate@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
@@ -2634,6 +2716,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
+colors@^1.1.2:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+ integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -2641,15 +2728,20 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
+commander@4.1.1, commander@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
+ integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+
commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
-commander@^4.0.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
- integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+common-tags@1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
+ integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
commondir@^1.0.1:
version "1.0.1"
@@ -2703,7 +2795,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-concat-stream@^1.5.0:
+concat-stream@^1.5.0, concat-stream@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -3153,6 +3245,49 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+cypress@^4.10.0:
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.10.0.tgz#6b507f4637af6a65ea285953f899951d65e82416"
+ integrity sha512-eFv1WPp4zFrAgZ6mwherBGVsTpHvay/hEF5F7U7yfAkTxsUQn/ZG/LdX67fIi3bKDTQXYzFv/CvywlQSeug8Bg==
+ dependencies:
+ "@cypress/listr-verbose-renderer" "0.4.1"
+ "@cypress/request" "2.88.5"
+ "@cypress/xvfb" "1.2.4"
+ "@types/sinonjs__fake-timers" "6.0.1"
+ "@types/sizzle" "2.3.2"
+ arch "2.1.2"
+ bluebird "3.7.2"
+ cachedir "2.3.0"
+ chalk "2.4.2"
+ check-more-types "2.24.0"
+ cli-table3 "0.5.1"
+ commander "4.1.1"
+ common-tags "1.8.0"
+ debug "4.1.1"
+ eventemitter2 "6.4.2"
+ execa "1.0.0"
+ executable "4.1.1"
+ extract-zip "1.7.0"
+ fs-extra "8.1.0"
+ getos "3.2.1"
+ is-ci "2.0.0"
+ is-installed-globally "0.3.2"
+ lazy-ass "1.6.0"
+ listr "0.14.3"
+ lodash "4.17.15"
+ log-symbols "3.0.0"
+ minimist "1.2.5"
+ moment "2.26.0"
+ ospath "1.2.2"
+ pretty-bytes "5.3.0"
+ ramda "0.26.1"
+ request-progress "3.0.0"
+ supports-color "7.1.0"
+ tmp "0.1.0"
+ untildify "4.0.0"
+ url "0.11.0"
+ yauzl "2.10.0"
+
damerau-levenshtein@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
@@ -3191,6 +3326,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
dependencies:
ms "2.0.0"
+debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
@@ -3198,20 +3340,13 @@ debug@=3.1.0:
dependencies:
ms "2.0.0"
-debug@^3.0.0, debug@^3.1.1, debug@^3.2.5:
+debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
dependencies:
ms "^2.1.1"
-debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
- integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
- dependencies:
- ms "^2.1.1"
-
decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -3502,9 +3637,9 @@ elegant-spinner@^1.0.1:
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
elliptic@^6.0.0:
- version "6.5.2"
- resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
- integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
+ integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
@@ -3880,6 +4015,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+eventemitter2@6.4.2:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970"
+ integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw==
+
eventemitter3@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
@@ -3910,7 +4050,7 @@ exec-sh@^0.3.2:
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
-execa@^1.0.0:
+execa@1.0.0, execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
@@ -3939,6 +4079,18 @@ execa@^3.2.0, execa@^3.4.0:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
+executable@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
+ integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==
+ dependencies:
+ pify "^2.2.0"
+
+exit-hook@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+ integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
+
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -4076,6 +4228,16 @@ extract-from-css@^0.4.4:
dependencies:
css "^2.1.0"
+extract-zip@1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
+ integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
+ dependencies:
+ concat-stream "^1.6.2"
+ debug "^2.6.9"
+ mkdirp "^0.5.4"
+ yauzl "^2.10.0"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -4127,6 +4289,13 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
+fd-slicer@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
+ integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
+ dependencies:
+ pend "~1.2.0"
+
figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -4372,6 +4541,15 @@ from2@^2.1.0:
inherits "^2.0.1"
readable-stream "^2.0.0"
+fs-extra@8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@@ -4497,6 +4675,13 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+getos@3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
+ integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==
+ dependencies:
+ async "^3.2.0"
+
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -4531,6 +4716,13 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl
once "^1.3.0"
path-is-absolute "^1.0.0"
+global-dirs@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201"
+ integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==
+ dependencies:
+ ini "^1.3.5"
+
global-modules@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -4609,6 +4801,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+ integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -5169,7 +5366,7 @@ is-callable@^1.1.4, is-callable@^1.1.5:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
-is-ci@^2.0.0:
+is-ci@2.0.0, is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
@@ -5288,6 +5485,14 @@ is-glob@^4.0.0, is-glob@^4.0.1:
dependencies:
is-extglob "^2.1.1"
+is-installed-globally@0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
+ integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
+ dependencies:
+ global-dirs "^2.0.1"
+ is-path-inside "^3.0.1"
+
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -5336,6 +5541,11 @@ is-path-inside@^2.1.0:
dependencies:
path-is-inside "^1.0.2"
+is-path-inside@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
+ integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
+
is-plain-obj@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -6021,6 +6231,13 @@ json5@^2.1.2:
dependencies:
minimist "^1.2.5"
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -6081,6 +6298,11 @@ last-call-webpack-plugin@^3.0.0:
lodash "^4.17.5"
webpack-sources "^1.1.0"
+lazy-ass@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
+ integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM=
+
lcid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -6168,7 +6390,7 @@ listr-verbose-renderer@^0.5.0:
date-fns "^1.27.2"
figures "^2.0.0"
-listr@^0.14.3:
+listr@0.14.3, listr@^0.14.3:
version "0.14.3"
resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586"
integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==
@@ -6275,6 +6497,11 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+lodash.once@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+ integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -6305,11 +6532,23 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.12:
+lodash@4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.12:
+ version "4.17.19"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+ integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
+
+log-symbols@3.0.0, log-symbols@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
+ integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
+ dependencies:
+ chalk "^2.4.2"
+
log-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -6317,13 +6556,6 @@ log-symbols@^1.0.2:
dependencies:
chalk "^1.0.0"
-log-symbols@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
- integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
- dependencies:
- chalk "^2.4.2"
-
log-update@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708"
@@ -6612,7 +6844,7 @@ minimatch@^3.0.4, minimatch@~3.0.2:
dependencies:
brace-expansion "^1.1.7"
-minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+minimist@1.2.5, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
@@ -6669,13 +6901,18 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
-"mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
+"mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
+moment@2.26.0:
+ version "2.26.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
+ integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
+
moment@^2.10.6:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -7097,6 +7334,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
+onetime@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+ integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
+
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -7189,6 +7431,11 @@ osenv@0, osenv@^0.1.4:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
+ospath@1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
+ integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
+
p-defer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
@@ -7450,6 +7697,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+ integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
+
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -7460,7 +7712,7 @@ picomatch@^2.0.4, picomatch@^2.0.5:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
-pify@^2.0.0, pify@^2.3.0:
+pify@^2.0.0, pify@^2.2.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
@@ -8214,6 +8466,11 @@ prettier@^1.16.4, prettier@^1.18.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
+pretty-bytes@5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"
+ integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==
+
pretty-format@^24.8.0, pretty-format@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
@@ -8268,11 +8525,6 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
-promise-polyfill@7.1.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.0.tgz#4d749485b44577c14137591c6f36e5d7e2dd3378"
- integrity sha512-P6NJ2wU/8fac44ENORsuqT8TiolKGB2u0fEClPtXezn7w5cmLIjM/7mhPlTebke2EPr6tmqZbXvnX0TxwykGrg==
-
prompts@^2.0.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.1.tgz#b63a9ce2809f106fa9ae1277c275b167af46ea05"
@@ -8408,6 +8660,11 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
+ramda@0.26.1:
+ version "0.26.1"
+ resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
+ integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
+
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -8640,6 +8897,13 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
+request-progress@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
+ integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
+ dependencies:
+ throttleit "^1.0.0"
+
request-promise-core@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
@@ -8756,6 +9020,14 @@ resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.
dependencies:
path-parse "^1.0.6"
+restore-cursor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+ integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=
+ dependencies:
+ exit-hook "^1.0.0"
+ onetime "^1.0.0"
+
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@@ -9151,11 +9423,6 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
-smoothscroll@0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/smoothscroll/-/smoothscroll-0.4.0.tgz#40e507b46461408ba1b787d0081e1e883c4124a5"
- integrity sha512-sggQ3U2Un38b3+q/j1P4Y4fCboCtoUIaBYoge+Lb6Xg1H8RTIif/hugVr+ErMtIDpvBbhQfTjtiTeYAfbw1ZGQ==
-
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -9620,6 +9887,13 @@ supports-color@6.1.0, supports-color@^6.1.0:
dependencies:
has-flag "^3.0.0"
+supports-color@7.1.0, supports-color@^7.0.0, supports-color@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+ integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+ dependencies:
+ has-flag "^4.0.0"
+
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -9632,13 +9906,6 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^7.0.0, supports-color@^7.1.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
- integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
- dependencies:
- has-flag "^4.0.0"
-
supports-hyperlinks@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47"
@@ -9771,6 +10038,11 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
+throttleit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+ integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
+
through2@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -9806,6 +10078,13 @@ tinycolor2@^1.1.2:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
+tmp@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
+ integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
+ dependencies:
+ rimraf "^2.6.3"
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -10047,6 +10326,11 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -10065,6 +10349,11 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
+untildify@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
+ integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
+
upath@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
@@ -10099,7 +10388,7 @@ url-parse@^1.4.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
-url@^0.11.0:
+url@0.11.0, url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
@@ -10211,13 +10500,6 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
-vue-aplayer@~0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/vue-aplayer/-/vue-aplayer-0.1.1.tgz#c5b486c664ac2818618ccf29a6dd1e4e2856dcdc"
- integrity sha1-xbSGxmSsKBhhjM8ppt0eTihW3Nw=
- dependencies:
- aplayer "^1.5.8"
-
vue-audio@~0.0.7:
version "0.0.12"
resolved "https://registry.yarnpkg.com/vue-audio/-/vue-audio-0.0.12.tgz#0e4d7af19bc2cc8924a90a57e01edd7de0945cc4"
@@ -10844,3 +11126,11 @@ yargs@^7.0.0:
which-module "^1.0.0"
y18n "^3.2.1"
yargs-parser "^5.0.0"
+
+yauzl@2.10.0, yauzl@^2.10.0:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
+ integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
+ dependencies:
+ buffer-crc32 "~0.2.3"
+ fd-slicer "~1.1.0"