Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
100f0b07ce
19 changed files with 284 additions and 96 deletions
|
@ -31,7 +31,7 @@ class ContactInboxBuilder
|
||||||
return unless @contact.phone_number
|
return unless @contact.phone_number
|
||||||
|
|
||||||
# whatsapp doesn't want the + in e164 format
|
# whatsapp doesn't want the + in e164 format
|
||||||
"#{@contact.phone_number}.delete('+')"
|
@contact.phone_number.delete('+').to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def twilio_source_id
|
def twilio_source_id
|
||||||
|
|
|
@ -51,6 +51,6 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.permit(:name, :email, :password, custom_attributes: {})
|
params.permit(:name, :display_name, :email, :password, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
suggestionsPlugin,
|
suggestionsPlugin,
|
||||||
triggerCharacters,
|
triggerCharacters,
|
||||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||||
import { EditorState } from 'prosemirror-state';
|
import { EditorState, Selection } from 'prosemirror-state';
|
||||||
import { defaultMarkdownParser } from 'prosemirror-markdown';
|
import { defaultMarkdownParser } from 'prosemirror-markdown';
|
||||||
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
|
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
|
||||||
|
|
||||||
|
@ -61,23 +61,28 @@ export default {
|
||||||
mixins: [eventListenerMixins],
|
mixins: [eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
|
editorId: { type: String, default: '' },
|
||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
isPrivate: { type: Boolean, default: false },
|
isPrivate: { type: Boolean, default: false },
|
||||||
isFormatMode: { type: Boolean, default: false },
|
|
||||||
enableSuggestions: { type: Boolean, default: true },
|
enableSuggestions: { type: Boolean, default: true },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lastValue: null,
|
|
||||||
showUserMentions: false,
|
showUserMentions: false,
|
||||||
showCannedMenu: false,
|
showCannedMenu: false,
|
||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
cannedSearchTerm: '',
|
cannedSearchTerm: '',
|
||||||
editorView: null,
|
editorView: null,
|
||||||
range: null,
|
range: null,
|
||||||
|
state: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
contentFromEditor() {
|
||||||
|
return addMentionsToMarkdownSerializer(
|
||||||
|
defaultMarkdownSerializer
|
||||||
|
).serialize(this.editorView.state.doc);
|
||||||
|
},
|
||||||
plugins() {
|
plugins() {
|
||||||
if (!this.enableSuggestions) {
|
if (!this.enableSuggestions) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -102,7 +107,6 @@ export default {
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
this.mentionSearchKey = '';
|
this.mentionSearchKey = '';
|
||||||
this.showUserMentions = false;
|
this.showUserMentions = false;
|
||||||
this.editorView = null;
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onKeyDown: ({ event }) => {
|
onKeyDown: ({ event }) => {
|
||||||
|
@ -131,7 +135,6 @@ export default {
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
this.cannedSearchTerm = '';
|
this.cannedSearchTerm = '';
|
||||||
this.showCannedMenu = false;
|
this.showCannedMenu = false;
|
||||||
this.editorView = null;
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onKeyDown: ({ event }) => {
|
onKeyDown: ({ event }) => {
|
||||||
|
@ -149,54 +152,57 @@ export default {
|
||||||
this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
|
this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
|
||||||
},
|
},
|
||||||
value(newValue = '') {
|
value(newValue = '') {
|
||||||
if (newValue !== this.lastValue) {
|
if (newValue !== this.contentFromEditor) {
|
||||||
const { tr } = this.state;
|
this.reloadState();
|
||||||
if (this.isFormatMode) {
|
|
||||||
this.state = createState(
|
|
||||||
newValue,
|
|
||||||
this.placeholder,
|
|
||||||
this.plugins,
|
|
||||||
this.isFormatMode
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
|
||||||
this.state = this.view.state.apply(tr);
|
|
||||||
}
|
|
||||||
this.view.updateState(this.state);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
editorId() {
|
||||||
|
this.reloadState();
|
||||||
|
},
|
||||||
|
isPrivate() {
|
||||||
|
this.reloadState();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.state = createState(this.value, this.placeholder, this.plugins);
|
this.state = createState(this.value, this.placeholder, this.plugins);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.view = new EditorView(this.$refs.editor, {
|
this.createEditorView();
|
||||||
state: this.state,
|
this.editorView.updateState(this.state);
|
||||||
dispatchTransaction: tx => {
|
|
||||||
this.state = this.state.apply(tx);
|
|
||||||
this.emitOnChange();
|
|
||||||
},
|
|
||||||
handleDOMEvents: {
|
|
||||||
keyup: () => {
|
|
||||||
this.onKeyup();
|
|
||||||
},
|
|
||||||
focus: () => {
|
|
||||||
this.onFocus();
|
|
||||||
},
|
|
||||||
blur: () => {
|
|
||||||
this.onBlur();
|
|
||||||
},
|
|
||||||
paste: (view, event) => {
|
|
||||||
const data = event.clipboardData.files;
|
|
||||||
if (data.length > 0) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.focusEditorInputField();
|
this.focusEditorInputField();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
reloadState() {
|
||||||
|
this.state = createState(this.value, this.placeholder, this.plugins);
|
||||||
|
this.editorView.updateState(this.state);
|
||||||
|
this.focusEditorInputField();
|
||||||
|
},
|
||||||
|
createEditorView() {
|
||||||
|
this.editorView = new EditorView(this.$refs.editor, {
|
||||||
|
state: this.state,
|
||||||
|
dispatchTransaction: tx => {
|
||||||
|
this.state = this.state.apply(tx);
|
||||||
|
this.emitOnChange();
|
||||||
|
},
|
||||||
|
handleDOMEvents: {
|
||||||
|
keyup: () => {
|
||||||
|
this.onKeyup();
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
this.onFocus();
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
this.onBlur();
|
||||||
|
},
|
||||||
|
paste: (view, event) => {
|
||||||
|
const data = event.clipboardData.files;
|
||||||
|
if (data.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
handleKeyEvents(e) {
|
handleKeyEvents(e) {
|
||||||
if (hasPressedAltAndPKey(e)) {
|
if (hasPressedAltAndPKey(e)) {
|
||||||
this.focusEditorInputField();
|
this.focusEditorInputField();
|
||||||
|
@ -206,47 +212,59 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusEditorInputField() {
|
focusEditorInputField() {
|
||||||
this.$refs.editor.querySelector('div.ProseMirror-woot-style').focus();
|
const { tr } = this.editorView.state;
|
||||||
|
const selection = Selection.atEnd(tr.doc);
|
||||||
|
|
||||||
|
this.editorView.dispatch(tr.setSelection(selection));
|
||||||
|
this.editorView.focus();
|
||||||
},
|
},
|
||||||
insertMentionNode(mentionItem) {
|
insertMentionNode(mentionItem) {
|
||||||
if (!this.view) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const node = this.view.state.schema.nodes.mention.create({
|
const node = this.editorView.state.schema.nodes.mention.create({
|
||||||
userId: mentionItem.key,
|
userId: mentionItem.key,
|
||||||
userFullName: mentionItem.label,
|
userFullName: mentionItem.label,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tr = this.view.state.tr.replaceWith(
|
const tr = this.editorView.state.tr.replaceWith(
|
||||||
this.range.from,
|
this.range.from,
|
||||||
this.range.to,
|
this.range.to,
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
this.state = this.view.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
return this.emitOnChange();
|
return this.emitOnChange();
|
||||||
},
|
},
|
||||||
|
|
||||||
insertCannedResponse(cannedItem) {
|
insertCannedResponse(cannedItem) {
|
||||||
if (!this.view) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tr = this.view.state.tr.insertText(
|
const tr = this.editorView.state.tr.insertText(
|
||||||
cannedItem,
|
cannedItem,
|
||||||
this.range.from,
|
this.range.from,
|
||||||
this.range.to
|
this.range.to
|
||||||
);
|
);
|
||||||
this.state = this.view.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
return this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
|
||||||
|
// Hacky fix for #5501
|
||||||
|
this.state = createState(
|
||||||
|
this.contentFromEditor,
|
||||||
|
this.placeholder,
|
||||||
|
this.plugins
|
||||||
|
);
|
||||||
|
this.editorView.updateState(this.state);
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
emitOnChange() {
|
emitOnChange() {
|
||||||
this.view.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
this.lastValue = addMentionsToMarkdownSerializer(
|
|
||||||
defaultMarkdownSerializer
|
this.$emit('input', this.contentFromEditor);
|
||||||
).serialize(this.state.doc);
|
|
||||||
this.$emit('input', this.lastValue);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hideMentions() {
|
hideMentions() {
|
||||||
this.showUserMentions = false;
|
this.showUserMentions = false;
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<woot-message-editor
|
<woot-message-editor
|
||||||
v-else
|
v-else
|
||||||
v-model="message"
|
v-model="message"
|
||||||
|
:editor-id="editorStateId"
|
||||||
class="input"
|
class="input"
|
||||||
:is-private="isOnPrivateNote"
|
:is-private="isOnPrivateNote"
|
||||||
:placeholder="messagePlaceHolder"
|
:placeholder="messagePlaceHolder"
|
||||||
|
@ -429,6 +430,13 @@ export default {
|
||||||
profilePath() {
|
profilePath() {
|
||||||
return frontendURL(`accounts/${this.accountId}/profile/settings`);
|
return frontendURL(`accounts/${this.accountId}/profile/settings`);
|
||||||
},
|
},
|
||||||
|
conversationId() {
|
||||||
|
return this.currentChat.id;
|
||||||
|
},
|
||||||
|
editorStateId() {
|
||||||
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
|
return key;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const LOCAL_STORAGE_KEYS = {
|
export const LOCAL_STORAGE_KEYS = {
|
||||||
DISMISSED_UPDATES: 'dismissedUpdates',
|
DISMISSED_UPDATES: 'dismissedUpdates',
|
||||||
WIDGET_BUILDER: 'widgetBubble_',
|
WIDGET_BUILDER: 'widgetBubble_',
|
||||||
|
DRAFT_MESSAGES: 'draftMessages',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LocalStorage = {
|
export const LocalStorage = {
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
articleTitle: '',
|
articleTitle: '',
|
||||||
articleContent: '',
|
articleContent: '',
|
||||||
|
saveArticle: () => {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
#
|
#
|
||||||
# index_articles_on_associated_article_id (associated_article_id)
|
# index_articles_on_associated_article_id (associated_article_id)
|
||||||
# index_articles_on_author_id (author_id)
|
# index_articles_on_author_id (author_id)
|
||||||
# index_articles_on_slug (slug)
|
# index_articles_on_slug (slug) UNIQUE
|
||||||
#
|
#
|
||||||
class Article < ApplicationRecord
|
class Article < ApplicationRecord
|
||||||
include PgSearch::Model
|
include PgSearch::Model
|
||||||
|
@ -45,6 +45,8 @@ class Article < ApplicationRecord
|
||||||
belongs_to :author, class_name: 'User'
|
belongs_to :author, class_name: 'User'
|
||||||
|
|
||||||
before_validation :ensure_account_id
|
before_validation :ensure_account_id
|
||||||
|
before_validation :ensure_article_slug
|
||||||
|
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :category_id, presence: true
|
validates :category_id, presence: true
|
||||||
validates :author_id, presence: true
|
validates :author_id, presence: true
|
||||||
|
@ -112,4 +114,8 @@ class Article < ApplicationRecord
|
||||||
def ensure_account_id
|
def ensure_account_id
|
||||||
self.account_id = portal&.account_id
|
self.account_id = portal&.account_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_article_slug
|
||||||
|
self.slug ||= "#{Time.now.utc.to_i}-#{title.underscore.parameterize(separator: '-')}" if title.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
class ContactInbox < ApplicationRecord
|
class ContactInbox < ApplicationRecord
|
||||||
include Pubsubable
|
include Pubsubable
|
||||||
|
include RegexHelper
|
||||||
validates :inbox_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
validates :contact_id, presence: true
|
validates :contact_id, presence: true
|
||||||
validates :source_id, presence: true
|
validates :source_id, presence: true
|
||||||
|
@ -51,10 +52,10 @@ class ContactInbox < ApplicationRecord
|
||||||
|
|
||||||
def validate_twilio_source_id
|
def validate_twilio_source_id
|
||||||
# https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164
|
# https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164
|
||||||
if inbox.channel.medium == 'sms' && !/\+[1-9]\d{1,14}\z/.match?(source_id)
|
if inbox.channel.medium == 'sms' && !TWILIO_CHANNEL_SMS_REGEX.match?(source_id)
|
||||||
errors.add(:source_id, 'invalid source id for twilio sms inbox. valid Regex /\+[1-9]\d{1,14}\z/')
|
errors.add(:source_id, "invalid source id for twilio sms inbox. valid Regex #{TWILIO_CHANNEL_SMS_REGEX}")
|
||||||
elsif inbox.channel.medium == 'whatsapp' && !/whatsapp:\+[1-9]\d{1,14}\z/.match?(source_id)
|
elsif inbox.channel.medium == 'whatsapp' && !TWILIO_CHANNEL_WHATSAPP_REGEX.match?(source_id)
|
||||||
errors.add(:source_id, 'invalid source id for twilio whatsapp inbox. valid Regex /whatsapp:\+[1-9]\d{1,14}\z/')
|
errors.add(:source_id, "invalid source id for twilio whatsapp inbox. valid Regex #{TWILIO_CHANNEL_WHATSAPP_REGEX}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,8 +63,15 @@ class ContactInbox < ApplicationRecord
|
||||||
errors.add(:source_id, "invalid source id for Email inbox. valid Regex #{Devise.email_regexp}") unless Devise.email_regexp.match?(source_id)
|
errors.add(:source_id, "invalid source id for Email inbox. valid Regex #{Devise.email_regexp}") unless Devise.email_regexp.match?(source_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_whatsapp_source_id
|
||||||
|
return if WHATSAPP_CHANNEL_REGEX.match?(source_id)
|
||||||
|
|
||||||
|
errors.add(:source_id, "invalid source id for whatsapp inbox. valid Regex #{WHATSAPP_CHANNEL_REGEX}")
|
||||||
|
end
|
||||||
|
|
||||||
def valid_source_id_format?
|
def valid_source_id_format?
|
||||||
validate_twilio_source_id if inbox.channel_type == 'Channel::TwilioSms'
|
validate_twilio_source_id if inbox.channel_type == 'Channel::TwilioSms'
|
||||||
validate_email_source_id if inbox.channel_type == 'Channel::Email'
|
validate_email_source_id if inbox.channel_type == 'Channel::Email'
|
||||||
|
validate_whatsapp_source_id if inbox.channel_type == 'Channel::Whatsapp'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -241,7 +241,7 @@ class Message < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_attachments_limit(_attachment)
|
def validate_attachments_limit(_attachment)
|
||||||
errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS
|
errors.add(:attachments, message: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_conversation_activity
|
def set_conversation_activity
|
||||||
|
|
6
db/migrate/20220930025317_add_unique_index_to_slug.rb
Normal file
6
db/migrate/20220930025317_add_unique_index_to_slug.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
class AddUniqueIndexToSlug < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
remove_index :articles, :slug
|
||||||
|
add_index :articles, :slug, unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_09_26_164441) do
|
ActiveRecord::Schema.define(version: 2022_09_30_025317) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -134,7 +134,7 @@ ActiveRecord::Schema.define(version: 2022_09_26_164441) do
|
||||||
t.string "slug", null: false
|
t.string "slug", null: false
|
||||||
t.index ["associated_article_id"], name: "index_articles_on_associated_article_id"
|
t.index ["associated_article_id"], name: "index_articles_on_associated_article_id"
|
||||||
t.index ["author_id"], name: "index_articles_on_author_id"
|
t.index ["author_id"], name: "index_articles_on_author_id"
|
||||||
t.index ["slug"], name: "index_articles_on_slug"
|
t.index ["slug"], name: "index_articles_on_slug", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "attachments", id: :serial, force: :cascade do |t|
|
create_table "attachments", id: :serial, force: :cascade do |t|
|
||||||
|
|
|
@ -6,4 +6,8 @@ module RegexHelper
|
||||||
# shouldn't start with a underscore or hyphen
|
# shouldn't start with a underscore or hyphen
|
||||||
UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE = Regexp.new('\A[\p{L}\p{N}]+[\p{L}\p{N}_-]+\Z')
|
UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE = Regexp.new('\A[\p{L}\p{N}]+[\p{L}\p{N}_-]+\Z')
|
||||||
MENTION_REGEX = Regexp.new('\[(@[\w_. ]+)\]\(mention://(?:user|team)/\d+/(.*?)+\)')
|
MENTION_REGEX = Regexp.new('\[(@[\w_. ]+)\]\(mention://(?:user|team)/\d+/(.*?)+\)')
|
||||||
|
|
||||||
|
TWILIO_CHANNEL_SMS_REGEX = Regexp.new('^\+\d{1,14}\z')
|
||||||
|
TWILIO_CHANNEL_WHATSAPP_REGEX = Regexp.new('^whatsapp:\+\d{1,14}\z')
|
||||||
|
WHATSAPP_CHANNEL_REGEX = Regexp.new('^\d{1,14}\z')
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: contact.phone_number
|
source_id: contact.phone_number
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
|
@ -27,7 +27,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: twilio_inbox.id
|
inbox_id: twilio_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
@ -38,8 +38,8 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: '+224213223422'
|
source_id: '+224213223422'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).not_to eq(existing_contact_inbox.id)
|
||||||
expect(contact_inbox.source_id).not_to be('+224213223422')
|
expect(contact_inbox.source_id).to eq('+224213223422')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
|
@ -48,7 +48,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: twilio_inbox.id
|
inbox_id: twilio_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).not_to be(contact.phone_number)
|
expect(contact_inbox.source_id).to eq(contact.phone_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: "whatsapp:#{contact.phone_number}"
|
source_id: "whatsapp:#{contact.phone_number}"
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
|
@ -74,7 +74,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: twilio_inbox.id
|
inbox_id: twilio_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
@ -85,8 +85,8 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: 'whatsapp:+555555'
|
source_id: 'whatsapp:+555555'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).not_to eq(existing_contact_inbox.id)
|
||||||
expect(contact_inbox.source_id).not_to be('whatsapp:+55555')
|
expect(contact_inbox.source_id).to eq('whatsapp:+555555')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
|
@ -95,7 +95,53 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: twilio_inbox.id
|
inbox_id: twilio_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).not_to be("whatsapp:#{contact.phone_number}")
|
expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'whatsapp inbox' do
|
||||||
|
let(:whatsapp_inbox) { create(:channel_whatsapp, account: account, sync_templates: false, validate_provider_config: false).inbox }
|
||||||
|
|
||||||
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: whatsapp_inbox.id,
|
||||||
|
source_id: contact.phone_number&.delete('+')
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: whatsapp_inbox.id
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: whatsapp_inbox.id,
|
||||||
|
source_id: '555555'
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
||||||
|
expect(contact_inbox.source_id).not_to be('555555')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
|
contact_inbox = described_class.new(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: whatsapp_inbox.id
|
||||||
|
).perform
|
||||||
|
|
||||||
|
expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -111,7 +157,7 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: contact.phone_number
|
source_id: contact.phone_number
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
|
@ -121,7 +167,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: sms_inbox.id
|
inbox_id: sms_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
@ -132,8 +178,8 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: '+224213223422'
|
source_id: '+224213223422'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).not_to eq(existing_contact_inbox.id)
|
||||||
expect(contact_inbox.source_id).not_to be('+224213223422')
|
expect(contact_inbox.source_id).to eq('+224213223422')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
|
@ -142,7 +188,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: sms_inbox.id
|
inbox_id: sms_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).not_to be(contact.phone_number)
|
expect(contact_inbox.source_id).to eq(contact.phone_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -158,7 +204,7 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: contact.email
|
source_id: contact.email
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
|
||||||
|
@ -168,7 +214,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: email_inbox.id
|
inbox_id: email_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
@ -179,8 +225,8 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: 'xyc@xyc.com'
|
source_id: 'xyc@xyc.com'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).not_to eq(existing_contact_inbox.id)
|
||||||
expect(contact_inbox.source_id).not_to be('xyc@xyc.com')
|
expect(contact_inbox.source_id).to eq('xyc@xyc.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
|
||||||
|
@ -189,7 +235,7 @@ describe ::ContactInboxBuilder do
|
||||||
inbox_id: email_inbox.id
|
inbox_id: email_inbox.id
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).not_to be(contact.email)
|
expect(contact_inbox.source_id).to eq(contact.email)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -205,7 +251,7 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: 'test'
|
source_id: 'test'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
|
@ -216,8 +262,8 @@ describe ::ContactInboxBuilder do
|
||||||
source_id: 'test'
|
source_id: 'test'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).not_to eq(existing_contact_inbox.id)
|
||||||
expect(contact_inbox.source_id).not_to be('test')
|
expect(contact_inbox.source_id).to eq('test')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
|
||||||
|
|
|
@ -96,15 +96,24 @@ RSpec.describe 'Platform Users API', type: :request do
|
||||||
|
|
||||||
it 'creates a new user and permissible for the user' do
|
it 'creates a new user and permissible for the user' do
|
||||||
expect do
|
expect do
|
||||||
post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!',
|
post '/platform/api/v1/users/', params: { name: 'test', display_name: 'displaytest',
|
||||||
|
email: 'test@test.com', password: 'Password1!',
|
||||||
custom_attributes: { test: 'test_create' } },
|
custom_attributes: { test: 'test_create' } },
|
||||||
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
headers: { api_access_token: platform_app.access_token.token }, as: :json
|
||||||
end.not_to enqueue_mail
|
end.not_to enqueue_mail
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
data = JSON.parse(response.body)
|
data = JSON.parse(response.body)
|
||||||
expect(data['email']).to eq('test@test.com')
|
expect(data).to match(
|
||||||
expect(data['custom_attributes']['test']).to eq('test_create')
|
hash_including(
|
||||||
|
'name' => 'test',
|
||||||
|
'display_name' => 'displaytest',
|
||||||
|
'email' => 'test@test.com',
|
||||||
|
'custom_attributes' => {
|
||||||
|
'test' => 'test_create'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
|
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,7 @@ FactoryBot.define do
|
||||||
account_id { 1 }
|
account_id { 1 }
|
||||||
category_id { 1 }
|
category_id { 1 }
|
||||||
author_id { 1 }
|
author_id { 1 }
|
||||||
title { 'MyString' }
|
title { Faker::Movie.title }
|
||||||
slug { 'MyString' }
|
|
||||||
content { 'MyText' }
|
content { 'MyText' }
|
||||||
description { 'MyDescrption' }
|
description { 'MyDescrption' }
|
||||||
status { 1 }
|
status { 1 }
|
||||||
|
|
|
@ -15,6 +15,8 @@ def generate_source_id(contact_inbox)
|
||||||
contact_inbox.inbox.channel.medium == 'sms' ? Faker::PhoneNumber.cell_phone_in_e164 : "whatsapp:#{Faker::PhoneNumber.cell_phone_in_e164}"
|
contact_inbox.inbox.channel.medium == 'sms' ? Faker::PhoneNumber.cell_phone_in_e164 : "whatsapp:#{Faker::PhoneNumber.cell_phone_in_e164}"
|
||||||
when 'Channel::Email'
|
when 'Channel::Email'
|
||||||
"#{SecureRandom.uuid}@acme.inc"
|
"#{SecureRandom.uuid}@acme.inc"
|
||||||
|
when 'Channel::Whatsapp'
|
||||||
|
Faker::PhoneNumber.cell_phone_in_e164.delete('+')
|
||||||
else
|
else
|
||||||
SecureRandom.uuid
|
SecureRandom.uuid
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,11 +119,23 @@ RSpec.describe Article, type: :model do
|
||||||
records = portal_1.articles.search(params)
|
records = portal_1.articles.search(params)
|
||||||
expect(records.count).to eq(2)
|
expect(records.count).to eq(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'auto saves article slug' do
|
||||||
|
article = create(:article, category_id: category_1.id, title: 'the awesome article 1', content: 'This is the content', portal_id: portal_1.id,
|
||||||
|
author_id: user.id)
|
||||||
|
expect(article.slug).to include('the-awesome-article-1')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with pagination' do
|
context 'with pagination' do
|
||||||
it 'returns paginated articles' do
|
it 'returns paginated articles' do
|
||||||
create_list(:article, 30, category_id: category_2.id, slug: 'title-1', title: 'title 1', portal_id: portal_2.id, author_id: user.id)
|
build_list(:article, 30) do |record, i|
|
||||||
|
record.category_id = category_2.id
|
||||||
|
record.title = "title #{i}"
|
||||||
|
record.portal_id = portal_2.id
|
||||||
|
record.author_id = user.id
|
||||||
|
record.save!
|
||||||
|
end
|
||||||
params = { category_slug: 'category_2' }
|
params = { category_slug: 'category_2' }
|
||||||
records = portal_2.articles.search(params)
|
records = portal_2.articles.search(params)
|
||||||
expect(records.count).to eq(25)
|
expect(records.count).to eq(25)
|
||||||
|
|
|
@ -37,4 +37,59 @@ RSpec.describe ContactInbox do
|
||||||
expect(obj.pubsub_token).to eq(new_token)
|
expect(obj.pubsub_token).to eq(new_token)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'validations' do
|
||||||
|
context 'when source_id' do
|
||||||
|
it 'validates whatsapp channel source_id' do
|
||||||
|
whatsapp_inbox = create(:channel_whatsapp, sync_templates: false, validate_provider_config: false).inbox
|
||||||
|
contact = create(:contact)
|
||||||
|
valid_source_id = build(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: '1234567890')
|
||||||
|
ci_character_in_source_id = build(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: '1234567890aaa')
|
||||||
|
ci_plus_in_source_id = build(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: '+1234567890')
|
||||||
|
expect(valid_source_id.valid?).to be(true)
|
||||||
|
expect(ci_character_in_source_id.valid?).to be(false)
|
||||||
|
expect(ci_character_in_source_id.errors.full_messages).to eq(
|
||||||
|
['Source invalid source id for whatsapp inbox. valid Regex (?-mix:^\\d{1,14}\\z)']
|
||||||
|
)
|
||||||
|
expect(ci_plus_in_source_id.valid?).to be(false)
|
||||||
|
expect(ci_plus_in_source_id.errors.full_messages).to eq(
|
||||||
|
['Source invalid source id for whatsapp inbox. valid Regex (?-mix:^\\d{1,14}\\z)']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validates twilio sms channel source_id' do
|
||||||
|
twilio_sms_inbox = create(:channel_twilio_sms).inbox
|
||||||
|
contact = create(:contact)
|
||||||
|
valid_source_id = build(:contact_inbox, contact: contact, inbox: twilio_sms_inbox, source_id: '+1234567890')
|
||||||
|
ci_character_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_sms_inbox, source_id: '+1234567890aaa')
|
||||||
|
ci_without_plus_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_sms_inbox, source_id: '1234567890')
|
||||||
|
expect(valid_source_id.valid?).to be(true)
|
||||||
|
expect(ci_character_in_source_id.valid?).to be(false)
|
||||||
|
expect(ci_character_in_source_id.errors.full_messages).to eq(
|
||||||
|
['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,14}\\z)']
|
||||||
|
)
|
||||||
|
expect(ci_without_plus_in_source_id.valid?).to be(false)
|
||||||
|
expect(ci_without_plus_in_source_id.errors.full_messages).to eq(
|
||||||
|
['Source invalid source id for twilio sms inbox. valid Regex (?-mix:^\\+\\d{1,14}\\z)']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validates twilio whatsapp channel source_id' do
|
||||||
|
twilio_whatsapp_inbox = create(:channel_twilio_sms, medium: :whatsapp).inbox
|
||||||
|
contact = create(:contact)
|
||||||
|
valid_source_id = build(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:+1234567890')
|
||||||
|
ci_character_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:+1234567890aaa')
|
||||||
|
ci_without_plus_in_source_id = build(:contact_inbox, contact: contact, inbox: twilio_whatsapp_inbox, source_id: 'whatsapp:1234567890')
|
||||||
|
expect(valid_source_id.valid?).to be(true)
|
||||||
|
expect(ci_character_in_source_id.valid?).to be(false)
|
||||||
|
expect(ci_character_in_source_id.errors.full_messages).to eq(
|
||||||
|
['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,14}\\z)']
|
||||||
|
)
|
||||||
|
expect(ci_without_plus_in_source_id.valid?).to be(false)
|
||||||
|
expect(ci_without_plus_in_source_id.errors.full_messages).to eq(
|
||||||
|
['Source invalid source id for twilio whatsapp inbox. valid Regex (?-mix:^whatsapp:\\+\\d{1,14}\\z)']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -106,6 +106,19 @@ RSpec.describe Message, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when attachments size maximum' do
|
||||||
|
let(:message) { build(:message, content_type: nil, account: create(:account)) }
|
||||||
|
|
||||||
|
it 'add errors to message for attachment size is more than allowed limit' do
|
||||||
|
16.times.each 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')
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(message.errors.messages).to eq({ attachments: ['exceeded maximum allowed'] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when email notifiable message' do
|
context 'when email notifiable message' do
|
||||||
let(:message) { build(:message, content_type: nil, account: create(:account)) }
|
let(:message) { build(:message, content_type: nil, account: create(:account)) }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue