Merge branch 'release/2.1.0'

This commit is contained in:
Sojan
2022-01-11 19:28:42 -08:00
84 changed files with 1482 additions and 261 deletions

View File

@@ -11,6 +11,7 @@ Metrics/ClassLength:
Max: 125
Exclude:
- 'app/models/conversation.rb'
- 'app/models/contact.rb'
- 'app/mailers/conversation_reply_mailer.rb'
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'

View File

@@ -50,7 +50,7 @@ decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
This Code of Conduct applies within all community spaces and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed

View File

@@ -2,4 +2,4 @@
Thanks for taking the time to contribute! :tada::+1:
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions.
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute.

View File

@@ -255,9 +255,9 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
google-protobuf (3.18.1)
google-protobuf (3.18.1-universal-darwin)
google-protobuf (3.18.1-x86_64-linux)
google-protobuf (3.19.2)
google-protobuf (3.19.2-x86_64-darwin)
google-protobuf (3.19.2-x86_64-linux)
googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)

View File

@@ -118,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2021, Chatwoot Inc - Released under the MIT License.
*Chatwoot* &copy; 2017-2022, Chatwoot Inc - Released under the MIT License.

View File

@@ -8,9 +8,11 @@ class Messages::MessageBuilder
@conversation = conversation
@user = user
@message_type = params[:message_type] || 'outgoing'
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
@attachments = params[:attachments]
return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
end
def perform

View File

@@ -0,0 +1,21 @@
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
before_action :check_authorization
def index
@automation_rules = Current.account.automation_rules
end
def create
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
end
private
def automation_rules_permit
params.permit(
:name, :description, :event_name, :account_id,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
actions: [:action_name, { action_params: [:intiated_at] }]
)
end
end

View File

@@ -118,7 +118,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def permitted_params(channel_attributes = [])
params.permit(
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
channel: [:type, *channel_attributes]
)
end

View File

@@ -55,7 +55,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if GlobalConfig.get_value('ENABLE_ACCOUNT_SIGNUP') == 'false'
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
end
def pundit_user

View File

@@ -7,6 +7,12 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
render json: notification_subscription
end
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy
head :ok
end
private
def set_user

View File

@@ -0,0 +1,21 @@
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
def show
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
# ref: https://github.com/rubocop/rubocop/issues/7767
# rubocop:disable Style/HashTransformValues
@fb_config = InstallationConfig.where(name: @allowed_configs)
.pluck(:name, :serialized_value)
.map { |name, serialized_value| [name, serialized_value['value']] }
.to_h
# rubocop:enable Style/HashTransformValues
end
def create
params['app_config'].each do |key, value|
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
i.value = value
i.save!
end
redirect_to super_admin_app_config_url
end
end

View File

@@ -12,6 +12,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
avatar_url: AvatarField,
id: Field::Number,
name: Field::String,
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
description: Field::String,
outgoing_url: Field::String,
created_at: Field::DateTime,
@@ -26,6 +27,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
COLLECTION_ATTRIBUTES = %i[
id
avatar_url
account
name
outgoing_url
].freeze
@@ -34,7 +36,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
avatar_url
account
name
description
outgoing_url
@@ -45,6 +47,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
account
description
outgoing_url
].freeze

View File

@@ -16,7 +16,8 @@ class AsyncDispatcher < BaseDispatcher
HookListener.instance,
InstallationWebhookListener.instance,
NotificationListener.instance,
WebhookListener.instance
WebhookListener.instance,
AutomationRuleListener.instance
]
end
end

View File

@@ -2,6 +2,6 @@ require 'administrate/field/base'
class AvatarField < Administrate::Field::Base
def avatar_url
data.presence || '/admin/avatar.png'
data.presence&.gsub('?d=404', '?d=mp')
end
end

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

@@ -1,7 +1,7 @@
<template>
<aside class="woot-sidebar">
<primary-sidebar
:logo-source="globalConfig.logo"
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"
:account-id="accountId"
:menu-items="primaryMenuItems"
@@ -191,16 +191,6 @@ export default {
background: var(--white);
display: flex;
}
.secondary-menu {
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100vh;
width: 19rem;
flex-shrink: 0;
overflow: auto;
padding: var(--space-small);
}
</style>
<style lang="scss">

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

@@ -168,3 +168,18 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.secondary-menu {
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100vh;
width: 19rem;
flex-shrink: 0;
overflow: hidden;
padding: var(--space-small);
&:hover {
overflow: auto;
}
}
</style>

View File

@@ -4,7 +4,7 @@
"HEADER_BTN_TXT": "Add Canned Response",
"LOADING": "Fetching Canned Responses",
"SEARCH_404": "There are no items matching this query",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation . </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>",
"LIST": {
"404": "There are no canned responses available in this account.",
"TITLE": "Manage canned responses",
@@ -17,12 +17,12 @@
},
"ADD": {
"TITLE": "Add Canned Response",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation .",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
"CANCEL_BUTTON_TEXT": "Cancel",
"FORM": {
"SHORT_CODE": {
"LABEL": "Short Code",
"PLACEHOLDER": "Please enter a shortcode",
"PLACEHOLDER": "Please enter a short code",
"ERROR": "Short Code is required"
},
"CONTENT": {

View File

@@ -129,7 +129,7 @@
},
"TEAM_MEMBERS": {
"TITLE": "Invite your team members",
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email address to the agent list.",
"DESCRIPTION": "Since you are getting ready to talk to your customer, bring in your teammates to assist you. You can invite your teammates by adding their email addresses to the agent list.",
"NEW_LINK": "Click here to invite a team member"
},
"INBOXES": {

View File

@@ -306,6 +306,10 @@
"ENABLED": "Enabled",
"DISABLED": "Disabled"
},
"ALLOW_MESSAGES_AFTER_RESOLVED": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
},
"ENABLE_HMAC": {
"LABEL": "Enable"
}
@@ -362,7 +366,9 @@
"INBOX_IDENTIFIER": "Inbox Identifier",
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email",
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address."
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved."
},
"FACEBOOK_REAUTHORIZE": {
"TITLE": "Reauthorize",

View File

@@ -89,14 +89,14 @@
"PLACEHOLDER": "Please enter the current password"
},
"PASSWORD": {
"LABEL": "Password",
"LABEL": "New password",
"ERROR": "Please enter a password of length 6 or more",
"PLACEHOLDER": "Please enter a new password"
},
"PASSWORD_CONFIRMATION": {
"LABEL": "Confirm new password",
"ERROR": "Confirm password should match the password",
"PLACEHOLDER": "Please re-enter your password"
"PLACEHOLDER": "Please re-enter your new password"
}
}
},

View File

@@ -5,6 +5,7 @@
:no-auto-load-md-icons="true"
hideBreadcrumbs
:placeholder="placeholder"
@selected="setCommandbarData"
/>
</template>

View File

@@ -10,17 +10,24 @@
/>
<div class="contact--details">
<h3 v-if="showAvatar" class="sub-block-title contact--name">
<div v-if="showAvatar" class="contact--name-wrap">
<h3 class="sub-block-title contact--name">
{{ contact.name }}
</h3>
<a
:href="contactProfileLink"
class="fs-default"
target="_blank"
rel="noopener nofollow noreferrer"
>
{{ contact.name }}
<fluent-icon size="16" icon="open" class="open-link--icon" />
<woot-button
size="tiny"
icon="open"
variant="clear"
color-scheme="secondary"
/>
</a>
</h3>
</div>
<p v-if="additionalAttributes.description" class="contact--bio">
{{ additionalAttributes.description }}
</p>
@@ -294,19 +301,20 @@ export default {
text-align: left;
}
.contact--name-wrap {
display: flex;
align-items: center;
margin-bottom: var(--space-small);
}
.contact--name {
text-transform: capitalize;
white-space: normal;
margin: 0 var(--space-smaller) 0 0;
a {
color: var(--color-body);
}
.open-link--icon {
color: var(--color-body);
font-size: var(--font-size-small);
margin-left: var(--space-smaller);
}
}
.contact--metadata {

View File

@@ -215,6 +215,25 @@
</p>
</label>
<label class="medium-9 columns">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.ALLOW_MESSAGES_AFTER_RESOLVED') }}
<select v-model="allowMessagesAfterResolved">
<option :value="true">
{{ $t('INBOX_MGMT.EDIT.ALLOW_MESSAGES_AFTER_RESOLVED.ENABLED') }}
</option>
<option :value="false">
{{ $t('INBOX_MGMT.EDIT.ALLOW_MESSAGES_AFTER_RESOLVED.DISABLED') }}
</option>
</select>
<p class="help-text">
{{
$t(
'INBOX_MGMT.SETTINGS_POPUP.ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT'
)
}}
</p>
</label>
<label v-if="isAWebWidgetInbox">
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
</label>
@@ -420,6 +439,7 @@ export default {
emailCollectEnabled: false,
isAgentListUpdating: false,
csatSurveyEnabled: false,
allowMessagesAfterResolved: true,
selectedInboxName: '',
channelWebsiteUrl: '',
webhookUrl: '',
@@ -583,6 +603,7 @@ export default {
this.autoAssignment = this.inbox.enable_auto_assignment;
this.emailCollectEnabled = this.inbox.enable_email_collect;
this.csatSurveyEnabled = this.inbox.csat_survey_enabled;
this.allowMessagesAfterResolved = this.inbox.allow_messages_after_resolved;
this.channelWebsiteUrl = this.inbox.website_url;
this.channelWelcomeTitle = this.inbox.welcome_title;
this.channelWelcomeTagline = this.inbox.welcome_tagline;
@@ -625,6 +646,7 @@ export default {
enable_auto_assignment: this.autoAssignment,
enable_email_collect: this.emailCollectEnabled,
csat_survey_enabled: this.csatSurveyEnabled,
allow_messages_after_resolved: this.allowMessagesAfterResolved,
greeting_enabled: this.greetingEnabled,
greeting_message: this.greetingMessage || '',
channel: {

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

@@ -15,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",
@@ -36,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",
@@ -70,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,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

@@ -58,9 +58,9 @@ export default {
return getContrastingTextColor(this.widgetColor);
},
hideReplyBox() {
const { csatSurveyEnabled } = window.chatwootWebChannel;
const { allowMessagesAfterResolved } = window.chatwootWebChannel;
const { status } = this.conversationAttributes;
return csatSurveyEnabled && status === 'resolved';
return !allowMessagesAfterResolved && status === 'resolved';
},
showEmailTranscriptButton() {
return this.currentUser && this.currentUser.email;

View File

@@ -0,0 +1,29 @@
class AutomationRuleListener < BaseListener
def conversation_status_changed(event_obj)
conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_status_changed', conversation)
@rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present?
end
end
def conversation_created(event_obj)
conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_created', conversation)
@rules.each do |rule|
conditions_match = AutomationRule::ConditionsFilterService.new(rule, conversation).perform
AutomationRule::ActionService.new(rule, conversation).perform if conditions_match.present?
end
end
def rule_present?(event_name, conversation)
@rules = AutomationRule.where(
event_name: event_name,
account_id: conversation.account_id
)
@rules.any?
end
end

View File

@@ -0,0 +1,53 @@
class TeamNotifications::AutomationNotificationMailer < ApplicationMailer
def conversation_creation(conversation, team, message)
return unless smtp_config_set_or_development?
@agents = team.team_members
@conversation = conversation
@message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_an_email_to_team
end
def conversation_updated(conversation, team)
return unless smtp_config_set_or_development?
@agents = team.team_members
@conversation = conversation
@message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_an_email_to_team
end
def message_created(message, agent)
return unless smtp_config_set_or_development?
@agent = agent
@conversation = message.conversation
@message = message
subject = "#{@agent.available_name}, You have been mentioned in conversation [ID - #{@conversation.display_id}]"
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: @agent.email, subject: subject)
end
private
def send_an_email_to_team
@agents.each do |agent|
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}."
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: agent.email, subject: subject)
end
end
def liquid_droppables
super.merge({
user: @agent,
conversation: @conversation,
inbox: @conversation.inbox,
message: @message
})
end
end

View File

@@ -69,6 +69,7 @@ class Account < ApplicationRecord
has_many :webhooks, dependent: :destroy_async
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
has_many :working_hours, dependent: :destroy_async
has_many :automation_rules, dependent: :destroy
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)

View File

@@ -0,0 +1,44 @@
# == Schema Information
#
# Table name: automation_rules
#
# id :bigint not null, primary key
# actions :jsonb not null
# conditions :jsonb not null
# description :text
# event_name :string not null
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_automation_rules_on_account_id (account_id)
#
class AutomationRule < ApplicationRecord
belongs_to :account
validates :account, presence: true
validate :json_conditions_format
validate :json_actions_format
CONDITIONS_ATTRS = %w[country_code status browser_language assignee_id team_id referer].freeze
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents].freeze
private
def json_conditions_format
return if conditions.nil?
attributes = conditions.map { |obj, _| obj['attribute_key'] }
(attributes - CONDITIONS_ATTRS).blank?
end
def json_actions_format
return if actions.nil?
attributes = actions.map { |obj, _| obj['attribute_key'] }
(attributes - ACTIONS_ATTRS).blank?
end
end

View File

@@ -8,4 +8,9 @@ module Labelable
def update_labels(labels = nil)
update!(label_list: labels)
end
def add_labels(new_labels = nil)
new_labels << labels
update!(label_list: new_labels)
end
end

View File

@@ -45,7 +45,7 @@ class Contact < ApplicationRecord
has_many :messages, as: :sender, dependent: :destroy_async
has_many :notes, dependent: :destroy_async
before_validation :prepare_email_attribute
before_validation :prepare_contact_attributes
after_create_commit :dispatch_create_event, :ip_lookup
after_update_commit :dispatch_update_event
after_destroy_commit :dispatch_destroy_event
@@ -146,10 +146,19 @@ class Contact < ApplicationRecord
ContactIpLookupJob.perform_later(self)
end
def prepare_contact_attributes
prepare_email_attribute
prepare_jsonb_attributes
end
def prepare_email_attribute
# So that the db unique constraint won't throw error when email is ''
self.email = nil if email.blank?
email.downcase! if email.present?
self.email = email.present? ? email.downcase : nil
end
def prepare_jsonb_attributes
self.additional_attributes = {} if additional_attributes.blank?
self.custom_attributes = {} if custom_attributes.blank?
end
def dispatch_create_event

View File

@@ -4,22 +4,23 @@
#
# Table name: inboxes
#
# id :integer not null, primary key
# channel_type :string
# csat_survey_enabled :boolean default(FALSE)
# email_address :string
# enable_auto_assignment :boolean default(TRUE)
# enable_email_collect :boolean default(TRUE)
# greeting_enabled :boolean default(FALSE)
# greeting_message :string
# name :string not null
# out_of_office_message :string
# timezone :string default("UTC")
# working_hours_enabled :boolean default(FALSE)
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
# channel_id :integer not null
# id :integer not null, primary key
# allow_messages_after_resolved :boolean default(TRUE)
# channel_type :string
# csat_survey_enabled :boolean default(FALSE)
# email_address :string
# enable_auto_assignment :boolean default(TRUE)
# enable_email_collect :boolean default(TRUE)
# greeting_enabled :boolean default(FALSE)
# greeting_message :string
# name :string not null
# out_of_office_message :string
# timezone :string default("UTC")
# working_hours_enabled :boolean default(FALSE)
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
# channel_id :integer not null
#
# Indexes
#

View File

@@ -16,11 +16,9 @@
# unique_permissibles_index (platform_app_id,permissible_id,permissible_type) UNIQUE
#
class PlatformAppPermissible < ApplicationRecord
include AccessTokenable
validates :platform_app, presence: true
validates :platform_app_id, uniqueness: { scope: [:permissible_id, :permissible_type] }
belongs_to :platform_app
belongs_to :permissible, polymorphic: true, dependent: :destroy_async
belongs_to :permissible, polymorphic: true
end

View File

@@ -0,0 +1,9 @@
class AutomationRulePolicy < ApplicationPolicy
def index?
@account_user.administrator?
end
def create?
@account_user.administrator?
end
end

View File

@@ -0,0 +1,63 @@
class AutomationRules::ActionService
def initialize(rule, conversation)
@rule = rule
@conversation = conversation
@account = @conversation.account
end
def perform
@rule.actions.each do |action, _current_index|
action = action.with_indifferent_access
send(action[:action_name], action[:action_params])
end
end
private
def send_message(message)
# params = { content: message, private: false }
# mb = Messages::MessageBuilder.new(@administrator, @conversation, params)
# mb.perform
end
def assign_team(team_ids = [])
return unless team_belongs_to_account?(team_ids)
@account.teams.find_by(id: team_ids)
@conversation.update!(team_id: team_ids[0])
end
def assign_best_agents(agent_ids = [])
return unless agent_belongs_to_account?(agent_ids)
@agent = @account.users.find_by(id: agent_ids)
@conversation.update_assignee(@agent)
end
def add_label(labels = [])
@conversation.add_labels(labels)
end
def send_email_to_team(params)
team = Team.find(params[:team_ids][0])
case @rule.event_name
when 'conversation_created', 'conversation_status_changed'
TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message])
when 'conversation_updated'
TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message])
end
end
def administrator
@administrator ||= @account.administrators.first
end
def agent_belongs_to_account?(agent_ids)
@account.agents.pluck(:id).include?(agent_ids[0])
end
def team_belongs_to_account?(team_ids)
@account.team_ids.include?(team_ids[0])
end
end

View File

@@ -0,0 +1,45 @@
require 'json'
class AutomationRules::ConditionsFilterService < FilterService
def initialize(rule, conversation)
super([], nil)
@rule = rule
@conversation = conversation
file = File.read('./lib/filters/filter_keys.json')
@filters = JSON.parse(file)
end
def perform
conversation_filters = @filters['conversations']
@rule.conditions.each_with_index do |query_hash, current_index|
current_filter = conversation_filters[query_hash['attribute_key']]
@query_string += conversation_query_string(current_filter, query_hash.with_indifferent_access, current_index)
end
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
records.any?
end
def conversation_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash['attribute_key']
query_operator = query_hash['query_operator']
filter_operator_value = filter_operation(query_hash, current_index)
case current_filter['attribute_type']
when 'additional_attributes'
" conversations.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
when 'standard'
if attribute_key == 'labels'
" tags.id #{filter_operator_value} #{query_operator} "
else
" conversations.#{attribute_key} #{filter_operator_value} #{query_operator} "
end
end
end
def base_relation
Conversation.where(id: @conversation)
end
end

View File

@@ -51,7 +51,6 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService
def send_to_facebook_page(message_content)
access_token = channel.page_access_token
app_secret_proof = calculate_app_secret_proof(GlobalConfigService.load('FB_APP_SECRET', ''), access_token)
query = { access_token: access_token }
query[:appsecret_proof] = app_secret_proof if app_secret_proof

View File

@@ -0,0 +1 @@
json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule

View File

@@ -0,0 +1,5 @@
json.data do
json.array! @automation_rules do |automation_rule|
json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: automation_rule
end
end

View File

@@ -0,0 +1,7 @@
json.id automation_rule.id
json.account_id automation_rule.account_id
json.name automation_rule.name
json.description automation_rule.description
json.event_name automation_rule.event_name
json.conditions automation_rule.conditions
json.actions automation_rule.actions

View File

@@ -13,6 +13,7 @@ json.out_of_office_message resource.out_of_office_message
json.working_hours resource.weekly_schedule
json.timezone resource.timezone
json.callback_webhook_url resource.callback_webhook_url
json.allow_messages_after_resolved resource.allow_messages_after_resolved
json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter?

View File

@@ -1 +1 @@
<%= image_tag field.avatar_url %>
<%= image_tag field.avatar_url if field.avatar_url.present? %>

View File

@@ -1 +1 @@
<%= image_tag field.avatar_url %>
<%= image_tag field.avatar_url if field.avatar_url.present? %>

View File

@@ -0,0 +1,8 @@
<p>Hi {{user.available_name}}</p>
<p>Time to save the world. A new conversation has been created in {{ inbox.name }}</p>
<p>
Click <a href="{{ action_url }}">here</a> to get cracking.
</p>

View File

@@ -0,0 +1,25 @@
<% content_for(:title) do %>
App Config
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
</header>
<section class="main-content__body">
<%= form_with url: super_admin_app_config_url , method: :post do |form| %>
<% @allowed_configs.each do |c| %>
<div class="field-unit">
<div class="field-unit__label">
<%= form.label "app_config[#{c}]", c %>
</div>
<div class="field-unit__field">
<%= form.text_field "app_config[#{c}]", value: @fb_config[c] %>
</div>
</div>
<% end %>
<div class="form-actions">
<%= form.submit "Submit" %>
</div>
<% end %>
</section>

View File

@@ -17,7 +17,8 @@ as defined by the routes in the `admin/` namespace
super_admins: 'ion ion-unlocked',
access_tokens: 'ion-key',
platform_apps: 'ion ion-social-buffer',
installation_configs: 'ion ion-settings'
installation_configs: 'ion ion-settings',
agent_bots: 'ion ion-social-android',
}
%>
@@ -32,8 +33,14 @@ as defined by the routes in the `admin/` namespace
<i class="ion ion-ios-keypad"></i>
<%= link_to "Dashboard", super_admin_root_url %>
</li>
<li class="navigation__link">
<i class="ion ion-android-settings"></i>
<%= link_to "App Config", super_admin_app_config_url %>
</li>
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
<% next if ["account_users", "agent_bots", "dashboard", "devise/sessions"].include? resource.resource %>
<% next if ["account_users", "dashboard", "devise/sessions", "app_configs" ].include? resource.resource %>
<li class="navigation__link navigation__link--<%= nav_link_state(resource) %>">
<i class="<%= sidebar_icons[resource.resource.to_sym] %>"></i>
<%= link_to(

View File

@@ -62,5 +62,5 @@ It renders the `_table` partial to display details about the resources.
table_title: "page-title"
) %>
<%= paginate resources %>
<%= paginate resources, param_name: '_page'%>
</section>

View File

@@ -1,66 +0,0 @@
<%#
# Index
This view is the template for the index page.
It is responsible for rendering the search bar, header and pagination.
It renders the `_table` partial to display details about the resources.
## Local variables:
- `page`:
An instance of [Administrate::Page::Collection][1].
Contains helper methods to help display a table,
and knows which attributes should be displayed in the resource's table.
- `resources`:
An instance of `ActiveRecord::Relation` containing the resources
that match the user's search criteria.
By default, these resources are passed to the table partial to be displayed.
- `search_term`:
A string containing the term the user has searched for, if any.
- `show_search_bar`:
A boolean that determines if the search bar should be shown.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<% content_for(:title) do %>
<%= display_resource_name(page.resource_name) %>
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
<% if show_search_bar %>
<%= render(
"search",
search_term: search_term,
resource_name: display_resource_name(page.resource_name)
) %>
<% end %>
<div>
<%= link_to(
t(
"administrate.actions.new_resource",
name: page.resource_name.titleize.downcase
),
[:new, namespace, page.resource_path.to_sym],
class: "button",
) if valid_action?(:new) && show_action?(:new, new_resource) %>
</div>
</header>
<section class="main-content__body main-content__body--flush">
<%= render(
"collection",
collection_presenter: page,
collection_field_name: resource_name,
page: page,
resources: resources,
table_title: "page-title"
) %>
<%= paginate resources %>
</section>

View File

@@ -23,7 +23,8 @@
csatSurveyEnabled: <%= @web_widget.inbox.csat_survey_enabled %>,
workingHours: <%= @web_widget.inbox.working_hours.to_json.html_safe %>,
outOfOfficeMessage: <%= @web_widget.inbox.out_of_office_message.to_json.html_safe %>,
utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].now.formatted_offset %>'
utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].now.formatted_offset %>',
allowMessagesAfterResolved: <%= @web_widget.inbox.allow_messages_after_resolved %>
}
window.chatwootWidgetDefaults = {
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,

View File

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

View File

@@ -53,8 +53,8 @@ de:
deleted: Diese Nachricht wurde gelöscht
activity:
status:
resolved: "Das Gespräch wurde von gelöst gelöst %{user_name}"
open: "Das Gespräch wurde von wieder eröffnet %{user_name}"
resolved: "Das Gespräch wurde von %{user_name} gelöst"
open: "Das Gespräch wurde von %{user_name} wieder eröffnet"
pending: "Das Gespräch wurde von %{user_name} als ausstehend markiert"
snoozed: "Das Gespräch wurde von %{user_name} zur Erinnerung markiert"
auto_resolved: "Das Gespräch wurde vom System aufgrund von %{duration} Tagen Inaktivität gelöst"

View File

@@ -52,6 +52,7 @@ Rails.application.routes.draw do
end
end
resources :canned_responses, except: [:show, :edit, :new]
resources :automation_rules, only: [:create, :index]
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
namespace :channels do
@@ -172,7 +173,7 @@ Rails.application.routes.draw do
end
end
resource :notification_subscriptions, only: [:create]
resource :notification_subscriptions, only: [:create, :destroy]
namespace :widget do
resource :config, only: [:create]
@@ -295,6 +296,8 @@ Rails.application.routes.draw do
namespace :super_admin do
root to: 'dashboard#index'
resource :app_config, only: [:show, :create]
# order of resources affect the order of sidebar navigation in super admin
resources :accounts
resources :users, only: [:index, :new, :create, :show, :edit, :update]

View File

@@ -0,0 +1,14 @@
class CreateAutomationRules < ActiveRecord::Migration[6.1]
def change
create_table :automation_rules do |t|
t.bigint :account_id, null: false
t.string :name, null: false
t.text :description
t.string :event_name, null: false
t.jsonb :conditions, null: false, default: '{}'
t.jsonb :actions, null: false, default: '{}'
t.timestamps
t.index :account_id, name: 'index_automation_rules_on_account_id'
end
end
end

View File

@@ -0,0 +1,16 @@
class AddAllowMessagesAfterResolvedToInbox < ActiveRecord::Migration[6.1]
def change
add_column :inboxes, :allow_messages_after_resolved, :boolean, default: true
update_csat_enabled_inboxes
end
def update_csat_enabled_inboxes
::Inbox.where(channel_type: 'Channel::WebWidget', csat_survey_enabled: true).find_in_batches do |inboxes_batch|
inboxes_batch.each do |inbox|
inbox.allow_messages_after_resolved = false
inbox.save!
end
end
end
end

View File

@@ -0,0 +1,8 @@
class UpdateNilContactAttributesToEmptyHash < ActiveRecord::Migration[6.1]
def change
# rubocop:disable Rails/SkipsModelValidations
Contact.where(custom_attributes: nil).update_all(custom_attributes: {})
Contact.where(additional_attributes: nil).update_all(additional_attributes: {})
# rubocop:enable Rails/SkipsModelValidations
end
end

View File

@@ -0,0 +1,5 @@
class RemovePlatformAppPermissibleAccessTokens < ActiveRecord::Migration[6.1]
def change
AccessToken.where(owner_type: 'PlatformAppPermissible').destroy_all
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_21_125545) do
ActiveRecord::Schema.define(version: 2022_01_11_223630) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -124,6 +124,18 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do
t.string "extension"
end
create_table "automation_rules", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", null: false
t.text "description"
t.string "event_name", null: false
t.jsonb "conditions", default: "{}", null: false
t.jsonb "actions", default: "{}", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_automation_rules_on_account_id"
end
create_table "campaigns", force: :cascade do |t|
t.integer "display_id", null: false
t.string "title", null: false
@@ -449,6 +461,7 @@ ActiveRecord::Schema.define(version: 2021_12_21_125545) do
t.string "timezone", default: "UTC"
t.boolean "enable_email_collect", default: true
t.boolean "csat_survey_enabled", default: false
t.boolean "allow_messages_after_resolved", default: true
t.index ["account_id"], name: "index_inboxes_on_account_id"
end

View File

@@ -0,0 +1,160 @@
{
"conversations": {
"status": {
"attribute_name": "Status",
"input_type": "multi_select",
"table_name": "conversations",
"filter_operators": [ "equal_to", "not_equal_to" ],
"attribute_type": "standard"
},
"assignee_id": {
"attribute_name": "Assignee Name",
"input_type": "search_box with name tags/plain text",
"table_name": "conversations",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"contact_id": {
"attribute_name": "Contact Name",
"input_type": "plain_text",
"table_name": "conversations",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"inbox_id": {
"attribute_name": "Inbox Name",
"input_type": "search_box",
"table_name": "conversations",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"team_id": {
"attribute_name": "Team Name",
"input_type": "search_box",
"table_name": "conversations",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"id": {
"attribute_name": "Conversation Identifier",
"input_type": "textbox",
"table_name": "conversations",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"campaign_id": {
"attribute_name": "Campaign Name",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
"data_type": "text",
"filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ],
"attribute_type": "standard"
},
"browser_language": {
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
"country_code": {
"attribute_name": "Country Name",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"referer": {
"attribute_name": "Referer link",
"input_type": "textbox",
"data_type": "link",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"plan": {
"attribute_name": "Plan",
"input_type": "multi_select",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
}
},
"contacts": {
"assignee_id": {
"attribute_name": "Assignee Name",
"input_type": "search_box with name tags/plain text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"contact_id": {
"attribute_name": "Contact Name",
"input_type": "plain_text",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"inbox_id": {
"attribute_name": "Inbox Name",
"input_type": "search_box",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"team_id": {
"attribute_name": "Team Name",
"input_type": "search_box",
"data_type": "number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"id": {
"attribute_name": "Conversation Identifier",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"campaign_id": {
"attribute_name": "Campaign Name",
"input_type": "textbox",
"data_type": "Number",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "is_present", "is_not_present" ],
"attribute_type": "standard"
},
"labels": {
"attribute_name": "Labels",
"input_type": "tags",
"data_type": "text",
"filter_operators": ["exactly_equal_to", "contains", "does_not_contain" ],
"attribute_type": "standard"
},
"browser_language": {
"attribute_name": "Browser Language",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
"attribute_type": "additional_attributes"
},
"country_code": {
"attribute_name": "Country Name",
"input_type": "textbox",
"data_type": "text",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
},
"referer": {
"attribute_name": "Referer link",
"input_type": "textbox",
"data_type": "link",
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain", "present", "is_not_present" ],
"attribute_type": "additional_attributes"
}
}
}

View File

@@ -1,7 +1,7 @@
class GlobalConfigService
def self.load(config_key, default_value)
config = GlobalConfig.get(config_key)
return config[config_key] if config[config_key].present?
config = ENV[config_key] || GlobalConfig.get(config_key)[config_key]
return config if config.present?
# To support migrating existing instance relying on env variables
# TODO: deprecate this later down the line

View File

@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "2.0.2",
"version": "2.1.0",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue} --fix",
@@ -42,7 +42,7 @@
"lodash.groupby": "^4.6.0",
"marked": "2.0.3",
"md5": "^2.3.0",
"ninja-keys": "https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902",
"ninja-keys": "^1.1.9",
"posthog-js": "^1.13.7",
"prosemirror-markdown": "1.5.1",
"prosemirror-state": "1.3.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,118 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::AutomationRulesController', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, inbox_id: inbox.id, contact_id: contact.id) }
describe 'GET /api/v1/accounts/{account.id}/automation_rules' do
context 'when it is an authenticated user' do
it 'returns all records' do
automation_rule = create(:automation_rule, account: account, name: 'Test Automation Rule')
get "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:data].first[:id]).to eq(automation_rule.id)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/automation_rules"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/automation_rules' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/automation_rules"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) do
{
name: 'Notify Conversation Created and mark priority query',
description: 'Notify all administrator about conversation created and mark priority query',
event_name: 'conversation_created',
conditions: [
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['en'],
query_operator: 'AND'
},
{
attribute_key: 'country_code',
filter_operator: 'equal_to',
values: %w[USA UK],
query_operator: nil
}
],
actions: [
{
action_name: :send_message,
action_params: ['Welcome to the chatwoot platform.']
},
{
action_name: :assign_team,
action_params: [1]
},
{
action_name: :add_label,
action_params: %w[support priority_customer]
},
{
action_name: :assign_best_administrator,
action_params: [1]
},
{
action_name: :update_additional_attributes,
action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
}
]
}.with_indifferent_access
end
it 'Saves for automation_rules for account with country_code and browser_language conditions' do
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
end
it 'Saves for automation_rules for account with status conditions' do
params[:conditions] = [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: ['resolved'],
query_operator: nil
}.with_indifferent_access
]
expect(account.automation_rules.count).to eq(0)
post "/api/v1/accounts/#{account.id}/automation_rules",
headers: administrator.create_new_auth_token,
params: params
expect(response).to have_http_status(:success)
expect(account.automation_rules.count).to eq(1)
end
end
end
end

View File

@@ -308,6 +308,17 @@ RSpec.describe 'Inboxes API', type: :request do
expect(response.body).to include('Line Inbox')
expect(response.body).to include('callback_webhook_url')
end
it 'creates the webwidget inbox that allow messages after conversation is resolved' do
post "/api/v1/accounts/#{account.id}/inboxes",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['allow_messages_after_resolved']).to be true
end
end
end
@@ -467,6 +478,16 @@ RSpec.describe 'Inboxes API', type: :request do
inbox.reload
expect(inbox.reload.weekly_schedule.find { |schedule| schedule['day_of_week'] == 0 }['open_hour']).to eq 9
end
it 'updates the webwidget inbox to disallow the messages after conversation is resolved' do
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
headers: admin.create_new_auth_token,
params: valid_params.merge({ allow_messages_after_resolved: false }),
as: :json
expect(response).to have_http_status(:success)
expect(inbox.reload.allow_messages_after_resolved).to be_falsey
end
end
end

View File

@@ -5,11 +5,6 @@ RSpec.describe 'Accounts API', type: :request do
let(:email) { Faker::Internet.email }
let(:user_full_name) { Faker::Name.name_with_middle }
before do
# to clear redis cache
GlobalConfig.clear_cache
end
context 'when posting to accounts with correct parameters' do
let(:account_builder) { double }
let(:account) { create(:account) }
@@ -20,56 +15,62 @@ RSpec.describe 'Accounts API', type: :request do
end
it 'calls account builder' do
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'nil')
allow(account_builder).to receive(:perform).and_return([user, account])
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return([user, account])
params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!' }
params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!' }
post api_v1_accounts_url,
params: params,
as: :json
post api_v1_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
end
end
it 'renders error response on invalid params' do
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'nil')
allow(account_builder).to receive(:perform).and_return(nil)
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
allow(account_builder).to receive(:perform).and_return(nil)
params = { account_name: nil, email: nil, user: nil, user_full_name: nil }
params = { account_name: nil, email: nil, user: nil, user_full_name: nil }
post api_v1_accounts_url,
params: params,
as: :json
post api_v1_accounts_url,
params: params,
as: :json
expect(AccountBuilder).to have_received(:new).with(params.merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json)
expect(AccountBuilder).to have_received(:new).with(params.merge(user_password: params[:password]))
expect(account_builder).to have_received(:perform)
expect(response).to have_http_status(:forbidden)
expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json)
end
end
end
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do
it 'responds 404 on requests' do
params = { account_name: 'test', email: email, user_full_name: user_full_name, password: 'Password1!' }
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false')
post api_v1_accounts_url,
params: params,
as: :json
expect(response).to have_http_status(:not_found)
params = { account_name: 'test', email: email, user_full_name: user_full_name }
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false' do
post api_v1_accounts_url,
params: params,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do
it 'does not respond 404 on requests' do
params = { account_name: 'test', email: email, user_full_name: user_full_name, password: 'Password1!' }
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'api_only')
post api_v1_accounts_url,
params: params,
as: :json
expect(response).to have_http_status(:success)
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'api_only' do
post api_v1_accounts_url,
params: params,
as: :json
expect(response).to have_http_status(:success)
end
end
end
end

View File

@@ -80,4 +80,32 @@ RSpec.describe 'Notifications Subscriptions API', type: :request do
end
end
end
describe 'DELETE /api/v1/notification_subscriptions' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete '/api/v1/notification_subscriptions'
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'delete existing notification subscription if subscription exists' do
subscription = create(:notification_subscription, subscription_type: 'fcm', subscription_attributes: { push_token: 'bUvZo8AYGGmCMr' },
user: agent)
delete '/api/v1/notification_subscriptions',
params: {
push_token: subscription.subscription_attributes['push_token']
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect { subscription.reload }.to raise_exception(ActiveRecord::RecordNotFound)
end
end
end
end

View File

@@ -0,0 +1,47 @@
require 'rails_helper'
RSpec.describe 'Super Admin Application Config API', type: :request do
let(:super_admin) { create(:super_admin) }
describe 'GET /super_admin/app_config' do
context 'when it is an unauthenticated super admin' do
it 'returns unauthorized' do
get '/super_admin/app_config'
expect(response).to have_http_status(:redirect)
end
end
context 'when it is an authenticated super admin' do
let!(:config) { create(:installation_config, { name: 'FB_APP_ID', value: 'TESTVALUE' }) }
it 'shows the app_config page' do
sign_in super_admin
get '/super_admin/app_config'
expect(response).to have_http_status(:success)
expect(response.body).to include(config.name)
end
end
end
describe 'POST /super_admin/app_config' do
context 'when it is an unauthenticated super admin' do
it 'returns unauthorized' do
post '/super_admin/app_config', params: { app_config: { TESTKEY: 'TESTVALUE' } }
expect(response).to have_http_status(:redirect)
end
end
context 'when it is an aunthenticated super admin' do
it 'shows the app_config page' do
sign_in super_admin
post '/super_admin/app_config', params: { app_config: { TESTKEY: 'TESTVALUE' } }
expect(response.status).to eq(302)
expect(response).should redirect_to(super_admin_app_config_path)
config = GlobalConfig.get('TESTKEY')
expect(config['TESTKEY']).to eq('TESTVALUE')
end
end
end
end

View File

@@ -0,0 +1,19 @@
FactoryBot.define do
factory :automation_rule do
account
event_name { 'conversation_status_changed' }
conditions { [{ 'values': ['resolved'], 'attribute_key': 'status', 'query_operator': nil, 'filter_operator': 'equal_to' }] }
actions do
[
{
'action_name' => 'send_email_to_team', 'action_params' => {
'message' => 'Please pay attention to this conversation, its from high priority customer', 'team_ids' => [1]
}
},
{ 'action_name' => 'assign_team', 'action_params' => [1] },
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
{ 'action_name' => 'assign_best_agents', 'action_params' => [1, 2, 3, 4] }
]
end
end
end

View File

@@ -10,35 +10,34 @@ describe GlobalConfigService do
GlobalConfig.clear_cache
end
it 'set default value if not found on db nor env var' do
value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq nil
# it 'set default value if not found on db nor env var' do
# value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
# expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq nil
described_class.load('ENABLE_ACCOUNT_SIGNUP', 'true')
# described_class.load('ENABLE_ACCOUNT_SIGNUP', 'true')
value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq 'true'
expect(InstallationConfig.find_by(name: 'ENABLE_ACCOUNT_SIGNUP')&.value).to eq 'true'
end
# value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
# expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq 'true'
# expect(InstallationConfig.find_by(name: 'ENABLE_ACCOUNT_SIGNUP')&.value).to eq 'true'
# end
it 'get value from env variable if not found on DB' do
it 'get value from env variable even if present on DB' do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'false' do
expect(InstallationConfig.find_by(name: 'ENABLE_ACCOUNT_SIGNUP')&.value).to eq nil
described_class.load('ENABLE_ACCOUNT_SIGNUP', 'true')
value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq 'false'
value = described_class.load('ENABLE_ACCOUNT_SIGNUP', 'true')
expect(value).to eq 'false'
end
end
it 'get value from DB if found' do
# Set a value in db first and make sure this value
# is not respected even when load() method is called with
# another value.
InstallationConfig.where(name: 'ENABLE_ACCOUNT_SIGNUP').first_or_create(value: 'true')
described_class.load('ENABLE_ACCOUNT_SIGNUP', 'false')
value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq 'true'
end
# it 'get value from DB if found' do
# # Set a value in db first and make sure this value
# # is not respected even when load() method is called with
# # another value.
# InstallationConfig.where(name: 'ENABLE_ACCOUNT_SIGNUP').first_or_create(value: 'true')
# described_class.load('ENABLE_ACCOUNT_SIGNUP', 'false')
# value = GlobalConfig.get('ENABLE_ACCOUNT_SIGNUP')
# expect(value['ENABLE_ACCOUNT_SIGNUP']).to eq 'true'
# end
end
end
end

View File

@@ -0,0 +1,72 @@
require 'rails_helper'
describe AutomationRuleListener do
let(:listener) { described_class.instance }
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, account: account, identifier: '123') }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
let(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: inbox, account: account) }
let(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
let(:team) { create(:team, account: account) }
let(:user_1) { create(:user, role: 0) }
let(:user_2) { create(:user, role: 0) }
let!(:event) do
Events::Base.new('conversation_status_changed', Time.zone.now, { conversation: conversation })
end
before do
create(:team_member, user: user_1, team: team)
create(:team_member, user: user_2, team: team)
create(:account_user, user: user_2, account: account)
create(:account_user, user: user_1, account: account)
conversation.resolved!
automation_rule.update!(actions:
[
{
'action_name' => 'send_email_to_team', 'action_params' => {
'message' => 'Please pay attention to this conversation, its from high priority customer',
'team_ids' => [team.id]
}
},
{ 'action_name' => 'assign_team', 'action_params' => [team.id] },
{ 'action_name' => 'add_label', 'action_params' => %w[support priority_customer] },
{ 'action_name' => 'assign_best_agents', 'action_params' => [user_1.id] }
])
end
describe '#conversation_status_changed' do
context 'when rule matches' do
it 'triggers automation rule to assign team' do
expect(conversation.team_id).not_to eq(team.id)
automation_rule
listener.conversation_status_changed(event)
conversation.reload
expect(conversation.team_id).to eq(team.id)
end
it 'triggers automation rule to add label' do
expect(conversation.labels).to eq([])
automation_rule
listener.conversation_status_changed(event)
conversation.reload
expect(conversation.labels.pluck(:name)).to eq(%w[support priority_customer])
end
it 'triggers automation rule to assign best agents' do
expect(conversation.assignee).to be_nil
automation_rule
listener.conversation_status_changed(event)
conversation.reload
expect(conversation.assignee).to eq(user_1)
end
end
end
end

View File

@@ -0,0 +1,54 @@
require 'rails_helper'
RSpec.describe AutomationRule, type: :model do
describe 'associations' do
let(:params) do
{
name: 'Notify Conversation Created and mark priority query',
description: 'Notify all administrator about conversation created and mark priority query',
event_name: 'conversation_created',
conditions: [
{
attribute_key: 'browser_language',
filter_operator: 'equal_to',
values: ['en'],
query_operator: 'AND'
},
{
attribute_key: 'country_code',
filter_operator: 'equal_to',
values: %w[USA UK],
query_operator: nil
}
],
actions: [
{
action_name: :send_message,
action_params: ['Welcome to the chatwoot platform.']
},
{
action_name: :assign_team,
action_params: [1]
},
{
action_name: :add_label,
action_params: %w[support priority_customer]
},
{
action_name: :assign_best_administrator,
action_params: [1]
},
{
action_name: :update_additional_attributes,
action_params: [{ intiated_at: '2021-12-03 17:25:26.844536 +0530' }]
}
]
}.with_indifferent_access
end
it 'returns valid record' do
rule = FactoryBot.build(:automation_rule, params)
expect(rule.valid?).to eq true
end
end
end

View File

@@ -11,4 +11,36 @@ RSpec.describe Contact do
it { is_expected.to belong_to(:account) }
it { is_expected.to have_many(:conversations).dependent(:destroy_async) }
end
context 'pepare contact attributes before validation' do
it 'sets email to lowercase' do
contact = create(:contact, email: 'Test@test.com')
expect(contact.email).to eq('test@test.com')
end
it 'sets email to nil when empty string' do
contact = create(:contact, email: '')
expect(contact.email).to be_nil
end
it 'sets custom_attributes to {} when nil' do
contact = create(:contact, custom_attributes: nil)
expect(contact.custom_attributes).to eq({})
end
it 'sets custom_attributes to {} when empty string' do
contact = create(:contact, custom_attributes: '')
expect(contact.custom_attributes).to eq({})
end
it 'sets additional_attributes to {} when nil' do
contact = create(:contact, additional_attributes: nil)
expect(contact.additional_attributes).to eq({})
end
it 'sets additional_attributes to {} when empty string' do
contact = create(:contact, additional_attributes: '')
expect(contact.additional_attributes).to eq({})
end
end
end

View File

@@ -0,0 +1,55 @@
tags:
- Contact
operationId: contactFilter
description: Filter contacts with custom filter options and pagination
summary: Contact Filter
security:
- userApiKey: []
- agentBotApiKey: []
parameters:
- name: page
in: query
type: integer
- name: payload
in: body
required: true
schema:
type: array
items:
type: object
properties:
attribute_key:
type: string
description: filter attribute name
filter_operator:
type: string
description: filter operator name
enum: [ equal_to, not_equal_to, contains, does_not_contain ]
values:
type: array
items:
type: string
description: array of the attribute values to filter
query_operator:
type: string
description: query operator name
enum: [ AND, OR ]
example:
- attribute_key: 'name'
filter_operator: 'equal_to'
values: ['en']
query_operator: 'AND'
- attribute_key: 'country_code'
filter_operator: 'equal_to'
values: ['us']
query_operator: nil
responses:
200:
description: Success
schema:
$ref: '#/definitions/contact_list'
400:
description: Bad Request Error
schema:
$ref: '#/definitions/bad_request_error'

View File

@@ -24,6 +24,7 @@ parameters:
filter_operator:
type: string
description: filter operator name
enum: [ equal_to, not_equal_to, contains, does_not_contain ]
values:
type: array
items:
@@ -32,6 +33,17 @@ parameters:
query_operator:
type: string
description: query operator name
enum: [ AND, OR ]
example:
- attribute_key: 'browser_language'
filter_operator: 'not_eq'
values: ['en']
query_operator: 'AND'
- attribute_key: 'status'
filter_operator: 'eq'
values: ['pending']
query_operator: nil
responses:
200:
description: Success

View File

@@ -164,6 +164,11 @@
$ref: ./application/contacts/conversations.yml
/api/v1/accounts/{account_id}/contacts/search:
$ref: ./application/contacts/search.yml
/api/v1/accounts/{account_id}/contacts/filter:
parameters:
- $ref: '#/parameters/account_id'
post:
$ref: ./application/contacts/filter.yml
/api/v1/accounts/{account_id}/contacts/{id}/contact_inboxes:
$ref: ./application/contact_inboxes/create.yml
/api/v1/accounts/{account_id}/contacts/{id}/contactable_inboxes:

View File

@@ -1571,6 +1571,114 @@
}
}
},
"/api/v1/accounts/{account_id}/contacts/filter": {
"parameters": [
{
"$ref": "#/parameters/account_id"
}
],
"post": {
"tags": [
"Contact"
],
"operationId": "contactFilter",
"description": "Filter contacts with custom filter options and pagination",
"summary": "Contact Filter",
"security": [
{
"userApiKey": [
]
},
{
"agentBotApiKey": [
]
}
],
"parameters": [
{
"name": "page",
"in": "query",
"type": "integer"
},
{
"name": "payload",
"in": "body",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"attribute_key": {
"type": "string",
"description": "filter attribute name"
},
"filter_operator": {
"type": "string",
"description": "filter operator name",
"enum": [
"equal_to",
"not_equal_to",
"contains",
"does_not_contain"
]
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "array of the attribute values to filter"
},
"query_operator": {
"type": "string",
"description": "query operator name",
"enum": [
"AND",
"OR"
]
}
}
},
"example": [
{
"attribute_key": "name",
"filter_operator": "equal_to",
"values": [
"en"
],
"query_operator": "AND"
},
{
"attribute_key": "country_code",
"filter_operator": "equal_to",
"values": [
"us"
],
"query_operator": "nil"
}
]
}
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"$ref": "#/definitions/contact_list"
}
},
"400": {
"description": "Bad Request Error",
"schema": {
"$ref": "#/definitions/bad_request_error"
}
}
}
}
},
"/api/v1/accounts/{account_id}/contacts/{id}/contact_inboxes": {
"post": {
"tags": [
@@ -1879,7 +1987,13 @@
},
"filter_operator": {
"type": "string",
"description": "filter operator name"
"description": "filter operator name",
"enum": [
"equal_to",
"not_equal_to",
"contains",
"does_not_contain"
]
},
"values": {
"type": "array",
@@ -1890,10 +2004,32 @@
},
"query_operator": {
"type": "string",
"description": "query operator name"
"description": "query operator name",
"enum": [
"AND",
"OR"
]
}
}
}
},
"example": [
{
"attribute_key": "browser_language",
"filter_operator": "not_eq",
"values": [
"en"
],
"query_operator": "AND"
},
{
"attribute_key": "status",
"filter_operator": "eq",
"values": [
"pending"
],
"query_operator": "nil"
}
]
}
}
],

View File

@@ -1511,10 +1511,10 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@lit/reactive-element@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.1.tgz#853cacd4d78d79059f33f66f8e7b0e5c34bee294"
integrity sha512-nSD5AA2AZkKuXuvGs8IK7K5ZczLAogfDd26zT9l6S7WzvqALdVWcW5vMUiTnZyj5SPcNwNNANj0koeV1ieqTFQ==
"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.1.1.tgz#523b29e529e881fce47bab764ea1b8058fd45796"
integrity sha512-B2JdRMwCGv+VpIRj3CYVQBx3muPDeE8y+HPgWqzrAHsO5/40BpwDFZeplIV790BaTqDVUDvZOKMSbuFM9zWC0w==
"@material/mwc-icon@0.25.3":
version "0.25.3"
@@ -9505,22 +9505,22 @@ listr@^0.14.3:
p-map "^2.0.0"
rxjs "^6.3.3"
lit-element@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.1.tgz#3c545af17d8a46268bc1dd5623a47486e6ff76f4"
integrity sha512-vs9uybH9ORyK49CFjoNGN85HM9h5bmisU4TQ63phe/+GYlwvY/3SIFYKdjV6xNvzz8v2MnVC+9+QOkPqh+Q3Ew==
lit-element@^3.0.0, lit-element@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.1.1.tgz#562d5ccbc8ba0c01d8ba4a0ac3576263167d2ccb"
integrity sha512-14ClnMAU8EXnzC+M2/KDd3SFmNUn1QUw1+GxWkEMwGV3iaH8ObunMlO5svzvaWlkSV0WlxJCi40NGnDVJ2XZKQ==
dependencies:
"@lit/reactive-element" "^1.0.0"
lit-html "^2.0.0"
"@lit/reactive-element" "^1.1.0"
lit-html "^2.1.0"
lit-html@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.1.tgz#63241015efa07bc9259b6f96f04abd052d2a1f95"
integrity sha512-KF5znvFdXbxTYM/GjpdOOnMsjgRcFGusTnB54ixnCTya5zUR0XqrDRj29ybuLS+jLXv1jji6Y8+g4W7WP8uL4w==
lit-html@^2.0.0, lit-html@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.1.1.tgz#f4da485798a0d967514d31730d387350fafb79f7"
integrity sha512-E4BImK6lopAYanJpvcGaAG8kQFF1ccIulPu2BRNZI7acFB6i4ujjjsnaPVFT1j/4lD9r8GKih0Y8d7/LH8SeyQ==
dependencies:
"@types/trusted-types" "^2.0.2"
lit@2.0.2, lit@^2.0.0:
lit@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
@@ -9529,6 +9529,15 @@ lit@2.0.2, lit@^2.0.0:
lit-element "^3.0.0"
lit-html "^2.0.0"
lit@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/lit/-/lit-2.1.1.tgz#65f43abca945988f696391f762c645ba51966b0b"
integrity sha512-yqDqf36IhXwOxIQSFqCMgpfvDCRdxLCLZl7m/+tO5C9W/OBHUj17qZpiMBT35v97QMVKcKEi1KZ3hZRyTwBNsQ==
dependencies:
"@lit/reactive-element" "^1.1.0"
lit-element "^3.1.0"
lit-html "^2.1.0"
load-json-file@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
@@ -10250,9 +10259,10 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
"ninja-keys@https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902":
version "1.1.6"
resolved "https://github.com/chatwoot/ninja-keys.git#b4c3233f676780af90c607866fa85e404c835902"
ninja-keys@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/ninja-keys/-/ninja-keys-1.1.9.tgz#0251c8635f5cb019f5efd667b69b12c293c5cd26"
integrity sha512-XJabKFQIQ2pivNSKA3I/6hm9S7yPhJ2xXDFfWZFOPtDTBOv5xCaqOczrlIaESXPMgjk3xL0izFWUDjSn8ByVvw==
dependencies:
"@material/mwc-icon" "0.25.3"
hotkeys-js "3.8.7"