Merge branch 'develop' of https://github.com/chatwoot/chatwoot into chore/chat-list-design

This commit is contained in:
Nithin David 2021-12-30 12:11:30 +05:30
commit eb9d9ace96
83 changed files with 1807 additions and 314 deletions

View file

@ -1,4 +1,4 @@
version: "2"
version: '2'
plugins:
rubocop:
enabled: false
@ -17,30 +17,30 @@ checks:
method-count:
enabled: true
config:
threshold: 30
threshold: 32
file-lines:
enabled: true
config:
threshold: 300
exclude_patterns:
- "spec/"
- "**/specs/"
- "db/*"
- "bin/**/*"
- "db/**/*"
- "config/**/*"
- "public/**/*"
- "vendor/**/*"
- "node_modules/**/*"
- "lib/tasks/auto_annotate_models.rake"
- "app/test-matchers.js"
- "docs/*"
- "**/*.md"
- "**/*.yml"
- "app/javascript/dashboard/i18n/locale"
- "**/*.stories.js"
- "stories/"
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js"
- "app/javascript/shared/constants/countries.js"
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js"
- "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js"
- 'spec/'
- '**/specs/'
- 'db/*'
- 'bin/**/*'
- 'db/**/*'
- 'config/**/*'
- 'public/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'lib/tasks/auto_annotate_models.rake'
- 'app/test-matchers.js'
- 'docs/*'
- '**/*.md'
- '**/*.yml'
- 'app/javascript/dashboard/i18n/locale'
- '**/*.stories.js'
- 'stories/'
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js'
- 'app/javascript/shared/constants/countries.js'
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'

View file

@ -28,9 +28,7 @@ module.exports = {
}],
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
'vue/singleline-html-element-content-newline': 'warn',
'vue/require-default-prop': 'warn',
'vue/require-prop-types': 'warn',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off']
},

View file

@ -121,6 +121,10 @@ gem 'hairtrigger'
gem 'procore-sift'
# parse email
gem 'email_reply_trimmer'
gem 'html2text'
group :production, :staging do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'

View file

@ -179,6 +179,7 @@ GEM
addressable (~> 2.8)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
email_reply_trimmer (0.1.13)
erubi (1.10.0)
erubis (2.7.0)
et-orbi (1.2.5)
@ -290,6 +291,8 @@ GEM
hashdiff (1.0.1)
hashie (4.1.0)
hkdf (0.3.0)
html2text (0.2.1)
nokogiri (~> 1.6)
http-accept (1.7.0)
http-cookie (1.0.4)
domain_name (~> 0.5)
@ -668,6 +671,7 @@ DEPENDENCIES
devise_token_auth
dotenv-rails
down (~> 5.0)
email_reply_trimmer
facebook-messenger
factory_bot_rails
faker
@ -682,6 +686,7 @@ DEPENDENCIES
haikunator
hairtrigger
hashie
html2text
image_processing
jbuilder
json_refs

View file

@ -1,11 +1,11 @@
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
before_action :fetch_inbox
before_action :current_agents_ids, only: [:update]
before_action :current_agents_ids, only: [:create, :update]
def create
authorize @inbox, :create?
ActiveRecord::Base.transaction do
params[:user_ids].map { |user_id| @inbox.add_member(user_id) }
agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
end
fetch_updated_agents
end

View file

@ -124,6 +124,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def get_channel_attributes(channel_type)
channel_type.constantize::EDITABLE_ATTRS.presence || []
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
channel_type.constantize::EDITABLE_ATTRS.presence
else
[]
end
end
end

View file

@ -8,7 +8,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
def create
ActiveRecord::Base.transaction do
@team_members = params[:user_ids].map { |user_id| @team.add_member(user_id) }
@team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) }
end
end

View file

@ -15,6 +15,7 @@ class AsyncDispatcher < BaseDispatcher
EventListener.instance,
HookListener.instance,
InstallationWebhookListener.instance,
NotificationListener.instance,
WebhookListener.instance
]
end

View file

@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
end
def listeners
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
[ActionCableListener.instance, AgentBotListener.instance]
end
end

View file

@ -1,16 +1,29 @@
module FileTypeHelper
# NOTE: video, audio, image, etc are filetypes previewable in frontend
def file_type(content_type)
return :image if [
'image/jpeg',
'image/png',
'image/gif',
'image/tiff',
'image/bmp'
].include?(content_type)
return :video if content_type.include?('video/')
return :image if image_file?(content_type)
return :video if video_file?(content_type)
return :audio if content_type.include?('audio/')
:file
end
def image_file?(content_type)
[
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
'image/webp'
].include?(content_type)
end
def video_file?(content_type)
[
'video/ogg',
'video/mp4',
'video/webm',
'video/quicktime'
].include?(content_type)
end
end

View file

@ -45,6 +45,9 @@
// 1. Global
// ---------
// Disable contrast warnings in Foundation.
$contrast-warnings: false;
$global-font-size: 10px;
$global-width: 100%;
$global-lineheight: 1.5;

View file

@ -11,7 +11,7 @@
<script>
export default {
props: {
message: String,
message: { type: String, default: '' },
showButton: Boolean,
duration: {
type: [String, Number],

View file

@ -52,12 +52,16 @@
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider v-if="isOpen" />
<woot-dropdown-sub-menu
v-if="isOpen"
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
@ -65,6 +69,9 @@
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="send-clock"
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
>
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
@ -73,6 +80,9 @@
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="dual-screen-clock"
@click="
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
"
@ -83,6 +93,9 @@
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="calendar-clock"
@click="
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
"
@ -110,6 +123,8 @@ import {
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import wootConstants from '../../constants';
import {
getUnixTime,
@ -129,6 +144,7 @@ export default {
WootDropdownItem,
WootDropdownMenu,
WootDropdownSubMenu,
WootDropdownDivider,
},
mixins: [clickaway, alertMixin, eventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
@ -269,5 +285,6 @@ export default {
margin-top: var(--space-micro);
right: 0;
max-width: 20rem;
min-width: 15.6rem;
}
</style>

View file

@ -54,7 +54,7 @@
:class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)"
>
<fluent-icon icon="person" class="icon icon--font" />
<fluent-icon icon="person" size="14" class="icon icon--font" />
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
</span>

View file

@ -18,12 +18,11 @@
export default {
props: {
disabled: Boolean,
isFullwidth: Boolean,
type: String,
size: String,
type: { type: String, default: '' },
size: { type: String, default: '' },
checked: Boolean,
name: String,
id: String,
name: { type: String, default: '' },
id: { type: String, default: '' },
},
data() {
return {

View file

@ -21,8 +21,6 @@
</li>
</template>
<script>
import TWEEN from 'tween.js';
export default {
name: 'WootTabsItem',
props: {
@ -57,19 +55,13 @@ export default {
},
},
data() {
return {
animatedNumber: 0,
};
},
computed: {
active() {
return this.index === this.$parent.index;
},
getItemCount() {
return this.animatedNumber || this.count;
return this.count;
},
colorScheme() {
if (this.variant === 'smooth') return 'secondary';
@ -84,27 +76,6 @@ export default {
},
},
watch: {
count(newValue, oldValue) {
let animationFrame;
const animate = time => {
TWEEN.update(time);
animationFrame = window.requestAnimationFrame(animate);
};
const tweeningNumber = { value: oldValue };
new TWEEN.Tween(tweeningNumber)
.easing(TWEEN.Easing.Quadratic.Out)
.to({ value: newValue }, 500)
.onUpdate(() => {
this.animatedNumber = tweeningNumber.value.toFixed(0);
})
.onComplete(() => {
window.cancelAnimationFrame(animationFrame);
})
.start();
animationFrame = window.requestAnimationFrame(animate);
},
},
methods: {
onTabClick(event) {
event.preventDefault();

View file

@ -91,7 +91,7 @@ export default {
this.isRounded ? '' : 'not-rounded',
];
},
withTextIconSize() {
iconSize() {
switch (this.size) {
case 'tiny':
return 12;
@ -106,26 +106,6 @@ export default {
return 16;
}
},
withoutTextIconSize() {
switch (this.size) {
case 'tiny':
return 14;
case 'small':
return 16;
case 'medium':
return 18;
case 'large':
return 20;
default:
return 18;
}
},
iconSize() {
return this.hasOnlyIcon
? this.withoutTextIconSize
: this.withTextIconSize;
},
},
methods: {
handleClick(evt) {

View file

@ -12,9 +12,9 @@
<script>
export default {
props: {
title: String,
message: String,
buttonText: String,
title: { type: String, default: '' },
message: { type: String, default: '' },
buttonText: { type: String, default: '' },
},
};
</script>

View file

@ -1,12 +1,12 @@
<template>
<div class="row loading-state">
<h6 class="message">{{ message }}<span class="spinner"></span></h6>
<h6 class="message">{{ message }}<span class="spinner" /></h6>
</div>
</template>
<script>
export default {
props: {
message: String,
message: { type: String, default: '' },
},
};
</script>

View file

@ -18,12 +18,12 @@
<script>
export default {
props: {
heading: String,
point: [Number, String],
index: Number,
desc: String,
heading: { type: String, default: '' },
point: { type: [Number, String], default: '' },
index: { type: Number, default: null },
desc: { type: String, default: '' },
selected: Boolean,
onClick: Function,
onClick: { type: Function, default: () => {} },
},
};
</script>

View file

@ -15,7 +15,7 @@
<file-upload
ref="upload"
:size="4096 * 4096"
accept="image/png, image/jpeg, image/gif, image/bmp, image/tiff, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
:accept="allowedFileTypes"
:drop="true"
:drop-directory="false"
@input-file="onFileUpload"
@ -84,6 +84,7 @@ import {
hasPressedAltAndAKey,
} from 'shared/helpers/KeyboardHelpers';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
import { REPLY_EDITOR_MODES } from './constants';
export default {
@ -161,6 +162,9 @@ export default {
showAttachButton() {
return this.showFileUpload || this.isNote;
},
allowedFileTypes() {
return ALLOWED_FILE_TYPES;
},
},
methods: {
handleKeyEvents(e) {

View file

@ -1,7 +1,7 @@
<template>
<li v-if="hasAttachments || data.content" :class="alignBubble">
<div :class="wrapClass">
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head
:email-attributes="contentAttributes.email"
:cc="emailHeadAttributes.cc"
@ -24,9 +24,10 @@
<div v-if="!isPending && hasAttachments">
<div v-for="attachment in data.attachments" :key="attachment.id">
<bubble-image
v-if="attachment.file_type === 'image'"
v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url"
:readable-time="readableTime"
@error="onImageLoadError"
/>
<audio v-else-if="attachment.file_type === 'audio'" controls>
<source :src="attachment.data_url" />
@ -72,8 +73,18 @@
{{ sender.name }}
</div>
</a>
<div v-if="isFailed" class="message-failed--alert">
<woot-button
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
size="tiny"
color-scheme="alert"
variant="clear"
icon="arrow-clockwise"
@click="retrySendMessage"
/>
</div>
</div>
<div class="context-menu-wrap">
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
<context-menu
v-if="isBubble && !isMessageDeleted"
:is-open="showContextMenu"
@ -133,6 +144,7 @@ export default {
data() {
return {
showContextMenu: false,
hasImageError: false,
};
},
computed: {
@ -246,10 +258,13 @@ export default {
hasText() {
return !!this.data.content;
},
sentByMessage() {
messageToolTip() {
if (this.isMessageDeleted) {
return false;
}
if (this.isFailed) {
return this.$t(`CONVERSATION.SEND_FAILED`);
}
const { sender } = this;
return this.data.message_type === 1 && !isEmptyObject(sender)
? {
@ -263,6 +278,7 @@ export default {
wrap: this.isBubble,
'activity-wrap': !this.isBubble,
'is-pending': this.isPending,
'is-failed': this.isFailed,
};
},
bubbleClass() {
@ -273,26 +289,45 @@ export default {
'is-video': this.hasMediaAttachment('video'),
'is-text': this.hasText,
'is-from-bot': this.isSentByBot,
'is-failed': this.isFailed,
};
},
isPending() {
return this.data.status === MESSAGE_STATUS.PROGRESS;
},
isFailed() {
return this.data.status === MESSAGE_STATUS.FAILED;
},
isSentByBot() {
if (this.isPending) return false;
if (this.isPending || this.isFailed) return false;
return !this.sender.type || this.sender.type === 'agent_bot';
},
contextMenuPosition() {
const { message_type: messageType } = this.data;
return messageType ? 'right' : 'left';
},
shouldShowContextMenu() {
return !(this.isFailed || this.isPending);
},
errorMessage() {
const { meta } = this.data;
return meta ? meta.error : '';
},
},
watch: {
data() {
this.hasImageError = false;
},
},
mounted() {
this.hasImageError = false;
},
methods: {
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0];
return fileType === type;
return fileType === type && !this.hasImageError;
}
return false;
},
@ -317,6 +352,12 @@ export default {
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
this.showContextMenu = false;
},
async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data);
},
onImageLoadError() {
this.hasImageError = true;
},
},
};
</script>
@ -383,6 +424,14 @@ export default {
color: var(--v-50);
}
}
&.is-failed {
background: var(--r-200);
.message-text--metadata .time {
color: var(--r-50);
}
}
}
&.is-pending {
@ -413,6 +462,13 @@ export default {
}
}
.message-failed--alert {
color: var(--r-900);
flex-grow: 1;
text-align: right;
margin-top: var(--space-smaller) var(--space-smaller) 0 0;
}
.button--delete-message {
visibility: hidden;
}
@ -438,6 +494,17 @@ li.right .context-menu-wrap {
li.right {
flex-direction: row-reverse;
justify-content: flex-end;
.wrap.is-pending {
margin-left: auto;
}
.wrap.is-failed {
display: flex;
flex-direction: row-reverse;
align-items: flex-end;
margin-left: auto;
}
}
.has-context-menu {

View file

@ -369,7 +369,10 @@ export default {
const messagePayload = this.getMessagePayload(newMessage);
this.clearMessage();
try {
await this.$store.dispatch('sendMessage', messagePayload);
await this.$store.dispatch(
'createPendingMessageAndSend',
messagePayload
);
this.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
} catch (error) {
const errorMessage =

View file

@ -1,6 +1,6 @@
<template>
<div class="image message-text__wrap">
<img :src="url" @click="onClick" />
<img :src="url" @click="onClick" @error="onImgError()" />
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
<img :src="url" class="modal-image" />
</woot-modal>
@ -9,6 +9,7 @@
<script>
export default {
components: {},
props: {
url: {
type: String,
@ -27,6 +28,9 @@ export default {
onClick() {
this.show = true;
},
onImgError() {
this.$emit('error');
},
},
};
</script>

View file

@ -21,12 +21,12 @@ export default {
},
props: {
show: Boolean,
onClose: Function,
onConfirm: Function,
title: String,
message: String,
confirmText: String,
rejectText: String,
onClose: { type: Function, default: () => {} },
onConfirm: { type: Function, default: () => {} },
title: { type: String, default: '' },
message: { type: String, default: '' },
confirmText: { type: String, default: '' },
rejectText: { type: String, default: '' },
},
};
</script>

View file

@ -90,6 +90,8 @@
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Sent by:",
"SEND_FAILED": "Couldn't send message! Try again",
"TRY_AGAIN": "retry",
"ASSIGNMENT": {
"SELECT_AGENT": "Select Agent",
"REMOVE": "Remove",

View file

@ -57,9 +57,9 @@ export default {
WootSubmitButton,
},
props: {
resetPasswordToken: String,
redirectUrl: String,
config: String,
resetPasswordToken: { type: String, default: '' },
redirectUrl: { type: String, default: '' },
config: { type: String, default: '' },
},
data() {
return {

View file

@ -10,8 +10,8 @@
<script>
export default {
props: {
headerTitle: String,
headerContent: String,
headerTitle: { type: String, default: '' },
headerContent: { type: String, default: '' },
},
};
</script>

View file

@ -25,9 +25,9 @@ export default {
SettingsHeader,
},
props: {
headerTitle: String,
headerButtonText: String,
icon: String,
headerTitle: { type: String, default: '' },
headerButtonText: { type: String, default: '' },
icon: { type: String, default: '' },
keepAlive: {
type: Boolean,
default: true,

View file

@ -61,10 +61,10 @@ export default {
Modal,
},
props: {
id: Number,
edcontent: String,
edshortCode: String,
onClose: Function,
id: { type: Number, default: null },
edcontent: { type: String, default: '' },
edshortCode: { type: String, default: '' },
onClose: { type: Function, default: () => {} },
},
data() {
return {

View file

@ -32,7 +32,11 @@
:label="inboxNameLabel"
:placeholder="inboxNamePlaceHolder"
/>
<label for="toggle-business-hours" class="toggle-input-wrap" v-if="isATwitterInbox">
<label
v-if="isATwitterInbox"
for="toggle-business-hours"
class="toggle-input-wrap"
>
<input
v-model="tweetsEnabled"
type="checkbox"

View file

@ -47,8 +47,8 @@ export default {
mixins: [globalConfigMixin],
props: {
integrationId: {
type: String,
default: '',
type: [String, Number],
required: true,
},
integrationLogo: {
type: String,

View file

@ -68,14 +68,17 @@ import globalConfigMixin from 'shared/mixins/globalConfigMixin';
export default {
mixins: [alertMixin, globalConfigMixin],
props: [
'integrationId',
'integrationLogo',
'integrationName',
'integrationDescription',
'integrationEnabled',
'integrationAction',
],
props: {
integrationId: {
type: [String, Number],
required: true,
},
integrationLogo: { type: String, default: '' },
integrationName: { type: String, default: '' },
integrationDescription: { type: String, default: '' },
integrationEnabled: { type: Boolean, default: false },
integrationAction: { type: String, default: '' },
},
data() {
return {
showDeleteConfirmationPopup: false,

View file

@ -33,7 +33,14 @@ export default {
IntegrationHelpText,
},
mixins: [globalConfigMixin],
props: ['integrationId', 'code'],
props: {
integrationId: {
type: [String, Number],
required: true,
},
code: { type: String, default: '' },
},
data() {
return {
integrationLoaded: false,

View file

@ -158,17 +158,33 @@ const actions = {
}
},
sendMessage: async ({ commit }, data) => {
// eslint-disable-next-line no-useless-catch
createPendingMessageAndSend: async ({ dispatch }, data) => {
const pendingMessage = createPendingMessage(data);
dispatch('sendMessageWithData', pendingMessage);
},
sendMessageWithData: async ({ commit }, pendingMessage) => {
try {
const pendingMessage = createPendingMessage(data);
commit(types.ADD_MESSAGE, pendingMessage);
commit(types.ADD_MESSAGE, {
...pendingMessage,
status: MESSAGE_STATUS.PROGRESS,
});
const response = await MessageApi.create(pendingMessage);
commit(types.ADD_MESSAGE, {
...response.data,
status: MESSAGE_STATUS.SENT,
});
} catch (error) {
const errorMessage = error.response
? error.response.data.error
: undefined;
commit(types.ADD_MESSAGE, {
...pendingMessage,
meta: {
error: errorMessage,
},
status: MESSAGE_STATUS.FAILED,
});
throw error;
}
},

View file

@ -1,19 +1,15 @@
<template>
<svg
:width="size"
:height="size"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path :d="icons[`${icon}-${type}`]" fill="currentColor" />
</svg>
<base-icon :size="size" :icon="icon" :type="type" :icons="icons" />
</template>
<script>
import BaseIcon from './Icon';
import icons from './dashboard-icons.json';
export default {
name: 'FluentIcon',
components: {
BaseIcon,
},
props: {
icon: {
type: String,

View file

@ -0,0 +1,49 @@
<template>
<svg
:width="size"
:height="size"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
v-for="source in pathSource"
:key="source"
:d="source"
fill="currentColor"
/>
</svg>
</template>
<script>
export default {
props: {
icon: {
type: String,
required: true,
},
icons: {
type: Object,
required: true,
},
size: {
type: [String, Number],
default: '20',
},
type: {
type: String,
default: 'outline',
},
},
computed: {
pathSource() {
// To support icons with multiple paths
const path = this.icons[`${this.icon}-${this.type}`];
if (path.constructor === Array) {
return path;
}
return [path];
},
},
};
</script>

View file

@ -1,27 +1,23 @@
<template>
<svg
:width="size"
:height="size"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path :d="icons[`${icon}-${type}`]" fill="currentColor" />
</svg>
<base-icon :size="size" :icon="icon" :type="type" :icons="icons" />
</template>
<script>
import BaseIcon from './Icon';
import icons from './icons.json';
export default {
name: 'FluentIcon',
components: {
BaseIcon,
},
props: {
icon: {
type: String,
required: true,
},
size: {
type: String,
default: '20px',
type: [String, Number],
default: '20',
},
type: {
type: String,

View file

@ -4,6 +4,7 @@
"alert-outline": "M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.25 1.25 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5ZM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.006-.147ZM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.004-.225A5.988 5.988 0 0 0 12 3.496Z",
"arrow-chevron-left-outline": "M15 17.898c0 1.074-1.265 1.648-2.073.941l-6.31-5.522a1.75 1.75 0 0 1 0-2.634l6.31-5.522c.808-.707 2.073-.133 2.073.941v11.796Z",
"arrow-chevron-right-outline": "M9 17.898c0 1.074 1.265 1.648 2.073.941l6.31-5.522a1.75 1.75 0 0 0 0-2.634l-6.31-5.522C10.265 4.454 9 5.028 9 6.102v11.796Z",
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
"arrow-download-outline": "M18.25 20.5a.75.75 0 1 1 0 1.5l-13 .004a.75.75 0 1 1 0-1.5l13-.004ZM11.648 2.012l.102-.007a.75.75 0 0 1 .743.648l.007.102-.001 13.685 3.722-3.72a.75.75 0 0 1 .976-.073l.085.073a.75.75 0 0 1 .072.976l-.073.084-4.997 4.997a.75.75 0 0 1-.976.073l-.085-.073-5.003-4.996a.75.75 0 0 1 .976-1.134l.084.072 3.719 3.714L11 2.755a.75.75 0 0 1 .648-.743l.102-.007-.102.007Z",
"arrow-redo-outline": "M19.25 2a.75.75 0 0 0-.743.648l-.007.102v5.69l-4.574-4.56a6.41 6.41 0 0 0-8.878-.179l-.186.18a6.41 6.41 0 0 0 0 9.063l8.845 8.84a.75.75 0 0 0 1.06-1.062l-8.845-8.838a4.91 4.91 0 0 1 6.766-7.112l.178.17L17.438 9.5H11.75a.75.75 0 0 0-.743.648L11 10.25c0 .38.282.694.648.743l.102.007h7.5a.75.75 0 0 0 .743-.648L20 10.25v-7.5a.75.75 0 0 0-.75-.75Z",
"arrow-reply-outline": "M9.277 16.221a.75.75 0 0 1-1.061 1.06l-4.997-5.003a.75.75 0 0 1 0-1.06L8.217 6.22a.75.75 0 0 1 1.061 1.06L5.557 11h7.842c1.595 0 2.81.242 3.889.764l.246.126a6.203 6.203 0 0 1 2.576 2.576c.61 1.14.89 2.418.89 4.135a.75.75 0 0 1-1.5 0c0-1.484-.228-2.52-.713-3.428a4.702 4.702 0 0 0-1.96-1.96c-.838-.448-1.786-.676-3.094-.709L13.4 12.5H5.562l3.715 3.721Z",
@ -14,7 +15,9 @@
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
"autocorrect-outline": "M13.461 4.934c.293.184.548.42.752.698l.117.171 2.945 4.696H21.5a.75.75 0 0 1 .743.649l.007.102a.75.75 0 0 1-.75.75l-3.284-.001.006.009-.009-.01a4.75 4.75 0 1 1-3.463-1.5h.756L13.059 6.6a1.25 1.25 0 0 0-2.04-.112l-.078.112-7.556 12.048a.75.75 0 0 1-1.322-.699l.052-.098L9.67 5.803a2.75 2.75 0 0 1 3.791-.869ZM14.751 12a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Z",
"book-contacts-outline": "M15.5 12.25a.75.75 0 0 0-.75-.75h-5a.75.75 0 0 0-.75.75v.5c0 1 1.383 1.75 3.25 1.75s3.25-.75 3.25-1.75v-.5ZM14 8.745C14 7.78 13.217 7 12.25 7s-1.75.779-1.75 1.745a1.75 1.75 0 1 0 3.5 0ZM4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15Zm1.5 0V18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1Z",
"book-clock-outline": ["M13 9.125v1.625h.75a.625.625 0 1 1 0 1.25H12.5a.615.615 0 0 1-.063-.003.625.625 0 0 1-.688-.622v-2.25a.625.625 0 1 1 1.251 0Z", "M12.375 6.005a4.75 4.75 0 1 0 0 9.5 4.75 4.75 0 0 0 0-9.5Zm-3.5 4.75a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z", "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z"],
"building-bank-outline": "M13.032 2.325a1.75 1.75 0 0 0-2.064 0L3.547 7.74c-.978.713-.473 2.26.736 2.26H4.5v5.8A2.75 2.75 0 0 0 3 18.25v1.5c0 .413.336.75.75.75h16.5a.75.75 0 0 0 .75-.75v-1.5a2.75 2.75 0 0 0-1.5-2.45V10h.217c1.21 0 1.713-1.547.736-2.26l-7.421-5.416Zm-1.18 1.211a.25.25 0 0 1 .295 0L18.95 8.5H5.05l6.803-4.964ZM18 10v5.5h-2V10h2Zm-3.5 0v5.5h-1.75V10h1.75Zm-3.25 0v5.5H9.5V10h1.75Zm-5.5 7h12.5c.69 0 1.25.56 1.25 1.25V19h-15v-.75c0-.69.56-1.25 1.25-1.25ZM6 15.5V10h2v5.5H6Z",
"calendar-clock-outline": ["M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"],
"call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z",
"chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z",
"chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z",
@ -35,6 +38,7 @@
"dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z",
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
"document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z",
"dual-screen-clock-outline": "M10.019 6.002a6.632 6.632 0 0 0 .058 1.5H3.75a.25.25 0 0 0-.25.25v12.494c0 .138.112.25.25.25h7.498l-.001-10.167c.416.57.924 1.07 1.5 1.479v8.69h7.498a.25.25 0 0 0 .25-.25v-8.62a6.535 6.535 0 0 0 1.501-1.656V20.25a1.75 1.75 0 0 1-1.75 1.75h-8.998l-.001-.003H3.75A1.75 1.75 0 0 1 2 20.246V7.751c0-.966.784-1.75 1.75-1.75h6.269Zm6.22 11.498a.75.75 0 0 1 .101 1.493L16.24 19h-1.5a.75.75 0 0 1-.102-1.493l.102-.007h1.5Zm-6.996 0a.75.75 0 0 1 .102 1.493L9.243 19H7.74a.75.75 0 0 1-.102-1.493l.102-.007h1.502ZM16.498 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm-1 2a.5.5 0 0 0-.5.5v4a.5.5 0 0 0 .5.5h3.001a.5.5 0 0 0 0-1h-2.501V3.5a.5.5 0 0 0-.5-.5Z",
"edit-outline": "M21.03 2.97a3.578 3.578 0 0 1 0 5.06L9.062 20a2.25 2.25 0 0 1-.999.58l-5.116 1.395a.75.75 0 0 1-.92-.921l1.395-5.116a2.25 2.25 0 0 1 .58-.999L15.97 2.97a3.578 3.578 0 0 1 5.06 0ZM15 6.06 5.062 16a.75.75 0 0 0-.193.333l-1.05 3.85 3.85-1.05A.75.75 0 0 0 8 18.938L17.94 9 15 6.06Zm2.03-2.03-.97.97L19 7.94l.97-.97a2.079 2.079 0 0 0-2.94-2.94Z",
"emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z",
"error-circle-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333Zm-.001 10.835a.999.999 0 1 1 0 1.998.999.999 0 0 1 0-1.998ZM11.994 7a.75.75 0 0 1 .744.648l.007.101.004 4.502a.75.75 0 0 1-1.493.103l-.007-.102-.004-4.501a.75.75 0 0 1 .75-.751Z",
@ -69,6 +73,7 @@
"resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z",
"search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z",
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
"send-clock-outline": "M5.694 12 2.299 3.272c-.236-.608.356-1.189.942-.982l.093.04 18 9a.752.752 0 0 1 .264 1.124 6.473 6.473 0 0 0-4.272-1.452L4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .724.556c-.472.26-.909.578-1.3.944H7.011l-2.609 6.71 6.753-3.377a6.522 6.522 0 0 0-.147 1.75l-7.674 3.838c-.583.291-1.217-.245-1.065-.847l.03-.096L5.694 12ZM23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 1 1 0 1H17a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 1 0v2.5Z",
"settings-outline": "M12.012 2.25c.734.008 1.465.093 2.182.253a.75.75 0 0 1 .582.649l.17 1.527a1.384 1.384 0 0 0 1.927 1.116l1.401-.615a.75.75 0 0 1 .85.174 9.792 9.792 0 0 1 2.204 3.792.75.75 0 0 1-.271.825l-1.242.916a1.381 1.381 0 0 0 0 2.226l1.243.915a.75.75 0 0 1 .272.826 9.797 9.797 0 0 1-2.204 3.792.75.75 0 0 1-.848.175l-1.407-.617a1.38 1.38 0 0 0-1.926 1.114l-.169 1.526a.75.75 0 0 1-.572.647 9.518 9.518 0 0 1-4.406 0 .75.75 0 0 1-.572-.647l-.168-1.524a1.382 1.382 0 0 0-1.926-1.11l-1.406.616a.75.75 0 0 1-.849-.175 9.798 9.798 0 0 1-2.204-3.796.75.75 0 0 1 .272-.826l1.243-.916a1.38 1.38 0 0 0 0-2.226l-1.243-.914a.75.75 0 0 1-.271-.826 9.793 9.793 0 0 1 2.204-3.792.75.75 0 0 1 .85-.174l1.4.615a1.387 1.387 0 0 0 1.93-1.118l.17-1.526a.75.75 0 0 1 .583-.65c.717-.159 1.45-.243 2.201-.252Zm0 1.5a9.135 9.135 0 0 0-1.354.117l-.109.977A2.886 2.886 0 0 1 6.525 7.17l-.898-.394a8.293 8.293 0 0 0-1.348 2.317l.798.587a2.881 2.881 0 0 1 0 4.643l-.799.588c.32.842.776 1.626 1.348 2.322l.905-.397a2.882 2.882 0 0 1 4.017 2.318l.11.984c.889.15 1.798.15 2.687 0l.11-.984a2.881 2.881 0 0 1 4.018-2.322l.905.396a8.296 8.296 0 0 0 1.347-2.318l-.798-.588a2.881 2.881 0 0 1 0-4.643l.796-.587a8.293 8.293 0 0 0-1.348-2.317l-.896.393a2.884 2.884 0 0 1-4.023-2.324l-.11-.976a8.988 8.988 0 0 0-1.333-.117ZM12 8.25a3.75 3.75 0 1 1 0 7.5 3.75 3.75 0 0 1 0-7.5Zm0 1.5a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z",
"share-outline": "M6.747 4h3.464a.75.75 0 0 1 .102 1.493l-.102.007H6.747a2.25 2.25 0 0 0-2.245 2.096l-.005.154v9.5a2.25 2.25 0 0 0 2.096 2.245l.154.005h9.5a2.25 2.25 0 0 0 2.245-2.096l.005-.154v-.498a.75.75 0 0 1 1.494-.101l.006.101v.498a3.75 3.75 0 0 1-3.55 3.745l-.2.005h-9.5a3.75 3.75 0 0 1-3.745-3.55l-.005-.2v-9.5a3.75 3.75 0 0 1 3.55-3.745l.2-.005h3.464-3.464ZM14.5 6.52V3.75a.75.75 0 0 1 1.187-.61l.082.069 5.994 5.75c.28.268.306.7.077.997l-.077.085-5.994 5.752a.75.75 0 0 1-1.262-.434l-.007-.107v-2.725l-.344.03c-2.4.25-4.7 1.33-6.914 3.26-.52.453-1.323.025-1.237-.658.664-5.32 3.446-8.252 8.195-8.62l.3-.02V3.75v2.77ZM16 5.509V7.25a.75.75 0 0 1-.75.75c-3.874 0-6.274 1.676-7.312 5.157l-.079.279.352-.237C10.45 11.737 12.798 11 15.251 11a.75.75 0 0 1 .743.648l.007.102v1.743L20.16 9.5l-4.16-3.991Z",
"sound-source-outline": "M3.5 12a8.5 8.5 0 1 1 14.762 5.748l.992 1.135A9.966 9.966 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12a9.966 9.966 0 0 0 2.746 6.883l.993-1.134A8.47 8.47 0 0 1 3.5 12Z M19.25 12.125a7.098 7.098 0 0 1-1.783 4.715l-.998-1.14a5.625 5.625 0 1 0-8.806-.15l-1.004 1.146a7.125 7.125 0 1 1 12.59-4.571Z M16.25 12a4.23 4.23 0 0 1-.821 2.511l-1.026-1.172a2.75 2.75 0 1 0-4.806 0L8.571 14.51A4.25 4.25 0 1 1 16.25 12Z M12.564 12.756a.75.75 0 0 0-1.128 0l-7 8A.75.75 0 0 0 5 22h14a.75.75 0 0 0 .564-1.244l-7-8Zm4.783 7.744H6.653L12 14.389l5.347 6.111Z",

View file

@ -1,4 +1,5 @@
{
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",

View file

@ -56,7 +56,7 @@ export default {
};
},
watch: {
value: function(newValue) {
value(newValue) {
this.greetingsMessage = newValue;
},
},

View file

@ -1,18 +1,21 @@
<template>
<li class="sub-menu-container">
<div class="sub-menu-title">
<span class="small">{{ title }}</span>
</div>
<ul class="sub-menu-li-container">
<woot-dropdown-header :title="title" />
<slot></slot>
</ul>
</li>
</template>
<script>
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
export default {
name: 'WootDropdownMenu',
componentName: 'WootDropdownMenu',
components: {
WootDropdownHeader,
},
props: {
title: {
type: String,
@ -23,21 +26,7 @@ export default {
</script>
<style lang="scss" scoped>
.sub-menu-container {
border-top: 1px solid var(--color-border);
margin-top: var(--space-micro);
&:not(:last-child) {
border-bottom: 1px solid var(--color-border);
}
}
.sub-menu-title {
padding: var(--space-one) var(--space-one) var(--space-smaller);
text-transform: uppercase;
.small {
color: var(--b-600);
font-weight: var(--font-weight-medium);
}
}
.sub-menu-li-container {

View file

@ -13,6 +13,17 @@ export const MESSAGE_TYPE = {
// Size in mega bytes
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
export const ALLOWED_FILE_TYPES =
'image/*,' +
'audio/*,' +
'video/*,' +
'.3gpp,' +
'text/csv, text/plain, application/json, application/pdf, text/rtf,' +
'application/zip, application/x-7z-compressed application/vnd.rar application/x-tar,' +
'application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/vnd.oasis.opendocument.text,' +
'application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' +
'application/vnd.openxmlformats-officedocument.wordprocessingml.document,';
export const CSAT_RATINGS = [
{
key: 'disappointed',

View file

@ -1,4 +1,4 @@
.file-uploads .attachment-button + label {
.file-uploads .attachment-button+label {
cursor: pointer;
}
@ -59,7 +59,7 @@
}
.agent-message-wrap {
+ .agent-message-wrap {
+.agent-message-wrap {
margin-top: $space-micro;
.agent-message .chat-bubble {
@ -67,11 +67,11 @@
}
}
+ .user-message-wrap {
+.user-message-wrap {
margin-top: $space-normal;
}
&.has-response + .user-message-wrap {
&.has-response+.user-message-wrap {
margin-top: $space-micro;
.chat-bubble {
@ -79,7 +79,7 @@
}
}
&.has-response + .agent-message-wrap {
&.has-response+.agent-message-wrap {
margin-top: $space-normal;
}
}
@ -98,9 +98,21 @@
max-width: 100%;
}
.in-progress {
.in-progress,
.is-failed {
opacity: 0.6;
}
.is-failed {
align-items: flex-end;
display: flex;
flex-direction: row-reverse;
.chat-bubble.user {
background: $color-error !important;
// TODO: Remove the important
}
}
}
@ -115,7 +127,7 @@
}
.user-message-wrap {
+ .user-message-wrap {
+.user-message-wrap {
margin-top: $space-micro;
.user-message .chat-bubble {
@ -123,7 +135,7 @@
}
}
+ .agent-message-wrap {
+.agent-message-wrap {
margin-top: $space-normal;
}
}
@ -154,7 +166,7 @@
border: 1px solid $color-border-dark;
}
+ .chat-bubble-wrap {
+.chat-bubble-wrap {
.chat-bubble {
border-top-left-radius: $space-smaller;
}
@ -176,7 +188,7 @@
border-radius: $space-two;
}
+ .chat-bubble-wrap {
+.chat-bubble-wrap {
.chat-bubble {
border-top-right-radius: $space-smaller;
}
@ -206,7 +218,7 @@
text-align: left;
word-break: break-word;
> a {
>a {
color: $color-primary;
word-break: break-all;
}
@ -218,7 +230,7 @@
&.user {
border-bottom-right-radius: $space-smaller;
> a {
>a {
color: $color-white;
}
}

View file

@ -27,16 +27,14 @@
:class="wrapClass"
>
<div v-for="attachment in message.attachments" :key="attachment.id">
<file-bubble
v-if="attachment.file_type !== 'image'"
:url="attachment.data_url"
/>
<image-bubble
v-else
v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url"
:thumb="attachment.thumb_url"
:readable-time="readableTime"
@error="onImageLoadError"
/>
<file-bubble v-else :url="attachment.data_url" />
</div>
</div>
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
@ -83,6 +81,11 @@ export default {
default: () => {},
},
},
data() {
return {
hasImageError: false,
};
},
computed: {
shouldDisplayAgentMessage() {
if (
@ -170,5 +173,18 @@ export default {
};
},
},
watch: {
message() {
this.hasImageError = false;
},
},
mounted() {
this.hasImageError = false;
},
methods: {
onImageLoadError() {
this.hasImageError = true;
},
},
};
</script>

View file

@ -1,7 +1,7 @@
<template>
<file-upload
:size="4096 * 2048"
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
:accept="allowedFileTypes"
@input-file="onFileUpload"
>
<button class="icon-button flex items-center justify-center">
@ -15,7 +15,10 @@
import FileUpload from 'vue-upload-component';
import Spinner from 'shared/components/Spinner.vue';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import {
MAXIMUM_FILE_UPLOAD_SIZE,
ALLOWED_FILE_TYPES,
} from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
export default {
@ -33,6 +36,9 @@ export default {
fileUploadSizeLimit() {
return MAXIMUM_FILE_UPLOAD_SIZE;
},
allowedFileTypes() {
return ALLOWED_FILE_TYPES;
},
},
methods: {
getFileType(fileType) {

View file

@ -6,7 +6,12 @@
class="image"
>
<div class="wrap">
<img :src="thumb" alt="Picture message" />
<img
:src="thumb"
alt="Picture message"
@click="onClick"
@error="onImgError"
/>
<span class="time">{{ readableTime }}</span>
</div>
</a>
@ -14,7 +19,16 @@
<script>
export default {
props: ['url', 'thumb', 'readableTime'],
props: {
url: { type: String, default: '' },
thumb: { type: String, default: '' },
readableTime: { type: String, default: '' },
},
methods: {
onImgError() {
this.$emit('error');
},
},
};
</script>
<style lang="scss" scoped>

View file

@ -11,9 +11,11 @@ export default {
props: {
src: {
type: String,
default: '',
},
size: {
type: String,
default: '',
},
},
computed: {

View file

@ -1,7 +1,10 @@
<template>
<div class="user-message-wrap">
<div class="user-message">
<div class="message-wrap" :class="{ 'in-progress': isInProgress }">
<div
class="message-wrap"
:class="{ 'in-progress': isInProgress, 'is-failed': isFailed }"
>
<user-message-bubble
v-if="showTextBubble"
:message="message.content"
@ -14,19 +17,33 @@
:style="{ backgroundColor: widgetColor }"
>
<div v-for="attachment in message.attachments" :key="attachment.id">
<file-bubble
v-if="attachment.file_type !== 'image'"
:url="attachment.data_url"
:is-in-progress="isInProgress"
/>
<image-bubble
v-else
v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url"
:thumb="attachment.thumb_url"
:readable-time="readableTime"
@error="onImageLoadError"
/>
<file-bubble
v-else
:url="attachment.data_url"
:is-in-progress="isInProgress"
/>
</div>
</div>
<div
v-if="isFailed"
class="flex justify-end align-middle px-4 py-2 text-red-700"
>
<button
v-if="!hasAttachments"
:title="$t('COMPONENTS.MESSAGE_BUBBLE.RETRY')"
class="inline-flex justify-center items-center ml-2"
@click="retrySendMessage"
>
<fluent-icon icon="arrow-clockwise" size="14" />
</button>
</div>
</div>
</div>
</div>
@ -35,6 +52,7 @@
<script>
import UserMessageBubble from 'widget/components/UserMessageBubble';
import ImageBubble from 'widget/components/ImageBubble';
import FluentIcon from 'shared/components/FluentIcon/Index';
import FileBubble from 'widget/components/FileBubble';
import timeMixin from 'dashboard/mixins/time';
import messageMixin from '../mixins/messageMixin';
@ -46,6 +64,7 @@ export default {
UserMessageBubble,
ImageBubble,
FileBubble,
FluentIcon,
},
mixins: [timeMixin, messageMixin],
props: {
@ -54,6 +73,11 @@ export default {
default: () => {},
},
},
data() {
return {
hasImageError: false,
};
},
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
@ -71,6 +95,35 @@ export default {
const { created_at: createdAt = '' } = this.message;
return this.messageStamp(createdAt);
},
isFailed() {
const { status = '' } = this.message;
return status === 'failed';
},
errorMessage() {
const { meta } = this.message;
return meta
? meta.error
: this.$t('COMPONENTS.MESSAGE_BUBBLE.ERROR_MESSAGE');
},
},
watch: {
message() {
this.hasImageError = false;
},
},
mounted() {
this.hasImageError = false;
},
methods: {
async retrySendMessage() {
await this.$store.dispatch(
'conversation/sendMessageWithData',
this.message
);
},
onImageLoadError() {
this.hasImageError = true;
},
},
};
</script>

View file

@ -6,6 +6,10 @@
},
"FORM_BUBBLE": {
"SUBMIT": "Submit"
},
"MESSAGE_BUBBLE": {
"RETRY": "Send message again",
"ERROR_MESSAGE": "Couldn't send, try again"
}
},
"TEAM_AVAILABILITY": {

View file

@ -24,15 +24,35 @@ export const actions = {
commit('setConversationUIFlag', { isCreating: false });
}
},
sendMessage: async ({ commit }, params) => {
sendMessage: async ({ dispatch }, params) => {
const { content } = params;
commit('pushMessageToConversation', createTemporaryMessage({ content }));
await sendMessageAPI(content);
const message = createTemporaryMessage({ content });
dispatch('sendMessageWithData', message);
},
sendMessageWithData: async ({ commit }, message) => {
const { id, content, meta = {} } = message;
commit('pushMessageToConversation', message);
commit('updateMessageMeta', { id, meta: { ...meta, error: '' } });
try {
const { data } = await sendMessageAPI(content);
commit('deleteMessage', message.id);
commit('pushMessageToConversation', { ...data, status: 'sent' });
} catch (error) {
commit('pushMessageToConversation', { ...message, status: 'failed' });
commit('updateMessageMeta', {
id,
meta: { ...meta, error: '' },
});
}
},
sendAttachment: async ({ commit }, params) => {
const {
attachment: { thumbUrl, fileType },
meta = {},
} = params;
const attachment = {
thumb_url: thumbUrl,
@ -50,7 +70,13 @@ export const actions = {
message: data,
tempId: tempMessage.id,
});
commit('pushMessageToConversation', { ...data, status: 'sent' });
} catch (error) {
commit('pushMessageToConversation', { ...tempMessage, status: 'failed' });
commit('updateMessageMeta', {
id: tempMessage.id,
meta: { ...meta, error: '' },
});
// Show error
}
},

View file

@ -72,6 +72,16 @@ export const mutations = {
};
},
updateMessageMeta($state, { id, meta }) {
const message = $state.conversations[id];
if (!message) return;
const newMeta = message.meta ? { ...message.meta, ...meta } : { ...meta };
Vue.set(message, 'meta', {
...newMeta,
});
},
deleteMessage($state, id) {
const messagesInbox = $state.conversations;
Vue.delete(messagesInbox, id);

View file

@ -6,6 +6,7 @@ jest.mock('../../../../helpers/uuid');
jest.mock('widget/helpers/axios');
const commit = jest.fn();
const dispatch = jest.fn();
describe('#actions', () => {
describe('#createConversation', () => {
@ -92,7 +93,7 @@ describe('#actions', () => {
});
describe('#sendMessage', () => {
it('sends correct mutations', () => {
it('sends correct mutations', async () => {
const mockDate = new Date(1466424490000);
getUuid.mockImplementationOnce(() => '1111');
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
@ -109,15 +110,16 @@ describe('#actions', () => {
search: '?param=1',
},
}));
actions.sendMessage({ commit }, { content: 'hello' });
await actions.sendMessage({ commit, dispatch }, { content: 'hello' });
spy.mockRestore();
windowSpy.mockRestore();
expect(commit).toBeCalledWith('pushMessageToConversation', {
id: '1111',
expect(dispatch).toBeCalledWith('sendMessageWithData', {
attachments: undefined,
content: 'hello',
status: 'in_progress',
created_at: 1466424490,
id: '1111',
message_type: 0,
status: 'in_progress',
});
});
});
@ -130,7 +132,7 @@ describe('#actions', () => {
const thumbUrl = '';
const attachment = { thumbUrl, fileType: 'file' };
actions.sendAttachment({ commit }, { attachment });
actions.sendAttachment({ commit, dispatch }, { attachment });
spy.mockRestore();
expect(commit).toBeCalledWith('pushMessageToConversation', {
id: '1111',

View file

@ -3,7 +3,7 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
def perform
Inbox.where(channel_type: 'Channel::Email').all.each do |inbox|
Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
end
end
end

View file

@ -16,7 +16,7 @@ class NotificationListener < BaseListener
def assignee_changed(event)
conversation, account = extract_conversation_and_account(event)
assignee = conversation.assignee
return unless conversation.notifiable_assignee_change?
return if event.data[:notifiable_assignee_change].blank?
return if conversation.pending?
NotificationBuilder.new(

View file

@ -63,14 +63,19 @@ class ReplyMailbox < ApplicationMailbox
# find conversation uuid from below pattern
# <conversation/#{@conversation.uuid}/messages/#{@messages&.last&.id}@#{@account.inbound_email_domain}>
def find_conversation_with_in_reply_to
in_reply_to = mail.in_reply_to
match_result = in_reply_to.match(ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN) if in_reply_to.present?
if match_result
find_conversation_by_uuid(match_result)
else
find_conversation_by_message_id(in_reply_to)
match_result = nil
in_reply_to_addresses = mail.in_reply_to
in_reply_to_addresses = [in_reply_to_addresses] if in_reply_to_addresses.is_a?(String)
in_reply_to_addresses.each do |in_reply_to|
match_result = in_reply_to.match(::ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN)
break if match_result
end
find_by_in_reply_to_addresses(match_result, in_reply_to_addresses)
end
def find_by_in_reply_to_addresses(match_result, in_reply_to_addresses)
find_conversation_by_uuid(match_result) if match_result
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
end
def verify_decoded_params

View file

@ -17,6 +17,18 @@
class Attachment < ApplicationRecord
include Rails.application.routes.url_helpers
ACCEPTABLE_FILE_TYPES = %w[
text/csv text/plain text/rtf
application/json application/pdf
application/zip application/x-7z-compressed application/vnd.rar application/x-tar
application/msword application/vnd.ms-excel application/vnd.ms-powerpoint application/rtf
application/vnd.oasis.opendocument.text
application/vnd.openxmlformats-officedocument.presentationml.presentation
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
application/vnd.openxmlformats-officedocument.wordprocessingml.document
].freeze
belongs_to :account
belongs_to :message
has_one_attached :file
@ -90,10 +102,19 @@ class Attachment < ApplicationRecord
def acceptable_file
return unless should_validate_file?
errors.add(:file, 'is too big') if file.byte_size > 40.megabytes
validate_file_size(file.byte_size)
validate_file_content_type(file.content_type)
end
acceptable_types = ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/tiff', 'application/pdf', 'audio/mpeg', 'video/mp4', 'audio/ogg',
'text/csv'].freeze
errors.add(:file, 'filetype not supported') unless acceptable_types.include?(file.content_type)
def validate_file_content_type(file_content_type)
errors.add(:file, 'type not supported') unless media_file?(file_content_type) || ACCEPTABLE_FILE_TYPES.include?(file_content_type)
end
def validate_file_size(byte_size)
errors.add(:file, 'size is too big') if byte_size > 40.megabytes
end
def media_file?(file_content_type)
file_content_type.start_with?('image/', 'video/', 'audio/')
end
end

View file

@ -24,7 +24,7 @@ module ActivityMessageHandler
I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration)
end
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def create_label_added(user_name, labels = [])
@ -33,7 +33,7 @@ module ActivityMessageHandler
params = { user_name: user_name, labels: labels.join(', ') }
content = I18n.t('conversations.activity.labels.added', **params)
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def create_label_removed(user_name, labels = [])
@ -42,7 +42,7 @@ module ActivityMessageHandler
params = { user_name: user_name, labels: labels.join(', ') }
content = I18n.t('conversations.activity.labels.removed', **params)
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def create_muted_message
@ -51,7 +51,7 @@ module ActivityMessageHandler
params = { user_name: Current.user.name }
content = I18n.t('conversations.activity.muted', **params)
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def create_unmuted_message
@ -60,7 +60,7 @@ module ActivityMessageHandler
params = { user_name: Current.user.name }
content = I18n.t('conversations.activity.unmuted', **params)
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def generate_team_change_activity_key
@ -82,7 +82,7 @@ module ActivityMessageHandler
params[:team_name] = generate_team_name_for_activity if key == 'removed'
content = I18n.t("conversations.activity.team.#{key}", **params)
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
def generate_assignee_change_activity_content(user_name)
@ -96,6 +96,6 @@ module ActivityMessageHandler
return unless user_name
content = generate_assignee_change_activity_content(user_name)
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
end
end

View file

@ -142,9 +142,9 @@ class Conversation < ApplicationRecord
end
def notifiable_assignee_change?
return false if self_assign?(assignee_id)
return false unless saved_change_to_assignee_id?
return false if assignee_id.blank?
return false if self_assign?(assignee_id)
true
end
@ -202,7 +202,7 @@ class Conversation < ApplicationRecord
end
def dispatcher_dispatch(event_name)
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self)
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?)
end
def conversation_status_changed_to_open?

View file

@ -10,12 +10,14 @@
#
# Indexes
#
# index_inbox_members_on_inbox_id (inbox_id)
# index_inbox_members_on_inbox_id (inbox_id)
# index_inbox_members_on_inbox_id_and_user_id (inbox_id,user_id) UNIQUE
#
class InboxMember < ApplicationRecord
validates :inbox_id, presence: true
validates :user_id, presence: true
validates :user_id, uniqueness: { scope: :inbox_id }
belongs_to :user
belongs_to :inbox

View file

@ -66,7 +66,7 @@ class Notification < ApplicationRecord
notification_type: notification_type,
primary_actor_id: primary_actor_id,
primary_actor_type: primary_actor_type,
primary_actor: primary_actor.push_event_data.slice(:conversation_id)
primary_actor: primary_actor.push_event_data.with_indifferent_access.slice('conversation_id', 'id')
}
end

View file

@ -22,4 +22,5 @@
class TeamMember < ApplicationRecord
belongs_to :user
belongs_to :team
validates :user_id, uniqueness: { scope: :team_id }
end

View file

@ -0,0 +1,31 @@
class HtmlParser
def self.parse_reply(raw_body)
new(raw_body).filtered_text
end
attr_reader :raw_body
def initialize(raw_body)
@raw_body = raw_body
end
def document
@document ||= Nokogiri::HTML(raw_body)
end
def filter_replies!
document.xpath('//blockquote').each { |n| n.replace('&gt; ') }
document.xpath('//table').each(&:remove)
end
def filtered_html
@filtered_html ||= begin
filter_replies!
document.inner_html
end
end
def filtered_text
@filtered_text ||= Html2Text.convert(filtered_html)
end
end

View file

@ -117,37 +117,10 @@ class MailPresenter < SimpleDelegator
end
def extract_reply(content)
@regex_arr ||= quoted_text_regexes
content_length = content.length
# calculates the matching regex closest to top of page
index = @regex_arr.inject(content_length) do |min, regex|
[(content.index(regex) || content_length), min].min
end
# NOTE: implement the reply parser over here
{
reply: content[0..(index - 1)].strip,
quoted_text: content[index..].strip
reply: content.strip,
quoted_text: content.strip
}
end
def quoted_text_regexes
return sender_agnostic_regexes if @account.nil? || @account.support_email.blank?
[
Regexp.new("From:\s* #{Regexp.escape(@account.support_email)}", Regexp::IGNORECASE),
Regexp.new("<#{Regexp.escape(@account.support_email)}>", Regexp::IGNORECASE),
Regexp.new("#{Regexp.escape(@account.support_email)}\s+wrote:", Regexp::IGNORECASE),
Regexp.new("On(.*)#{Regexp.escape(@account.support_email)}(.*)wrote:", Regexp::IGNORECASE)
] + sender_agnostic_regexes
end
def sender_agnostic_regexes
@sender_agnostic_regexes ||= [
Regexp.new("^.*On.*(\n)?wrote:$", Regexp::IGNORECASE),
Regexp.new('^.*On(.*)(.*)wrote:$', Regexp::IGNORECASE),
Regexp.new("-+original\s+message-+\s*$", Regexp::IGNORECASE),
Regexp.new("from:\s*$", Regexp::IGNORECASE)
]
end
end

View file

@ -1,5 +1,5 @@
shared: &shared
version: '2.0.0'
version: '2.0.2'
development:
<<: *shared

View file

@ -0,0 +1,19 @@
# ref: https://dev.to/nodefiend/rails-migration-adding-a-unique-index-and-deleting-duplicates-5cde
class AddUniqueIndexOnInboxMembers < ActiveRecord::Migration[6.1]
def up
# partioning the duplicate records and then removing where more than one row is found
ActiveRecord::Base.connection.execute('
DELETE FROM inbox_members WHERE id IN (SELECT id from (
SELECT id, user_id, inbox_id, ROW_NUMBER() OVER w AS rnum FROM inbox_members WINDOW w AS (
PARTITION BY inbox_id, user_id ORDER BY id
)
) t WHERE t.rnum > 1)
')
add_index :inbox_members, [:inbox_id, :user_id], unique: true
end
def down
remove_index :inbox_members, [:inbox_id, :user_id]
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_12_19_031453) do
ActiveRecord::Schema.define(version: 2021_12_21_125545) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@ -429,6 +429,7 @@ ActiveRecord::Schema.define(version: 2021_12_19_031453) do
t.integer "inbox_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["inbox_id", "user_id"], name: "index_inbox_members_on_inbox_id_and_user_id", unique: true
t.index ["inbox_id"], name: "index_inbox_members_on_inbox_id"
end

View file

@ -2,6 +2,8 @@ module ExceptionList
REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest,
RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError,
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout,
RestClient::TemporaryRedirect, RestClient::SSLCertificateNotVerified, RestClient::PaymentRequired,
RestClient::BadGateway, RestClient::Unauthorized, RestClient::PayloadTooLarge,
RestClient::MovedPermanently, RestClient::ServiceUnavailable, Errno::ECONNREFUSED, SocketError].freeze
SMTP_EXCEPTIONS = [
Net::SMTPSyntaxError

View file

@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "2.0.0",
"version": "2.0.2",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue} --fix",
@ -51,7 +51,6 @@
"semver": "7.3.5",
"spinkit": "~1.2.5",
"tailwindcss": "^1.9.6",
"tween.js": "~16.6.0",
"url-loader": "^2.0.0",
"v-tooltip": "~2.1.3",
"vue": "2.6.12",

View file

@ -81,7 +81,7 @@ RSpec.describe 'Inbox Member API', type: :request do
end
it 'add inbox members' do
params = { inbox_id: inbox.id, user_ids: [agent_to_add.id] }
params = { inbox_id: inbox.id, user_ids: [old_agent.id, agent_to_add.id] }
post "/api/v1/accounts/#{account.id}/inbox_members",
headers: administrator.create_new_auth_token,

View file

@ -55,6 +55,8 @@ RSpec.describe 'Team Members API', type: :request do
it 'add a new team members when its administrator' do
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
params = { user_ids: user_ids }
# have a team member added already
create(:team_member, team: team, user: User.find(user_ids.first))
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
@ -63,7 +65,7 @@ RSpec.describe 'Team Members API', type: :request do
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response.count).to eq(user_ids.count)
expect(json_response.count).to eq(user_ids.count - 1)
end
end
end
@ -93,15 +95,16 @@ RSpec.describe 'Team Members API', type: :request do
it 'destroys the team members when its administrator' do
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
params = { user_ids: user_ids }
user_ids.each { |id| create(:team_member, team: team, user: User.find(id)) }
params = { user_ids: user_ids.first(3) }
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
params: params,
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(team.team_members.count).to eq(0)
expect(team.team_members.count).to eq(2)
end
end
end

View file

@ -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
X-Forwarded-For: reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com
In-Reply-To: <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>
In-Reply-To: <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com> <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>
X-Mailer: Apple Mail (2.1244.3)

47
spec/fixtures/files/mail_with_quote.eml vendored Normal file
View file

@ -0,0 +1,47 @@
MIME-Version: 1.0
Date: Thu, 19 Aug 2021 14:14:31 +0530
References: <CAFkiBVxGoURoqdkY-O_25F-8b41kb-GWBc6hh4Djd5ynwOikXA@mail.gmail.com> <0100017b5d8efc70-c7f18809-aa55-48f6-91fd-b626092ed8b3-000000@email.amazonses.com>
In-Reply-To: <0100017b5d8efc70-c7f18809-aa55-48f6-91fd-b626092ed8b3-000000@email.amazonses.com>
Message-ID: <CAFkiBVwJjO_k_e-LpiKi7MAQAKbHX5nkEPcf0y1R=bjcEHogMg@mail.gmail.com>
Subject: Re: Checking mail forwarding to cw inbox
From: Sony Mathew <sony@chatwoot.com>
To: Tejaswini <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com>
Content-Type: multipart/alternative; boundary="0000000000004af64505c9e58f03"
--0000000000004af64505c9e58f03
Content-Type: text/plain; charset="UTF-8"
Yes, I am providing you step how to reproduce this issue
On Thu, Aug 19, 2021 at 2:07 PM Tejaswini from Email sender test <
tejaswini@chatwoot.com> wrote:
> Any update on this?
>
>
--
* Sony Mathew*
Software developer
*Mob:9999999999
--0000000000004af64505c9e58f03
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<div dir=3D"ltr">Yes, I am providing you step how to reproduce this issue</=
div><br><div class=3D"gmail_quote"><div dir=3D"ltr" class=3D"gmail_attr">On=
Thu, Aug 19, 2021 at 2:07 PM Tejaswini from Email sender test &l=
t;<a href=3D"mailto:tejaswini@chatwoot.com">tejaswini@chatwoot.com</a>&gt; wrot=
e:<br></div><blockquote class=3D"gmail_quote" style=3D"margin:0px 0px 0px 0=
.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"> <p>
</p><p>Any update on this?</p>
<p></p>
</blockquote></div><br clear=3D"all"><div><br></div>-- <br><div dir=3D"ltr"=
class=3D"gmail_signature"><div dir=3D"ltr"><div><div dir=3D"ltr"><div><div=
><b>Sony Mathew.</b><br></div><span style=3D"font-family:&quot;times ne=
w roman&quot;,serif"><span></span><span></span>Software developer</span><br=
></div><b>Mob:9999999999</b></div></div></div></div>
--0000000000004af64505c9e58f03--

View file

@ -3,7 +3,7 @@ Mime-Version: 1.0 (Apple Message framework v1244.3)
Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
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>
In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>, <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
To: "Replies" <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com>
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>
@ -628,4 +628,4 @@ r/Fxp3Y0d2/4tvsR95f/AAH7Gvwmn9rUHsOgUCg//9k=
--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1--
--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74--
--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74--

View file

@ -3,7 +3,7 @@ Mime-Version: 1.0 (Apple Message framework v1244.3)
Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
Subject: Discussion: Let's debate these attachments
Date: Tue, 20 Apr 2020 04:20:20 -0400
In-Reply-To: <account/1/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>
In-Reply-To: <account/1/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com> <conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489/messages/123@test.com>
To: "Replies" <test@example.com>
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>

1061
spec/fixtures/files/welcome_html.eml vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ RSpec.describe ReplyMailbox, type: :mailbox do
let(:account) { create(:account) }
let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
let(:reply_mail) { create_inbound_email_from_fixture('reply.eml') }
let(:mail_with_quote) { create_inbound_email_from_fixture('mail_with_quote.eml') }
let(:conversation) { create(:conversation, assignee: agent, inbox: create(:inbox, account: account, greeting_enabled: false), account: account) }
let(:described_subject) { described_class.receive reply_mail }
let(:serialized_attributes) do

View file

@ -78,19 +78,11 @@ shared_examples_for 'assignment_handler' do
expect(conversation.reload.assignee).to eq(agent)
end
it 'creates a new notification for the agent' do
it 'dispaches assignee changed event' do
# TODO: FIX me
# expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once).with('assignee.changed', anything, anything, anything, anything))
expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once))
expect(update_assignee).to eq(true)
expect(agent.notifications.count).to eq(1)
end
it 'does not create assignment notification if notification setting is turned off' do
notification_setting = agent.notification_settings.first
notification_setting.unselect_all_email_flags
notification_setting.unselect_all_push_flags
notification_setting.save!
expect(update_assignee).to eq(true)
expect(agent.notifications.count).to eq(0)
end
context 'when agent is current user' do

View file

@ -54,7 +54,7 @@ RSpec.describe Conversation, type: :model do
it 'runs after_create callbacks' do
# send_events
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation)
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false)
end
end
@ -85,11 +85,11 @@ RSpec.describe Conversation, type: :model do
label_list: [label.title]
)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation)
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation)
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation)
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
end
it 'creates conversation activities' do

View file

@ -78,4 +78,23 @@ RSpec.describe Notification do
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John Peter please check this?"
end
end
context 'when fcm push data' do
it 'returns correct data for primary actor conversation' do
notification = create(:notification, notification_type: 'conversation_creation')
expect(notification.fcm_push_data[:primary_actor]).to eq({
'id' => notification.primary_actor.display_id
})
end
it 'returns correct data for primary actor message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
expect(notification.fcm_push_data[:primary_actor]).to eq({
'id' => notification.primary_actor.id,
'conversation_id' => notification.primary_actor.conversation.display_id
})
end
end
end

View file

@ -0,0 +1,15 @@
require 'rails_helper'
RSpec.describe HtmlParser do
include ActionMailbox::TestHelper
describe 'parsed mail decorator' do
let(:html_mail) { create_inbound_email_from_fixture('welcome_html.eml').mail }
it 'parse html content in the mail' do
decorated_html_mail = described_class.parse_reply(html_mail.text_part.decoded)
expect(decorated_html_mail[0..70]).to eq(
"I'm learning English as a first language for the past 13 years, but to "
)
end
end
end

View file

@ -4,6 +4,7 @@ RSpec.describe MailPresenter do
describe 'parsed mail decorator' do
let(:mail) { create_inbound_email_from_fixture('welcome.eml').mail }
let(:html_mail) { create_inbound_email_from_fixture('welcome_html.eml').mail }
let(:decorated_mail) { described_class.new(mail) }
let(:mail_with_no_subject) { create_inbound_email_from_fixture('mail_with_no_subject.eml').mail }
@ -56,5 +57,13 @@ RSpec.describe MailPresenter do
it 'give email from in downcased format' do
expect(decorated_mail.from.first.eql?(mail.from.first.downcase)).to eq true
end
it 'parse html content in the mail' do
decorated_html_mail = described_class.new(html_mail)
expect(decorated_html_mail.subject).to eq('Fwd: How good are you in English? How did you improve your English?')
expect(decorated_html_mail.text_content[:reply][0..70]).to eq(
"I'm learning English as a first language for the past 13 years, but to "
)
end
end
end

View file

@ -14533,11 +14533,6 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tween.js@~16.6.0:
version "16.6.0"
resolved "https://registry.yarnpkg.com/tween.js/-/tween.js-16.6.0.tgz#739104c9336cc4f11ee53f9ce7cede51e6723624"
integrity sha1-c5EEyTNsxPEe5T+c587eUeZyNiQ=
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"