Merge branch 'release/2.1.0'
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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* © 2017-2021, Chatwoot Inc - Released under the MIT License.
|
||||
*Chatwoot* © 2017-2022, Chatwoot Inc - Released under the MIT License.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
21
app/controllers/super_admin/app_configs_controller.rb
Normal file
21
app/controllers/super_admin/app_configs_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,8 @@ class AsyncDispatcher < BaseDispatcher
|
||||
HookListener.instance,
|
||||
InstallationWebhookListener.instance,
|
||||
NotificationListener.instance,
|
||||
WebhookListener.instance
|
||||
WebhookListener.instance,
|
||||
AutomationRuleListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:no-auto-load-md-icons="true"
|
||||
hideBreadcrumbs
|
||||
:placeholder="placeholder"
|
||||
@selected="setCommandbarData"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
49
app/javascript/shared/components/FluentIcon/Icon.vue
Normal file
49
app/javascript/shared/components/FluentIcon/Icon.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
29
app/listeners/automation_rule_listener.rb
Normal file
29
app/listeners/automation_rule_listener.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
44
app/models/automation_rule.rb
Normal file
44
app/models/automation_rule.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
9
app/policies/automation_rule_policy.rb
Normal file
9
app/policies/automation_rule_policy.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class AutomationRulePolicy < ApplicationPolicy
|
||||
def index?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def create?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
63
app/services/automation_rules/action_service.rb
Normal file
63
app/services/automation_rules/action_service.rb
Normal 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
|
||||
45
app/services/automation_rules/conditions_filter_service.rb
Normal file
45
app/services/automation_rules/conditions_filter_service.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! 'api/v1/accounts/automation_rules/partials/automation_rule.json.jbuilder', automation_rule: @automation_rule
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<%= image_tag field.avatar_url %>
|
||||
<%= image_tag field.avatar_url if field.avatar_url.present? %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<%= image_tag field.avatar_url %>
|
||||
<%= image_tag field.avatar_url if field.avatar_url.present? %>
|
||||
|
||||
@@ -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>
|
||||
25
app/views/super_admin/app_configs/show.html.erb
Normal file
25
app/views/super_admin/app_configs/show.html.erb
Normal 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>
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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)) %>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '2.0.2'
|
||||
version: '2.1.0'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
14
db/migrate/20211110101046_create_automation_rules.rb
Normal file
14
db/migrate/20211110101046_create_automation_rules.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemovePlatformAppPermissibleAccessTokens < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
AccessToken.where(owner_type: 'PlatformAppPermissible').destroy_all
|
||||
end
|
||||
end
|
||||
15
db/schema.rb
15
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 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
|
||||
|
||||
|
||||
160
lib/automation_rules/conditions.json
Normal file
160
lib/automation_rules/conditions.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
47
spec/controllers/super_admin/app_config_controller_spec.rb
Normal file
47
spec/controllers/super_admin/app_config_controller_spec.rb
Normal 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
|
||||
19
spec/factories/automation_rules.rb
Normal file
19
spec/factories/automation_rules.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
72
spec/listeners/automation_rule_listener_spec.rb
Normal file
72
spec/listeners/automation_rule_listener_spec.rb
Normal 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
|
||||
54
spec/models/automation_rule_spec.rb
Normal file
54
spec/models/automation_rule_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
55
swagger/paths/application/contacts/filter.yml
Normal file
55
swagger/paths/application/contacts/filter.yml
Normal 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'
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
46
yarn.lock
46
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user