Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd016c5ad4 | ||
|
|
dcd18072b4 | ||
|
|
4244945e7b | ||
|
|
17725e4dd0 | ||
|
|
d55bb63aa0 | ||
|
|
bda2d1d9a4 | ||
|
|
6e22695b85 | ||
|
|
146e46d79f | ||
|
|
0af27a2387 | ||
|
|
7380f0e7ce | ||
|
|
2c5a0abcce | ||
|
|
f23e33c1a9 | ||
|
|
5588d6e344 | ||
|
|
59ace66c6f | ||
|
|
decef1ad52 | ||
|
|
0e19a4196b | ||
|
|
04dd65687b | ||
|
|
5f503b1a57 | ||
|
|
5f6e974531 | ||
|
|
a023eabf59 | ||
|
|
cb1e25a487 | ||
|
|
bcf7c6c45a | ||
|
|
b411b6ca21 | ||
|
|
83231b9678 |
@@ -109,9 +109,9 @@ gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.12.0', require: false
|
||||
gem 'sentry-rails', '>= 5.13.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.12.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.13.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.1.3'
|
||||
@@ -200,6 +200,8 @@ group :development do
|
||||
# profiling
|
||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+17
-11
@@ -451,13 +451,16 @@ GEM
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.7.4)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 7.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
@@ -555,10 +558,12 @@ GEM
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.1)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.4.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.1)
|
||||
@@ -599,7 +604,7 @@ GEM
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
@@ -704,13 +709,13 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.12.0)
|
||||
sentry-rails (5.13.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.12.0)
|
||||
sentry-ruby (5.12.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sentry-ruby (5.13.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.12.0)
|
||||
sentry-ruby (~> 5.12.0)
|
||||
sentry-sidekiq (5.13.0)
|
||||
sentry-ruby (~> 5.13.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
@@ -760,7 +765,7 @@ GEM
|
||||
stripe (8.5.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.2.2)
|
||||
thor (1.3.0)
|
||||
tilt (2.2.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
@@ -895,6 +900,7 @@ DEPENDENCIES
|
||||
listen
|
||||
lograge (~> 0.14.0)
|
||||
maxminddb
|
||||
meta_request
|
||||
mock_redis
|
||||
neighbor
|
||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||
@@ -929,9 +935,9 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.12.0)
|
||||
sentry-rails (>= 5.13.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.12.0)
|
||||
sentry-sidekiq (>= 5.13.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.1.3)
|
||||
sidekiq-cron (>= 1.11.0)
|
||||
|
||||
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def logo
|
||||
@portal.logo.purge if @portal.logo.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def process_attached_logo
|
||||
blob_id = params[:blob_id]
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
|
||||
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
|
||||
deletePortal(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}`);
|
||||
}
|
||||
|
||||
deleteLogo(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortalsAPI;
|
||||
|
||||
@@ -16,3 +16,23 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-hooks {
|
||||
.formulate-input[data-type='checkbox'] {
|
||||
.formulate-input-wrapper {
|
||||
@apply flex;
|
||||
|
||||
.formulate-input-element {
|
||||
@apply pr-2;
|
||||
|
||||
input {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formulate-input-element-decorator {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
|
||||
}),
|
||||
isAICTAModalDismissed() {
|
||||
return this.uiSettings.is_open_ai_cta_modal_dismissed;
|
||||
@@ -70,7 +71,8 @@ export default {
|
||||
return (
|
||||
this.isAdmin &&
|
||||
!this.isAIIntegrationEnabled &&
|
||||
!this.isAICTAModalDismissed
|
||||
!this.isAICTAModalDismissed &&
|
||||
this.isAChatwootInstance
|
||||
);
|
||||
},
|
||||
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
|
||||
|
||||
@@ -114,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
// constants
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { REPLY_POLICY } from 'shared/constants/links';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
@@ -285,14 +284,6 @@ export default {
|
||||
return this.currentChat.unread_count || 0;
|
||||
},
|
||||
inboxSupportsReplyTo() {
|
||||
if (
|
||||
!this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.MESSAGE_REPLY_TO
|
||||
)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
|
||||
outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
|
||||
@@ -349,7 +340,7 @@ export default {
|
||||
// method available in mixin, need to ensure that integrations are present
|
||||
await this.fetchIntegrationsIfRequired();
|
||||
|
||||
if (!this.isAIIntegrationEnabled) {
|
||||
if (!this.isLabelSuggestionFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,6 @@ import {
|
||||
replaceSignature,
|
||||
extractTextFromMarkdown,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
@@ -276,11 +275,7 @@ export default {
|
||||
return (
|
||||
this.inReplyTo?.id &&
|
||||
!this.isPrivate &&
|
||||
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.MESSAGE_REPLY_TO
|
||||
)
|
||||
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
|
||||
);
|
||||
},
|
||||
showRichContentEditor() {
|
||||
|
||||
@@ -192,7 +192,7 @@ export default {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Messages will be marked as sent for the Email channel if they have a source ID.
|
||||
if (this.isAnEmailChannel) {
|
||||
return !!this.sourceId;
|
||||
}
|
||||
@@ -201,11 +201,12 @@ export default {
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isAFacebookInbox ||
|
||||
this.isASmsInbox
|
||||
this.isASmsInbox ||
|
||||
this.isATelegramChannel
|
||||
) {
|
||||
return this.sourceId && this.isSent;
|
||||
}
|
||||
// There is no source id for the line channel
|
||||
// All messages will be mark as sent for the Line channel, as there is no source ID.
|
||||
if (this.isALineChannel) {
|
||||
return true;
|
||||
}
|
||||
@@ -216,15 +217,15 @@ export default {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
this.isASmsInbox
|
||||
this.isASmsInbox ||
|
||||
this.isAFacebookInbox
|
||||
) {
|
||||
return this.sourceId && this.isDelivered;
|
||||
}
|
||||
// We will consider messages as delivered for web widget inbox and API inbox if they are sent
|
||||
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
return this.isSent;
|
||||
}
|
||||
@@ -238,12 +239,6 @@ export default {
|
||||
if (!this.showStatusIndicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt >= this.createdAt;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isATwilioChannel ||
|
||||
@@ -252,6 +247,10 @@ export default {
|
||||
return this.sourceId && this.isRead;
|
||||
}
|
||||
|
||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||
return this.isRead;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,6 +16,5 @@ export const FEATURE_FLAGS = {
|
||||
TEAM_MANAGEMENT: 'team_management',
|
||||
VOICE_RECORDER: 'voice_recorder',
|
||||
AUDIT_LOGS: 'audit_logs',
|
||||
MESSAGE_REPLY_TO: 'message_reply_to',
|
||||
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
|
||||
};
|
||||
|
||||
@@ -96,3 +96,15 @@ export const getArticleSearchURL = ({
|
||||
|
||||
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
|
||||
};
|
||||
|
||||
export const hasValidAvatarUrl = avatarUrl => {
|
||||
try {
|
||||
const { host: avatarUrlHost } = new URL(avatarUrl);
|
||||
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
|
||||
avatarUrlHost
|
||||
);
|
||||
return avatarUrl && !isFromGravatar;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isValidURL,
|
||||
conversationListPageURL,
|
||||
getArticleSearchURL,
|
||||
hasValidAvatarUrl,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
@@ -164,4 +165,29 @@ describe('#URL Helpers', () => {
|
||||
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasValidAvatarUrl', () => {
|
||||
test('should return true for valid non-Gravatar URL', () => {
|
||||
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
|
||||
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('should return false for a Gravatar URL (gravatar)', () => {
|
||||
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle invalid URL', () => {
|
||||
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
|
||||
});
|
||||
|
||||
test('should return false for empty or undefined URL', () => {
|
||||
expect(hasValidAvatarUrl('')).toBe(false);
|
||||
expect(hasValidAvatarUrl()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,7 +223,10 @@
|
||||
"LOGO": {
|
||||
"LABEL": "Logo",
|
||||
"UPLOAD_BUTTON": "Upload logo",
|
||||
"HELP_TEXT": "This logo will be displayed on the portal header."
|
||||
"HELP_TEXT": "This logo will be displayed on the portal header.",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully",
|
||||
"IMAGE_UPLOAD_ERROR": "Logo deleted successfully",
|
||||
"IMAGE_DELETE_ERROR": "Error while deleting logo"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Name",
|
||||
|
||||
@@ -15,18 +15,26 @@ export default {
|
||||
currentChat: 'getSelectedChat',
|
||||
replyMode: 'draftMessages/getReplyEditorMode',
|
||||
}),
|
||||
isAIIntegrationEnabled() {
|
||||
return !!this.appIntegrations.find(
|
||||
aiIntegration() {
|
||||
return this.appIntegrations.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
);
|
||||
)?.hooks[0];
|
||||
},
|
||||
isAIIntegrationEnabled() {
|
||||
return !!this.aiIntegration;
|
||||
},
|
||||
isLabelSuggestionFeatureEnabled() {
|
||||
if (this.aiIntegration) {
|
||||
const { settings = {} } = this.aiIntegration || {};
|
||||
return settings.label_suggestion;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isFetchingAppIntegrations() {
|
||||
return this.uiFlags.isFetching;
|
||||
},
|
||||
hookId() {
|
||||
return this.appIntegrations.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
).hooks[0].id;
|
||||
return this.aiIntegration.id;
|
||||
},
|
||||
draftMessage() {
|
||||
return this.$store.getters['draftMessages/get'](this.draftKey);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<div v-else class="chat-message-input is-focused">
|
||||
<resizable-text-area
|
||||
id="chat-input"
|
||||
:rows="1"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.CHAT_INPUT_PLACEHOLDER')
|
||||
"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<resizable-text-area
|
||||
v-model="articleTitle"
|
||||
type="text"
|
||||
rows="1"
|
||||
:rows="1"
|
||||
class="article-heading"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.TITLE_PLACEHOLDER')"
|
||||
@focus="onFocus"
|
||||
|
||||
+73
-18
@@ -14,21 +14,24 @@
|
||||
>
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<div class="mb-4">
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL') }}
|
||||
</label>
|
||||
<div class="flex items-center flex-row">
|
||||
<thumbnail :username="name" size="56px" variant="square" />
|
||||
<woot-button
|
||||
v-if="false"
|
||||
class="ml-3"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="upload"
|
||||
size="small"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.UPLOAD_BUTTON') }}
|
||||
</woot-button>
|
||||
<woot-avatar-uploader
|
||||
ref="imageUpload"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL')"
|
||||
:src="logoUrl"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<div v-if="showDeleteButton" class="avatar-delete-btn">
|
||||
<woot-button
|
||||
type="button"
|
||||
color-scheme="alert"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="deleteAvatar"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="mt-1 mb-0 text-xs text-slate-600 dark:text-slate-400 not-italic"
|
||||
@@ -87,17 +90,20 @@
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { isDomain } from 'shared/helpers/Validators';
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
@@ -118,6 +124,10 @@ export default {
|
||||
slug: '',
|
||||
domain: '',
|
||||
alertMessage: '',
|
||||
|
||||
// Logouploader keys
|
||||
avatarBlobId: '',
|
||||
logoUrl: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
@@ -159,6 +169,9 @@ export default {
|
||||
exampleURL: EXAMPLE_URL,
|
||||
});
|
||||
},
|
||||
showDeleteButton() {
|
||||
return hasValidAvatarUrl(this.logoUrl);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const portal = this.portal || {};
|
||||
@@ -166,6 +179,13 @@ export default {
|
||||
this.slug = portal.slug || '';
|
||||
this.domain = portal.custom_domain || '';
|
||||
this.alertMessage = '';
|
||||
if (portal.logo) {
|
||||
const {
|
||||
logo: { file_url: logoURL, blob_id: blobId },
|
||||
} = portal;
|
||||
this.logoUrl = logoURL;
|
||||
this.avatarBlobId = blobId;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onNameChange() {
|
||||
@@ -181,9 +201,44 @@ export default {
|
||||
name: this.name,
|
||||
slug: this.slug,
|
||||
custom_domain: this.domain,
|
||||
blob_id: this.avatarBlobId || null,
|
||||
};
|
||||
this.$emit('submit', portal);
|
||||
},
|
||||
async deleteAvatar() {
|
||||
this.logoUrl = '';
|
||||
this.avatarBlobId = '';
|
||||
this.$emit('delete-logo');
|
||||
},
|
||||
onFileChange({ file }) {
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
this.uploadLogoToStorage(file);
|
||||
} else {
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
|
||||
{
|
||||
size: MAXIMUM_FILE_UPLOAD_SIZE,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.$refs.imageUpload.value = '';
|
||||
},
|
||||
async uploadLogoToStorage(file) {
|
||||
try {
|
||||
const { fileUrl, blobId } = await uploadFile(file);
|
||||
if (fileUrl) {
|
||||
this.logoUrl = fileUrl;
|
||||
this.avatarBlobId = blobId;
|
||||
}
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+14
@@ -7,6 +7,7 @@
|
||||
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
||||
"
|
||||
@submit="updatePortalSettings"
|
||||
@delete-logo="deleteLogo"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -70,6 +71,19 @@ export default {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
async deleteLogo() {
|
||||
try {
|
||||
const portalSlug = this.lastPortalSlug;
|
||||
await this.$store.dispatch('portals/deleteLogo', {
|
||||
portalSlug,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR');
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -40,9 +40,12 @@ export default {
|
||||
methods: {
|
||||
async createPortal(portal) {
|
||||
try {
|
||||
const { blob_id: blobId } = portal;
|
||||
await this.$store.dispatch('portals/create', {
|
||||
portal,
|
||||
blob_id: blobId,
|
||||
});
|
||||
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- eslint-disable vue/v-slot-style -->
|
||||
<template>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div class="flex flex-col h-auto overflow-auto integration-hooks">
|
||||
<woot-modal-header
|
||||
:header-title="integration.name"
|
||||
:header-content="integration.description"
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
||||
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
|
||||
import NotificationSettings from './NotificationSettings.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
@@ -198,6 +199,9 @@ export default {
|
||||
currentUserId: 'getCurrentUserID',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
showDeleteButton() {
|
||||
return hasValidAvatarUrl(this.avatarUrl);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUserId(newCurrentUserId, prevCurrentUserId) {
|
||||
@@ -265,9 +269,6 @@ export default {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
|
||||
}
|
||||
},
|
||||
showDeleteButton() {
|
||||
return this.avatarUrl && !this.avatarUrl.includes('www.gravatar.com');
|
||||
},
|
||||
toggleEditorMessageKey(key) {
|
||||
this.updateUISettings({ editor_message_key: key });
|
||||
this.showAlert(
|
||||
|
||||
@@ -2,6 +2,7 @@ import PortalAPI from 'dashboard/api/helpCenter/portals';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
import { types } from './mutations';
|
||||
const portalAPIs = new PortalAPI();
|
||||
|
||||
export const actions = {
|
||||
index: async ({ commit }) => {
|
||||
try {
|
||||
@@ -89,6 +90,23 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteLogo: async ({ commit }, { portalSlug }) => {
|
||||
commit(types.SET_HELP_PORTAL_UI_FLAG, {
|
||||
uiFlags: { isUpdating: true },
|
||||
portalSlug,
|
||||
});
|
||||
try {
|
||||
await portalAPIs.deleteLogo(portalSlug);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_HELP_PORTAL_UI_FLAG, {
|
||||
uiFlags: { isUpdating: false },
|
||||
portalSlug,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updatePortal: async ({ commit }, portal) => {
|
||||
commit(types.UPDATE_PORTAL_ENTRY, portal);
|
||||
},
|
||||
|
||||
@@ -2,27 +2,21 @@
|
||||
<div class="hidden lg:block flex-1 py-6 scroll-mt-24 pl-4">
|
||||
<div v-if="rows.length > 0" class="sticky top-24 py-2 overflow-auto">
|
||||
<nav class="max-w-2xl">
|
||||
<h2
|
||||
id="on-this-page-title"
|
||||
class="text-slate-800 pl-6 dark:text-slate-50 font-semibold tracking-wide py-3 leading-7 border-l-2 border-solid border-slate-100 dark:border-slate-800"
|
||||
:class="{
|
||||
'!border-slate-400 dark:!border-slate-100': !currentSlug,
|
||||
}"
|
||||
<ol
|
||||
role="list"
|
||||
class="flex flex-col gap-2 text-base border-l-2 border-solid border-slate-100 dark:border-slate-800"
|
||||
>
|
||||
{{ tocHeader }}
|
||||
</h2>
|
||||
<ol role="list" class="text-base">
|
||||
<li
|
||||
v-for="element in rows"
|
||||
:key="element.slug"
|
||||
class="leading-6 py-2 pl-6 border-l-2 border-solid"
|
||||
class="leading-6 border-l-2 relative -left-0.5 border-solid"
|
||||
:class="elementBorderStyles(element)"
|
||||
>
|
||||
<p :class="getClassName(element)">
|
||||
<p class="py-2 px-4" :class="getClassName(element)">
|
||||
<a
|
||||
:href="`#${element.slug}`"
|
||||
data-turbolinks="false"
|
||||
class="text-base cursor-pointer"
|
||||
class="font-medium text-sm tracking-[0.28px] cursor-pointer"
|
||||
:class="elementTextStyles(element)"
|
||||
>
|
||||
{{ element.title }}
|
||||
@@ -45,12 +39,10 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
currentSlug: window.location?.hash?.substring(1) || '',
|
||||
intersectionObserver: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tocHeader() {
|
||||
return window.portalConfig.tocHeader;
|
||||
},
|
||||
h1Count() {
|
||||
return this.rows.filter(el => el.tag === 'h1').length;
|
||||
},
|
||||
@@ -59,10 +51,14 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initializeIntersectionObserver();
|
||||
window.addEventListener('hashchange', this.onURLHashChange);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('hashchange', this.onURLHashChange);
|
||||
if (this.intersectionObserver) {
|
||||
this.intersectionObserver.disconnect();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getClassName(el) {
|
||||
@@ -85,18 +81,32 @@ export default {
|
||||
|
||||
return '';
|
||||
},
|
||||
initializeIntersectionObserver() {
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
const currentSection = entries.find(entry => entry.isIntersecting);
|
||||
if (currentSection) {
|
||||
this.currentSlug = currentSection.target.id;
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: 0.25,
|
||||
rootMargin: '0px 0px -20% 0px',
|
||||
}
|
||||
);
|
||||
|
||||
this.rows.forEach(el => {
|
||||
const sectionElement = document.getElementById(el.slug);
|
||||
if (!sectionElement) return;
|
||||
this.intersectionObserver.observe(sectionElement);
|
||||
});
|
||||
},
|
||||
onURLHashChange() {
|
||||
this.currentSlug = window.location?.hash?.substring(1) || '';
|
||||
},
|
||||
isElementActive(el) {
|
||||
return this.currentSlug === el.slug;
|
||||
},
|
||||
tocHeaderBorderStyles() {
|
||||
if (this.currentSlug === '') {
|
||||
return 'border-slate-400 dark:border-slate-100';
|
||||
}
|
||||
return 'border-slate-100 dark:border-slate-800';
|
||||
},
|
||||
elementBorderStyles(el) {
|
||||
if (this.isElementActive(el)) {
|
||||
return 'border-slate-400 dark:border-slate-50 transition-colors duration-200';
|
||||
@@ -105,7 +115,7 @@ export default {
|
||||
},
|
||||
elementTextStyles(el) {
|
||||
if (this.isElementActive(el)) {
|
||||
return 'font-semibold text-slate-900 dark:text-slate-25';
|
||||
return 'text-slate-900 dark:text-slate-25 transition-colors duration-200';
|
||||
}
|
||||
return 'text-slate-700 dark:text-slate-100';
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<resizable-text-area
|
||||
v-else
|
||||
v-model="greetingsMessage"
|
||||
rows="4"
|
||||
:rows="4"
|
||||
type="text"
|
||||
class="greetings--textarea"
|
||||
:label="label"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:placeholder="placeholder"
|
||||
:rows="rows"
|
||||
:value="value"
|
||||
@input="onInput"
|
||||
@focus="onFocus"
|
||||
@@ -36,6 +37,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
// add this as a prop, so that we won't have to include uiSettingsMixin
|
||||
sendWithSignature: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -50,6 +50,7 @@ export const getters = {
|
||||
get: $state => $state,
|
||||
isOnChatwootCloud: $state => $state.deploymentEnv === 'cloud',
|
||||
isACustomBrandedInstance: $state => $state.installationName !== 'Chatwoot',
|
||||
isAChatwootInstance: $state => $state.installationName === 'Chatwoot',
|
||||
};
|
||||
|
||||
export const actions = {};
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
<drag-wrapper
|
||||
class="space-y-2"
|
||||
direction="right"
|
||||
:disabled="!allowReplyTo"
|
||||
@dragged="toggleReply"
|
||||
>
|
||||
<AgentMessageBubble
|
||||
@@ -58,7 +57,6 @@
|
||||
</drag-wrapper>
|
||||
<div class="flex flex-col justify-end">
|
||||
<message-reply-button
|
||||
v-if="allowReplyTo"
|
||||
class="transition-opacity delay-75 opacity-0 group-hover:opacity-100 sm:opacity-0"
|
||||
@click="toggleReply"
|
||||
/>
|
||||
@@ -127,7 +125,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
allowReplyTo: window.chatwootWebChannel.allowReplyTo || false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -217,7 +214,6 @@ export default {
|
||||
};
|
||||
},
|
||||
hasReplyTo() {
|
||||
if (!this.allowReplyTo) return false;
|
||||
return this.replyTo && (this.replyTo.content || this.replyTo.attachments);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -68,7 +68,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
inReplyTo: null,
|
||||
allowReplyTo: window.chatwootWebChannel.allowReplyTo || false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -91,8 +90,6 @@ export default {
|
||||
return this.currentUser && this.currentUser.email;
|
||||
},
|
||||
hasReplyTo() {
|
||||
if (!this.allowReplyTo) return false;
|
||||
|
||||
return (
|
||||
this.inReplyTo && (this.inReplyTo.content || this.inReplyTo.attachments)
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
id="chat-input"
|
||||
ref="chatInput"
|
||||
v-model="userInput"
|
||||
:rows="1"
|
||||
:aria-label="$t('CHAT_PLACEHOLDER')"
|
||||
:placeholder="$t('CHAT_PLACEHOLDER')"
|
||||
class="form-input user-message-input is-focused"
|
||||
|
||||
@@ -11,16 +11,12 @@
|
||||
<div class="flex justify-end gap-1">
|
||||
<div class="flex flex-col justify-end">
|
||||
<message-reply-button
|
||||
v-if="!isInProgress && !isFailed && allowReplyTo"
|
||||
v-if="!isInProgress && !isFailed"
|
||||
class="transition-opacity delay-75 opacity-0 group-hover:opacity-100 sm:opacity-0"
|
||||
@click="toggleReply"
|
||||
/>
|
||||
</div>
|
||||
<drag-wrapper
|
||||
direction="left"
|
||||
:disabled="!allowReplyTo"
|
||||
@dragged="toggleReply"
|
||||
>
|
||||
<drag-wrapper direction="left" @dragged="toggleReply">
|
||||
<user-message-bubble
|
||||
v-if="showTextBubble"
|
||||
:message="message.content"
|
||||
@@ -111,7 +107,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
allowReplyTo: window.chatwootWebChannel.allowReplyTo || false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -142,7 +137,6 @@ export default {
|
||||
: this.$t('COMPONENTS.MESSAGE_BUBBLE.ERROR_MESSAGE');
|
||||
},
|
||||
hasReplyTo() {
|
||||
if (!this.allowReplyTo) return false;
|
||||
return this.replyTo && (this.replyTo.content || this.replyTo.attachments);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# auto_resolve_duration :integer
|
||||
# custom_attributes :jsonb
|
||||
# domain :string(100)
|
||||
# feature_flags :integer default(0), not null
|
||||
# feature_flags :bigint default(0), not null
|
||||
# limits :jsonb
|
||||
# locale :integer default("en")
|
||||
# name :string not null
|
||||
|
||||
@@ -27,6 +27,10 @@ class PortalPolicy < ApplicationPolicy
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def logo?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def portal_member?
|
||||
|
||||
@@ -17,7 +17,10 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
|
||||
def send_message_to_facebook(delivery_params)
|
||||
result = Facebook::Messenger::Bot.deliver(delivery_params, page_id: channel.page_id)
|
||||
parsed_result = JSON.parse(result)
|
||||
message.update!(status: :failed, external_error: external_error(parsed_result)) if parsed_result['error'].present?
|
||||
if parsed_result['error'].present?
|
||||
message.update!(status: :failed, external_error: external_error(parsed_result))
|
||||
Rails.logger.info "Facebook::SendOnFacebookService: Error sending message to Facebook : Page - #{channel.page_id} : #{result}"
|
||||
end
|
||||
message.update!(source_id: parsed_result['message_id']) if parsed_result['message_id'].present?
|
||||
end
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ json.phone_number resource.phone_number
|
||||
json.identifier resource.identifier
|
||||
json.thumbnail resource.avatar_url
|
||||
json.custom_attributes resource.custom_attributes
|
||||
json.conversations_count resource.conversations_count if resource[:conversations_count].present?
|
||||
json.last_activity_at resource.last_activity_at.to_i if resource[:last_activity_at].present?
|
||||
json.created_at resource.created_at.to_i if resource[:created_at].present?
|
||||
# we only want to output contact inbox when its /contacts endpoints
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
json.website_channel_config do
|
||||
json.allow_messages_after_resolved @web_widget.inbox.allow_messages_after_resolved
|
||||
json.allow_reply_to @web_widget.inbox.account.feature_enabled?('message_reply_to')
|
||||
json.api_host ENV.fetch('FRONTEND_URL', nil)
|
||||
json.auth_token @token
|
||||
json.avatar_url @web_widget.inbox.avatar_url
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-2 gap-y-2 gap-2">
|
||||
<% featured_articles.each do |article| %>
|
||||
<a class="text-slate-700 dark:text-slate-100 leading-7" href="<%= generate_article_link(portal.slug, article.slug, @theme) %>">
|
||||
<div class="flex justify-between items-center py-1 px-2 rounded-lg gap-3 hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<div class="flex justify-between items-start py-1 px-2 rounded-lg gap-3 hover:bg-slate-50 dark:hover:bg-slate-800">
|
||||
<%= article.title %>
|
||||
<span class="flex items-center font-normal">
|
||||
<span class="flex items-center font-normal mt-1.5">
|
||||
<%= render partial: 'icons/chevron-right' %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
timezone: '<%= @web_widget.inbox.timezone %>',
|
||||
allowMessagesAfterResolved: <%= @web_widget.inbox.allow_messages_after_resolved %>,
|
||||
disableBranding: <%= @web_widget.inbox.account.feature_enabled?('disable_branding') %>,
|
||||
allowReplyTo: <%= @web_widget.inbox.account.feature_enabled?('message_reply_to') %>
|
||||
}
|
||||
window.chatwootPubsubToken = '<%= @contact_inbox.pubsub_token %>'
|
||||
window.authToken = '<%= @token %>'
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '3.2.0'
|
||||
version: '3.3.1'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -2,5 +2,11 @@
|
||||
|
||||
# Configure sensitive parameters which will be filtered from the log file.
|
||||
Rails.application.config.filter_parameters += [
|
||||
:password, :secret, :token, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn
|
||||
:password, :secret, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn
|
||||
]
|
||||
|
||||
# Regex to filter all occurrences of 'token' in keys except for 'website_token'
|
||||
filter_regex = /\A(?!.*\bwebsite_token\b).*token/i
|
||||
|
||||
# Apply the regex for filtering
|
||||
Rails.application.config.filter_parameters += [filter_regex]
|
||||
|
||||
@@ -139,6 +139,7 @@ openai:
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": { "type": "string" },
|
||||
"label_suggestion": { "type": "boolean" },
|
||||
},
|
||||
"required": ["api_key"],
|
||||
"additionalProperties": false,
|
||||
@@ -150,5 +151,11 @@ openai:
|
||||
"name": "api_key",
|
||||
"validation": "required",
|
||||
},
|
||||
{
|
||||
"label": "Show label suggestions",
|
||||
"type": "checkbox",
|
||||
"name": "label_suggestion",
|
||||
"validation": "",
|
||||
},
|
||||
]
|
||||
visible_properties: ['api_key']
|
||||
visible_properties: ['api_key', 'label_suggestion']
|
||||
|
||||
@@ -217,6 +217,7 @@ Rails.application.routes.draw do
|
||||
member do
|
||||
patch :archive
|
||||
put :add_members
|
||||
delete :logo
|
||||
end
|
||||
resources :categories
|
||||
resources :articles do
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class ChangeFeatureFlagsToBigintInAccounts < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
change_column :accounts, :feature_flags, :bigint
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :accounts, :feature_flags, :integer
|
||||
end
|
||||
end
|
||||
+2
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_10_13_072802) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -49,7 +49,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_13_072802) do
|
||||
t.integer "locale", default: 0
|
||||
t.string "domain", limit: 100
|
||||
t.string "support_email", limit: 100
|
||||
t.integer "feature_flags", default: 0, null: false
|
||||
t.bigint "feature_flags", default: 0, null: false
|
||||
t.integer "auto_resolve_duration"
|
||||
t.jsonb "limits", default: {}
|
||||
t.jsonb "custom_attributes", default: {}
|
||||
|
||||
@@ -21,11 +21,7 @@ module Enterprise::Integrations::OpenaiProcessorService
|
||||
def labels_with_messages
|
||||
conversation = find_conversation
|
||||
|
||||
# return nil if conversation is not present
|
||||
return nil if conversation.nil?
|
||||
|
||||
# return nil if conversation has less than 3 incoming messages
|
||||
return nil if conversation.messages.incoming.count < 3
|
||||
return nil unless valid_conversation?(conversation)
|
||||
|
||||
labels = hook.account.labels.pluck(:title).join(', ')
|
||||
character_count = labels.length
|
||||
@@ -38,6 +34,19 @@ module Enterprise::Integrations::OpenaiProcessorService
|
||||
"Messages:\n#{messages}\nLabels:\n#{labels}"
|
||||
end
|
||||
|
||||
def valid_conversation?(conversation)
|
||||
return false if conversation.nil?
|
||||
return false if conversation.messages.incoming.count < 3
|
||||
|
||||
# Think Mark think, at this point the conversation is beyond saving
|
||||
return false if conversation.messages.count > 100
|
||||
|
||||
# if there are more than 20 messages, only trigger this if the last message is from the client
|
||||
return false if conversation.messages.count > 20 && !conversation.messages.last.incoming?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def summarize_body
|
||||
{
|
||||
model: self.class::GPT_MODEL,
|
||||
@@ -50,8 +59,10 @@ module Enterprise::Integrations::OpenaiProcessorService
|
||||
end
|
||||
|
||||
def label_suggestion_body
|
||||
return unless label_suggestions_enabled?
|
||||
|
||||
content = labels_with_messages
|
||||
return nil if content.blank?
|
||||
return value_from_cache if content.blank?
|
||||
|
||||
{
|
||||
model: self.class::GPT_MODEL,
|
||||
@@ -64,4 +75,8 @@ module Enterprise::Integrations::OpenaiProcessorService
|
||||
]
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def label_suggestions_enabled?
|
||||
hook.settings['label_suggestion'].present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||
|
||||
def message_content
|
||||
private_indicator = message.private? ? 'private: ' : ''
|
||||
sanitized_content = ActionView::Base.full_sanitizer.sanitize(message_text)
|
||||
sanitized_content = ActionView::Base.full_sanitizer.sanitize(format_message_content)
|
||||
|
||||
if conversation.identifier.present?
|
||||
"#{private_indicator}#{sanitized_content}"
|
||||
@@ -53,6 +53,10 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||
end
|
||||
end
|
||||
|
||||
def format_message_content
|
||||
message.message_type == 'activity' ? "_#{message_text}_" : message_text
|
||||
end
|
||||
|
||||
def message_text
|
||||
if message.content.present?
|
||||
message.content.gsub(MENTION_REGEX, '\1')
|
||||
@@ -79,7 +83,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||
end
|
||||
|
||||
def avatar_url(sender)
|
||||
sender_type = sender.instance_of?(Contact) ? 'contact' : 'user'
|
||||
sender_type = sender_type(sender).downcase
|
||||
blob_key = sender&.avatar&.attached? ? sender.avatar.blob.key : nil
|
||||
generate_url(sender_type, blob_key)
|
||||
end
|
||||
@@ -137,7 +141,15 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||
end
|
||||
|
||||
def sender_type(sender)
|
||||
sender.instance_of?(Contact) ? 'Contact' : 'Agent'
|
||||
if sender.instance_of?(Contact)
|
||||
'Contact'
|
||||
elsif message.message_type == 'template' && sender.nil?
|
||||
'Bot'
|
||||
elsif message.message_type == 'activity' && sender.nil?
|
||||
'System'
|
||||
else
|
||||
'Agent'
|
||||
end
|
||||
end
|
||||
|
||||
def update_reference_id
|
||||
|
||||
@@ -74,11 +74,14 @@ class Seeders::AccountSeeder
|
||||
|
||||
def seed_contacts
|
||||
@account_data['contacts'].each do |contact_data|
|
||||
contact = @account.contacts.create!(contact_data.slice('name', 'email'))
|
||||
Avatar::AvatarFromUrlJob.perform_later(contact, "https://xsgames.co/randomusers/avatar.php?g=#{contact_data['gender']}")
|
||||
contact = @account.contacts.find_or_initialize_by(email: contact_data['email'])
|
||||
if contact.new_record?
|
||||
contact.update!(contact_data.slice('name', 'email'))
|
||||
Avatar::AvatarFromUrlJob.perform_later(contact, "https://xsgames.co/randomusers/avatar.php?g=#{contact_data['gender']}")
|
||||
end
|
||||
contact_data['conversations'].each do |conversation_data|
|
||||
inbox = @account.inboxes.find_by(channel_type: conversation_data['channel'])
|
||||
contact_inbox = inbox.contact_inboxes.create!(contact: contact, source_id: (conversation_data['source_id'] || SecureRandom.hex))
|
||||
contact_inbox = inbox.contact_inboxes.create_or_find_by!(contact: contact, source_id: (conversation_data['source_id'] || SecureRandom.hex))
|
||||
create_conversation(contact_inbox: contact_inbox, conversation_data: conversation_data)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,10 +12,11 @@ class Webhooks::Trigger
|
||||
end
|
||||
|
||||
def execute
|
||||
perform_request
|
||||
response = perform_request
|
||||
rescue StandardError => e
|
||||
handle_error(e)
|
||||
Rails.logger.warn "Exception: Invalid webhook URL #{@url} : #{e.message}"
|
||||
Rails.logger.warn "Webhooks: Exception: Invalid webhook URL #{@url} : #{e.message}"
|
||||
Rails.logger.warn "Webhooks: Response body: #{response.body}" if response.respond_to?(:body)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
@@ -210,4 +210,37 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}/logo' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/logo"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
before do
|
||||
portal.logo.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||
end
|
||||
|
||||
it 'throw error if agent' do
|
||||
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/logo",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'delete portal logo if admin' do
|
||||
delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/logo",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect { portal.logo.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -183,11 +183,11 @@ describe Integrations::Slack::SendOnSlackService do
|
||||
it 'sent a template message on slack' do
|
||||
builder = described_class.new(message: template_message, hook: hook)
|
||||
allow(builder).to receive(:slack_client).and_return(slack_client)
|
||||
|
||||
template_message.update!(sender: nil)
|
||||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: template_message.content,
|
||||
username: "#{template_message.sender.name} (Contact)",
|
||||
username: 'Bot',
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: anything,
|
||||
unfurl_links: true
|
||||
@@ -198,6 +198,24 @@ describe Integrations::Slack::SendOnSlackService do
|
||||
expect(template_message.external_source_id_slack).to eq 'cw-origin-6789.12345'
|
||||
end
|
||||
|
||||
it 'sent a activity message on slack' do
|
||||
template_message.update!(message_type: :activity)
|
||||
template_message.update!(sender: nil)
|
||||
builder = described_class.new(message: template_message, hook: hook)
|
||||
allow(builder).to receive(:slack_client).and_return(slack_client)
|
||||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: "_#{template_message.content}_",
|
||||
username: 'System',
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: anything,
|
||||
unfurl_links: true
|
||||
).and_return(slack_message)
|
||||
|
||||
builder.perform
|
||||
expect(template_message.external_source_id_slack).to eq 'cw-origin-6789.12345'
|
||||
end
|
||||
|
||||
it 'disables hook on Slack AccountInactive error' do
|
||||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
|
||||
Reference in New Issue
Block a user