Merge branch 'develop' into chore/conversation-participants
This commit is contained in:
commit
cce18ed536
141 changed files with 2907 additions and 509 deletions
|
@ -54,3 +54,5 @@ exclude_patterns:
|
||||||
- 'app/javascript/widget/i18n/index.js'
|
- 'app/javascript/widget/i18n/index.js'
|
||||||
- 'app/javascript/survey/i18n/index.js'
|
- 'app/javascript/survey/i18n/index.js'
|
||||||
- 'app/javascript/shared/constants/locales.js'
|
- 'app/javascript/shared/constants/locales.js'
|
||||||
|
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
|
||||||
|
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
|
||||||
|
|
|
@ -34,6 +34,11 @@ REDIS_SENTINELS=
|
||||||
# You can find list of master using "SENTINEL masters" command
|
# You can find list of master using "SENTINEL masters" command
|
||||||
REDIS_SENTINEL_MASTER_NAME=
|
REDIS_SENTINEL_MASTER_NAME=
|
||||||
|
|
||||||
|
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
||||||
|
# Use the following environment variable to customize passwords for sentinels.
|
||||||
|
# Use empty string if sentinels are configured with out passwords
|
||||||
|
# REDIS_SENTINEL_PASSWORD=
|
||||||
|
|
||||||
# Redis premium breakage in heroku fix
|
# Redis premium breakage in heroku fix
|
||||||
# enable the following configuration
|
# enable the following configuration
|
||||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||||
|
|
|
@ -35,7 +35,13 @@ class Messages::MessageBuilder
|
||||||
file: uploaded_attachment
|
file: uploaded_attachment
|
||||||
)
|
)
|
||||||
|
|
||||||
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
attachment.file_type = if uploaded_attachment.is_a?(String)
|
||||||
|
file_type_by_signed_id(
|
||||||
|
uploaded_attachment
|
||||||
|
)
|
||||||
|
else
|
||||||
|
file_type(uploaded_attachment&.content_type)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@articles_count = @portal.articles.count
|
@portal_articles = @portal.articles
|
||||||
@articles = @portal.articles
|
@all_articles = @portal_articles.search(list_params)
|
||||||
@articles = @articles.search(list_params) if list_params.present?
|
@articles_count = @all_articles.count
|
||||||
|
@articles = @all_articles.page(@current_page)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -37,7 +38,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
def portal
|
||||||
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def article_params
|
def article_params
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@current_locale = params[:locale]
|
||||||
@categories = @portal.categories.search(params)
|
@categories = @portal.categories.search(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
||||||
|
|
||||||
@macro.save!
|
@macro.save!
|
||||||
|
process_attachments
|
||||||
|
@macro
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_file
|
||||||
|
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
key: nil,
|
||||||
|
io: params[:attachment].tempfile,
|
||||||
|
filename: params[:attachment].original_filename,
|
||||||
|
content_type: params[:attachment].content_type
|
||||||
|
)
|
||||||
|
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@macro.update!(macros_with_user)
|
@macro.update!(macros_with_user)
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
@macro.set_visibility(current_user, permitted_params)
|
||||||
|
process_attachments
|
||||||
@macro.save!
|
@macro.save!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
|
@ -42,6 +55,17 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_attachments
|
||||||
|
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||||
|
return if actions.blank?
|
||||||
|
|
||||||
|
actions.each do |action|
|
||||||
|
blob_id = action['action_params']
|
||||||
|
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||||
|
@macro.files.attach(blob)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :account_id, :visibility,
|
:name, :account_id, :visibility,
|
||||||
|
|
|
@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
@portal.members << agents
|
@portal.members << agents
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@all_articles = @portal.articles
|
||||||
|
@articles = @all_articles.search(locale: params[:locale])
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@portal = Current.account.portals.build(portal_params)
|
@portal = Current.account.portals.build(portal_params)
|
||||||
|
|
|
@ -3,16 +3,12 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||||
@resource = Account.new(account_params)
|
@resource = Account.new(account_params)
|
||||||
@resource.save!
|
@resource.save!
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
render json: @resource
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show; end
|
||||||
render json: @resource
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.update!(account_params)
|
@resource.update!(account_params)
|
||||||
render json: @resource
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -27,6 +23,14 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:name, :locale)
|
if permitted_params[:enabled_features]
|
||||||
|
return permitted_params.except(:enabled_features).merge(selected_feature_flags: permitted_params[:enabled_features].map(&:to_sym))
|
||||||
|
end
|
||||||
|
|
||||||
|
permitted_params
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:name, :locale, enabled_features: [], limits: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :set_category
|
before_action :set_category, except: [:index]
|
||||||
before_action :set_article, only: [:show]
|
before_action :set_article, only: [:show]
|
||||||
layout 'portal'
|
layout 'portal'
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(slug: params[:category_slug])
|
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
def portal
|
||||||
|
|
|
@ -8,6 +8,12 @@ module FileTypeHelper
|
||||||
:file
|
:file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Used in case of DIRECT_UPLOADS_ENABLED=true
|
||||||
|
def file_type_by_signed_id(signed_id)
|
||||||
|
blob = ActiveStorage::Blob.find_signed(signed_id)
|
||||||
|
file_type(blob&.content_type)
|
||||||
|
end
|
||||||
|
|
||||||
def image_file?(content_type)
|
def image_file?(content_type)
|
||||||
[
|
[
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
|
|
|
@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI {
|
||||||
super('categories', { accountScoped: true });
|
super('categories', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ portalSlug }) {
|
get({ portalSlug, locale }) {
|
||||||
return axios.get(`${this.url}/${portalSlug}/categories`);
|
return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
create({ portalSlug, categoryObj }) {
|
create({ portalSlug, categoryObj }) {
|
||||||
|
|
|
@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient {
|
||||||
super('portals', { accountScoped: true });
|
super('portals', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPortal({ portalSlug, locale }) {
|
||||||
|
return axios.get(`${this.url}/${portalSlug}?locale=${locale}`);
|
||||||
|
}
|
||||||
|
|
||||||
updatePortal({ portalSlug, portalObj }) {
|
updatePortal({ portalSlug, portalObj }) {
|
||||||
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,12 @@ import Button from './ui/WootButton';
|
||||||
import Code from './Code';
|
import Code from './Code';
|
||||||
import ColorPicker from './widgets/ColorPicker';
|
import ColorPicker from './widgets/ColorPicker';
|
||||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||||
|
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||||
|
import ContextMenu from './ui/ContextMenu.vue';
|
||||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
|
import FeatureToggle from './widgets/FeatureToggle';
|
||||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||||
import Input from './widgets/forms/Input.vue';
|
import Input from './widgets/forms/Input.vue';
|
||||||
import Label from './ui/Label';
|
import Label from './ui/Label';
|
||||||
|
@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton';
|
||||||
import Tabs from './ui/Tabs/Tabs';
|
import Tabs from './ui/Tabs/Tabs';
|
||||||
import TabsItem from './ui/Tabs/TabsItem';
|
import TabsItem from './ui/Tabs/TabsItem';
|
||||||
import Thumbnail from './widgets/Thumbnail.vue';
|
import Thumbnail from './widgets/Thumbnail.vue';
|
||||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
|
||||||
import ContextMenu from './ui/ContextMenu.vue';
|
|
||||||
|
|
||||||
const WootUIKit = {
|
const WootUIKit = {
|
||||||
AvatarUploader,
|
AvatarUploader,
|
||||||
|
@ -31,9 +32,12 @@ const WootUIKit = {
|
||||||
Code,
|
Code,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
ConfirmDeleteModal,
|
ConfirmDeleteModal,
|
||||||
|
ConfirmModal,
|
||||||
|
ContextMenu,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
FeatureToggle,
|
||||||
HorizontalBar,
|
HorizontalBar,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
|
@ -47,8 +51,6 @@ const WootUIKit = {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsItem,
|
TabsItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
ConfirmModal,
|
|
||||||
ContextMenu,
|
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
const keys = Object.keys(this);
|
const keys = Object.keys(this);
|
||||||
keys.pop(); // remove 'install' from keys
|
keys.pop(); // remove 'install' from keys
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
:class="{ 'text-truncate': shouldTruncate }"
|
:class="{ 'text-truncate': shouldTruncate }"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view">
|
<span v-if="showChildCount" class="count-view">
|
||||||
{{ childItemCount }}
|
{{ childItemCount }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -76,7 +76,7 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
isHelpCenterSidebar: {
|
showChildCount: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -127,11 +127,16 @@ $label-badge-size: var(--space-slab);
|
||||||
color: var(--w-500);
|
color: var(--w-500);
|
||||||
border-color: var(--w-25);
|
border-color: var(--w-25);
|
||||||
}
|
}
|
||||||
|
&.is-active .count-view {
|
||||||
|
background: var(--w-75);
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
line-height: var(--space-two);
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-icon {
|
.inbox-icon {
|
||||||
|
@ -175,10 +180,6 @@ $label-badge-size: var(--space-slab);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
margin-left: var(--space-smaller);
|
margin-left: var(--space-smaller);
|
||||||
padding: var(--space-zero) var(--space-smaller);
|
padding: var(--space-zero) var(--space-smaller);
|
||||||
|
line-height: var(--font-size-small);
|
||||||
&.is-active {
|
|
||||||
background: var(--w-50);
|
|
||||||
color: var(--w-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,16 +4,15 @@
|
||||||
<span class="secondary-menu--header fs-small">
|
<span class="secondary-menu--header fs-small">
|
||||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="isHelpCenterSidebar" class="submenu-icons">
|
<div v-if="menuItem.showNewButton" class="submenu-icons">
|
||||||
<woot-button
|
<woot-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
|
icon="add"
|
||||||
class="submenu-icon"
|
class="submenu-icon"
|
||||||
@click="onClickOpen"
|
@click="onClickOpen"
|
||||||
>
|
/>
|
||||||
<fluent-icon icon="add" size="16" />
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -28,11 +27,7 @@
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||||
<span
|
<span v-if="showChildCount(menuItem.count)" class="count-view">
|
||||||
v-if="isHelpCenterSidebar"
|
|
||||||
class="count-view"
|
|
||||||
:class="computedClass"
|
|
||||||
>
|
|
||||||
{{ `${menuItem.count}` }}
|
{{ `${menuItem.count}` }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -55,7 +50,7 @@
|
||||||
:should-truncate="child.truncateLabel"
|
:should-truncate="child.truncateLabel"
|
||||||
:icon="computedInboxClass(child)"
|
:icon="computedInboxClass(child)"
|
||||||
:warning-icon="computedInboxErrorClass(child)"
|
:warning-icon="computedInboxErrorClass(child)"
|
||||||
:is-help-center-sidebar="isHelpCenterSidebar"
|
:show-child-count="showChildCount(child.count)"
|
||||||
:child-item-count="child.count"
|
:child-item-count="child.count"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -64,10 +59,10 @@
|
||||||
:to="menuItem.toState"
|
:to="menuItem.toState"
|
||||||
custom
|
custom
|
||||||
>
|
>
|
||||||
<li>
|
<li class="menu-item--new">
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
class="button small clear menu-item--new secondary"
|
class="button small link clear secondary"
|
||||||
:class="{ 'is-active': isActive }"
|
:class="{ 'is-active': isActive }"
|
||||||
@click="e => newLinkClick(e, navigate)"
|
@click="e => newLinkClick(e, navigate)"
|
||||||
>
|
>
|
||||||
|
@ -78,9 +73,6 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
<p v-if="isHelpCenterSidebar && isCategoryEmpty" class="empty-text">
|
|
||||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
|
||||||
</p>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
@ -104,14 +96,6 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
isHelpCenterSidebar: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isCategoryEmpty: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
@ -161,8 +145,8 @@ export default {
|
||||||
this.menuItem.toStateName === 'settings_applications'
|
this.menuItem.toStateName === 'settings_applications'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isArticlesView() {
|
isCurrentRoute() {
|
||||||
return this.$store.state.route.name === this.menuItem.toStateName;
|
return this.$store.state.route.name.includes(this.menuItem.toStateName);
|
||||||
},
|
},
|
||||||
|
|
||||||
computedClass() {
|
computedClass() {
|
||||||
|
@ -181,12 +165,11 @@ export default {
|
||||||
}
|
}
|
||||||
return ' ';
|
return ' ';
|
||||||
}
|
}
|
||||||
if (this.isHelpCenterSidebar) {
|
|
||||||
if (this.isArticlesView) {
|
if (this.isCurrentRoute) {
|
||||||
return 'is-active';
|
return 'is-active';
|
||||||
}
|
|
||||||
return ' ';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -222,6 +205,9 @@ export default {
|
||||||
onClickOpen() {
|
onClickOpen() {
|
||||||
this.$emit('open');
|
this.$emit('open');
|
||||||
},
|
},
|
||||||
|
showChildCount(count) {
|
||||||
|
return Number.isInteger(count);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -277,6 +263,11 @@ export default {
|
||||||
color: var(--w-500);
|
color: var(--w-500);
|
||||||
border-color: var(--w-25);
|
border-color: var(--w-25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-active .count-view {
|
||||||
|
background: var(--w-75);
|
||||||
|
color: var(--w-600);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-menu--icon {
|
.secondary-menu--icon {
|
||||||
|
@ -306,15 +297,12 @@ export default {
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item .button.menu-item--new {
|
.sidebar-item .menu-item--new {
|
||||||
display: inline-flex;
|
padding: var(--space-small) 0;
|
||||||
height: var(--space-medium);
|
|
||||||
margin: var(--space-smaller) 0;
|
|
||||||
padding: var(--space-smaller);
|
|
||||||
color: var(--s-500);
|
|
||||||
|
|
||||||
&:hover {
|
.button {
|
||||||
color: var(--w-500);
|
display: inline-flex;
|
||||||
|
color: var(--s-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,11 +328,6 @@ export default {
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
margin-left: var(--space-smaller);
|
margin-left: var(--space-smaller);
|
||||||
padding: var(--space-zero) var(--space-smaller);
|
padding: var(--space-zero) var(--space-smaller);
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: var(--w-50);
|
|
||||||
color: var(--w-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submenu-icons {
|
.submenu-icons {
|
||||||
|
@ -356,10 +339,4 @@ export default {
|
||||||
margin-left: var(--space-small);
|
margin-left: var(--space-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
color: var(--s-500);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
margin: var(--space-smaller);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="time-ago">
|
<span class="time-ago">
|
||||||
<span> {{ timeAgo }}</span>
|
<span>{{ timeAgo }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ZERO = 0;
|
|
||||||
const MINUTE_IN_MILLI_SECONDS = 60000;
|
const MINUTE_IN_MILLI_SECONDS = 60000;
|
||||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||||
|
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
import { differenceInMilliseconds } from 'date-fns';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimeAgo',
|
name: 'TimeAgo',
|
||||||
|
@ -28,51 +26,40 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
timeAgo: '',
|
timeAgo: this.dynamicTime(this.timestamp),
|
||||||
timer: null,
|
timer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
watch: {
|
||||||
|
timestamp() {
|
||||||
|
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.isAutoRefreshEnabled) {
|
||||||
|
this.createTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
createTimer() {
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||||
|
this.createTimer();
|
||||||
|
}, this.refreshTime());
|
||||||
|
},
|
||||||
refreshTime() {
|
refreshTime() {
|
||||||
const timeDiff = differenceInMilliseconds(
|
const timeDiff = Date.now() - this.timestamp * 1000;
|
||||||
new Date(),
|
|
||||||
new Date(this.timestamp * 1000)
|
|
||||||
);
|
|
||||||
if (timeDiff > DAY_IN_MILLI_SECONDS) {
|
if (timeDiff > DAY_IN_MILLI_SECONDS) {
|
||||||
return DAY_IN_MILLI_SECONDS;
|
return DAY_IN_MILLI_SECONDS;
|
||||||
}
|
}
|
||||||
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
|
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
|
||||||
return HOUR_IN_MILLI_SECONDS;
|
return HOUR_IN_MILLI_SECONDS;
|
||||||
}
|
}
|
||||||
if (timeDiff > MINUTE_IN_MILLI_SECONDS) {
|
|
||||||
return MINUTE_IN_MILLI_SECONDS;
|
return MINUTE_IN_MILLI_SECONDS;
|
||||||
}
|
|
||||||
return ZERO;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
|
||||||
if (this.isAutoRefreshEnabled) {
|
|
||||||
this.createTimer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.clearTimer();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createTimer() {
|
|
||||||
const refreshTime = this.refreshTime;
|
|
||||||
if (refreshTime > ZERO) {
|
|
||||||
this.timer = setTimeout(() => {
|
|
||||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
|
||||||
this.createTimer();
|
|
||||||
}, refreshTime);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearTimer() {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="filter" :class="actionInputStyles">
|
||||||
class="filter"
|
|
||||||
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
|
|
||||||
>
|
|
||||||
<div class="filter-inputs">
|
<div class="filter-inputs">
|
||||||
<select
|
<select
|
||||||
v-model="action_name"
|
v-model="action_name"
|
||||||
|
@ -60,6 +57,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="!isMacro"
|
||||||
icon="dismiss"
|
icon="dismiss"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
|
@ -120,6 +118,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
isMacro: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
action_name: {
|
action_name: {
|
||||||
|
@ -146,6 +148,12 @@ export default {
|
||||||
return this.actionTypes.find(action => action.key === this.action_name)
|
return this.actionTypes.find(action => action.key === this.action_name)
|
||||||
.inputType;
|
.inputType;
|
||||||
},
|
},
|
||||||
|
actionInputStyles() {
|
||||||
|
return {
|
||||||
|
'has-error': this.v.action_params.$dirty && this.v.action_params.$error,
|
||||||
|
'is-a-macro': this.isMacro,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAction() {
|
removeAction() {
|
||||||
|
@ -165,9 +173,21 @@ export default {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-medium);
|
border-radius: var(--border-radius-medium);
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-small);
|
||||||
|
|
||||||
|
&.is-a-macro {
|
||||||
|
margin-bottom: 0;
|
||||||
|
background: var(--white);
|
||||||
|
padding: var(--space-zero);
|
||||||
|
border: unset;
|
||||||
|
border-radius: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter.error {
|
.no-margin-bottom {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter.has-error {
|
||||||
background: var(--r-50);
|
background: var(--r-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,5 +113,6 @@ input[type='file'] {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="avatar-container" :style="style" aria-hidden="true">
|
||||||
class="avatar-container"
|
{{ userInitial }}
|
||||||
:style="[style, customStyle]"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<span>{{ userInitial }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,69 +12,26 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
backgroundColor: {
|
|
||||||
type: String,
|
|
||||||
default: '#c2e1ff',
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: '#1976cc',
|
|
||||||
},
|
|
||||||
customStyle: {
|
|
||||||
type: Object,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
size: {
|
size: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 40,
|
default: 40,
|
||||||
},
|
},
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
rounded: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
type: String,
|
|
||||||
default: 'circle',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
style() {
|
style() {
|
||||||
let style = {
|
return {
|
||||||
width: `${this.size}px`,
|
|
||||||
height: `${this.size}px`,
|
|
||||||
borderRadius:
|
|
||||||
this.variant === 'square' ? 'var(--border-radius-large)' : '50%',
|
|
||||||
lineHeight: `${this.size + Math.floor(this.size / 20)}px`,
|
|
||||||
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.backgroundColor) {
|
|
||||||
style = { ...style, backgroundColor: this.backgroundColor };
|
|
||||||
}
|
|
||||||
if (this.color) {
|
|
||||||
style = { ...style, color: this.color };
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
},
|
},
|
||||||
userInitial() {
|
userInitial() {
|
||||||
return this.initials || this.initial(this.username);
|
const parts = this.username.split(/[ -]/);
|
||||||
},
|
let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
initial(username) {
|
|
||||||
const parts = username ? username.split(/[ -]/) : [];
|
|
||||||
let initials = '';
|
|
||||||
for (let i = 0; i < parts.length; i += 1) {
|
|
||||||
initials += parts[i].charAt(0);
|
|
||||||
}
|
|
||||||
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
||||||
initials = initials.replace(/[a-z]+/g, '');
|
initials = initials.replace(/[a-z]+/g, '');
|
||||||
}
|
}
|
||||||
initials = initials.substring(0, 2).toUpperCase();
|
initials = initials.substring(0, 2).toUpperCase();
|
||||||
|
|
||||||
return initials;
|
return initials;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -88,6 +41,7 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
line-height: 100%;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="isFeatureEnabled">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
featureKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
}),
|
||||||
|
isFeatureEnabled() {
|
||||||
|
return this.isFeatureEnabledonAccount(this.accountId, this.featureKey);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -83,75 +83,71 @@ export default {
|
||||||
},
|
},
|
||||||
pageSize: {
|
pageSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 15,
|
default: 25,
|
||||||
},
|
},
|
||||||
totalCount: {
|
totalCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
onPageChange: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isFooterVisible() {
|
isFooterVisible() {
|
||||||
return this.totalCount && !(this.firstIndex > this.totalCount);
|
return this.totalCount && !(this.firstIndex > this.totalCount);
|
||||||
},
|
},
|
||||||
firstIndex() {
|
firstIndex() {
|
||||||
const firstIndex = this.pageSize * (this.currentPage - 1) + 1;
|
return this.pageSize * (this.currentPage - 1) + 1;
|
||||||
return firstIndex;
|
|
||||||
},
|
},
|
||||||
lastIndex() {
|
lastIndex() {
|
||||||
const index = Math.min(this.totalCount, this.pageSize * this.currentPage);
|
return Math.min(this.totalCount, this.pageSize * this.currentPage);
|
||||||
return index;
|
|
||||||
},
|
},
|
||||||
searchButtonClass() {
|
searchButtonClass() {
|
||||||
return this.searchQuery !== '' ? 'show' : '';
|
return this.searchQuery !== '' ? 'show' : '';
|
||||||
},
|
},
|
||||||
hasLastPage() {
|
hasLastPage() {
|
||||||
const isDisabled =
|
return !!Math.ceil(this.totalCount / this.pageSize);
|
||||||
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
|
||||||
return isDisabled;
|
|
||||||
},
|
},
|
||||||
hasFirstPage() {
|
hasFirstPage() {
|
||||||
const isDisabled = this.currentPage === 1;
|
return this.currentPage === 1;
|
||||||
return isDisabled;
|
|
||||||
},
|
},
|
||||||
hasNextPage() {
|
hasNextPage() {
|
||||||
const isDisabled =
|
return this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||||
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
|
||||||
return isDisabled;
|
|
||||||
},
|
},
|
||||||
hasPrevPage() {
|
hasPrevPage() {
|
||||||
const isDisabled = this.currentPage === 1;
|
return this.currentPage === 1;
|
||||||
return isDisabled;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNextPage() {
|
onNextPage() {
|
||||||
if (this.hasNextPage) return;
|
if (this.hasNextPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newPage = this.currentPage + 1;
|
const newPage = this.currentPage + 1;
|
||||||
this.onPageChange(newPage);
|
this.onPageChange(newPage);
|
||||||
},
|
},
|
||||||
onPrevPage() {
|
onPrevPage() {
|
||||||
if (this.hasPrevPage) return;
|
if (this.hasPrevPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newPage = this.currentPage - 1;
|
const newPage = this.currentPage - 1;
|
||||||
this.onPageChange(newPage);
|
this.onPageChange(newPage);
|
||||||
},
|
},
|
||||||
onFirstPage() {
|
onFirstPage() {
|
||||||
if (this.hasFirstPage) return;
|
if (this.hasFirstPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newPage = 1;
|
const newPage = 1;
|
||||||
this.onPageChange(newPage);
|
this.onPageChange(newPage);
|
||||||
},
|
},
|
||||||
onLastPage() {
|
onLastPage() {
|
||||||
if (this.hasLastPage) return;
|
if (this.hasLastPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newPage = Math.ceil(this.totalCount / this.pageSize);
|
const newPage = Math.ceil(this.totalCount / this.pageSize);
|
||||||
this.onPageChange(newPage);
|
this.onPageChange(newPage);
|
||||||
},
|
},
|
||||||
|
onPageChange(page) {
|
||||||
|
this.$emit('page-change', page);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,74 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="user-thumbnail-box" :style="{ height: size, width: size }">
|
<div class="user-thumbnail-box" :style="{ height: size, width: size }">
|
||||||
<img
|
<img
|
||||||
v-if="!imgError && Boolean(src)"
|
v-if="!imgError && src"
|
||||||
id="image"
|
|
||||||
:src="src"
|
:src="src"
|
||||||
:class="thumbnailClass"
|
:class="thumbnailClass"
|
||||||
@error="onImgError()"
|
@error="onImgError"
|
||||||
/>
|
/>
|
||||||
<Avatar
|
<Avatar
|
||||||
v-else
|
v-else
|
||||||
:username="userNameWithoutEmoji"
|
:username="userNameWithoutEmoji"
|
||||||
:class="thumbnailClass"
|
:class="thumbnailClass"
|
||||||
:size="avatarSize"
|
:size="avatarSize"
|
||||||
:variant="variant"
|
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-if="badge === 'instagram_direct_message'"
|
v-if="badgeSrc"
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
class="source-badge"
|
||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
src="/integrations/channels/badges/instagram-dm.png"
|
:src="`/integrations/channels/badges/${badgeSrc}.png`"
|
||||||
/>
|
alt="Badge"
|
||||||
<img
|
|
||||||
v-else-if="badge === 'facebook'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/messenger.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="badge === 'twitter-tweet'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/twitter-tweet.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="badge === 'twitter-dm'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/twitter-dm.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="badge === 'whatsapp'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/whatsapp.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="badge === 'sms'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/sms.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="badge === 'Channel::Line'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/line.png"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else-if="badge === 'Channel::Telegram'"
|
|
||||||
id="badge"
|
|
||||||
class="source-badge"
|
|
||||||
:style="badgeStyle"
|
|
||||||
src="/integrations/channels/badges/telegram.png"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showStatusIndicator"
|
v-if="showStatusIndicator"
|
||||||
|
@ -83,7 +32,7 @@
|
||||||
* Src - source for round image
|
* Src - source for round image
|
||||||
* Size - Size of the thumbnail
|
* Size - Size of the thumbnail
|
||||||
* Badge - Chat source indication { fb / telegram }
|
* Badge - Chat source indication { fb / telegram }
|
||||||
* Username - User name for avatar
|
* Username - Username for avatar
|
||||||
*/
|
*/
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
import { removeEmoji } from 'shared/helpers/emoji';
|
import { removeEmoji } from 'shared/helpers/emoji';
|
||||||
|
@ -103,7 +52,7 @@ export default {
|
||||||
},
|
},
|
||||||
badge: {
|
badge: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'fb',
|
default: '',
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -142,6 +91,19 @@ export default {
|
||||||
avatarSize() {
|
avatarSize() {
|
||||||
return Number(this.size.replace(/\D+/g, ''));
|
return Number(this.size.replace(/\D+/g, ''));
|
||||||
},
|
},
|
||||||
|
badgeSrc() {
|
||||||
|
return {
|
||||||
|
instagram_direct_message: 'instagram-dm',
|
||||||
|
facebook: 'messenger',
|
||||||
|
'twitter-tweet': 'twitter-tweet',
|
||||||
|
'twitter-dm': 'twitter-dm',
|
||||||
|
whatsapp: 'whatsapp',
|
||||||
|
sms: 'sms',
|
||||||
|
'Channel::Line': 'line',
|
||||||
|
'Channel::Telegram': 'telegram',
|
||||||
|
'Channel::WebWidget': '',
|
||||||
|
}[this.badge];
|
||||||
|
},
|
||||||
badgeStyle() {
|
badgeStyle() {
|
||||||
const size = Math.floor(this.avatarSize / 3);
|
const size = Math.floor(this.avatarSize / 3);
|
||||||
const badgeSize = `${size + 2}px`;
|
const badgeSize = `${size + 2}px`;
|
||||||
|
@ -160,12 +122,10 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
src: {
|
src(value, oldValue) {
|
||||||
handler(value, oldValue) {
|
if (value !== oldValue && this.imgError) {
|
||||||
if (value !== oldValue && this.imgError) {
|
this.imgError = false;
|
||||||
this.imgError = false;
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -229,9 +189,5 @@ export default {
|
||||||
.user-online-status--offline {
|
.user-online-status--offline {
|
||||||
background: var(--s-500);
|
background: var(--s-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-online-status--offline {
|
|
||||||
background: var(--s-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -25,6 +25,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
'notification.created': this.onNotificationCreated,
|
'notification.created': this.onNotificationCreated,
|
||||||
'first.reply.created': this.onFirstReplyCreated,
|
'first.reply.created': this.onFirstReplyCreated,
|
||||||
'conversation.read': this.onConversationRead,
|
'conversation.read': this.onConversationRead,
|
||||||
|
'conversation.updated': this.onConversationUpdated,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +86,11 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
this.fetchConversationStats();
|
this.fetchConversationStats();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onConversationUpdated = data => {
|
||||||
|
this.app.$store.dispatch('updateConversation', data);
|
||||||
|
this.fetchConversationStats();
|
||||||
|
};
|
||||||
|
|
||||||
onTypingOn = ({ conversation, user }) => {
|
onTypingOn = ({ conversation, user }) => {
|
||||||
const conversationId = conversation.id;
|
const conversationId = conversation.id;
|
||||||
|
|
||||||
|
|
71
app/javascript/dashboard/helper/specs/macrosFixtures.js
Normal file
71
app/javascript/dashboard/helper/specs/macrosFixtures.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
export const teams = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '⚙️ sales team',
|
||||||
|
description: 'This is our internal sales team',
|
||||||
|
allow_auto_assign: true,
|
||||||
|
account_id: 1,
|
||||||
|
is_member: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '🤷♂️ fayaz',
|
||||||
|
description: 'Test',
|
||||||
|
allow_auto_assign: true,
|
||||||
|
account_id: 1,
|
||||||
|
is_member: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '🇮🇳 apac sales',
|
||||||
|
description: 'Sales team for France Territory',
|
||||||
|
allow_auto_assign: true,
|
||||||
|
account_id: 1,
|
||||||
|
is_member: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const labels = [
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'sales',
|
||||||
|
description: 'sales team',
|
||||||
|
color: '#8EA20F',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'billing',
|
||||||
|
description: 'billing',
|
||||||
|
color: '#4077DA',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'snoozed',
|
||||||
|
description: 'Items marked for later',
|
||||||
|
color: '#D12F42',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'mobile-app',
|
||||||
|
description: 'tech team',
|
||||||
|
color: '#2DB1CC',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
title: 'human-resources-department-with-long-title',
|
||||||
|
description: 'Test',
|
||||||
|
color: '#FF6E09',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
title: 'priority',
|
||||||
|
description: 'For important sales leads',
|
||||||
|
color: '#7E7CED',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
];
|
52
app/javascript/dashboard/helper/specs/macrosHelper.spec.js
Normal file
52
app/javascript/dashboard/helper/specs/macrosHelper.spec.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {
|
||||||
|
emptyMacro,
|
||||||
|
resolveActionName,
|
||||||
|
resolveLabels,
|
||||||
|
resolveTeamIds,
|
||||||
|
} from '../../routes/dashboard/settings/macros/macroHelper';
|
||||||
|
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
||||||
|
import { teams, labels } from './macrosFixtures';
|
||||||
|
|
||||||
|
describe('#emptyMacro', () => {
|
||||||
|
const defaultMacro = {
|
||||||
|
name: '',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action_name: 'assign_team',
|
||||||
|
action_params: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
visibility: 'global',
|
||||||
|
};
|
||||||
|
it('returns the default macro', () => {
|
||||||
|
expect(emptyMacro).toEqual(defaultMacro);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#resolveActionName', () => {
|
||||||
|
it('resolve action name from key and return the correct label', () => {
|
||||||
|
expect(resolveActionName(MACRO_ACTION_TYPES[0].key)).toEqual(
|
||||||
|
MACRO_ACTION_TYPES[0].label
|
||||||
|
);
|
||||||
|
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).toEqual(
|
||||||
|
MACRO_ACTION_TYPES[1].label
|
||||||
|
);
|
||||||
|
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
|
||||||
|
MACRO_ACTION_TYPES[0].label
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#resolveTeamIds', () => {
|
||||||
|
it('resolves team names from ids, and returns a joined string', () => {
|
||||||
|
const resolvedTeams = '⚙️ sales team, 🤷♂️ fayaz';
|
||||||
|
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#resolveLabels', () => {
|
||||||
|
it('resolves labels names from ids and returns a joined string', () => {
|
||||||
|
const resolvedLabels = 'sales, billing';
|
||||||
|
expect(resolveLabels(labels, ['sales', 'billing'])).toEqual(resolvedLabels);
|
||||||
|
});
|
||||||
|
});
|
|
@ -208,7 +208,8 @@
|
||||||
"CONVERSATION_LABELS": "Conversation Labels",
|
"CONVERSATION_LABELS": "Conversation Labels",
|
||||||
"CONVERSATION_INFO": "Conversation Information",
|
"CONVERSATION_INFO": "Conversation Information",
|
||||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||||
"PREVIOUS_CONVERSATION": "Previous Conversations"
|
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||||
|
"MACROS": "Macros"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||||
|
|
|
@ -1,5 +1,73 @@
|
||||||
{
|
{
|
||||||
"MACROS": {
|
"MACROS": {
|
||||||
"HEADER": "Macros"
|
"HEADER": "Macros",
|
||||||
|
"HEADER_BTN_TXT": "Add a new macro",
|
||||||
|
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||||
|
"LOADING": "Fetching macros",
|
||||||
|
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
||||||
|
"ERROR": "Something went wrong. Please try again",
|
||||||
|
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
||||||
|
"ADD": {
|
||||||
|
"FORM": {
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Macro name",
|
||||||
|
"PLACEHOLDER": "Enter a name for your macro",
|
||||||
|
"ERROR": "Name is required for creating a macro"
|
||||||
|
},
|
||||||
|
"ACTIONS": {
|
||||||
|
"LABEL": "Actions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Macro added successfully",
|
||||||
|
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"TABLE_HEADER": ["Name", "Created by", "Last updated by", "Visibility"],
|
||||||
|
"404": "No macros found"
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"TOOLTIP": "Delete macro",
|
||||||
|
"CONFIRM": {
|
||||||
|
"MESSAGE": "Are you sure to delete ",
|
||||||
|
"YES": "Yes, Delete",
|
||||||
|
"NO": "No"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Macro deleted successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"TOOLTIP": "Edit macro",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Macro updated successfully",
|
||||||
|
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDITOR": {
|
||||||
|
"START_FLOW": "Start Flow",
|
||||||
|
"END_FLOW": "End Flow",
|
||||||
|
"LOADING": "Fetching macro",
|
||||||
|
"ADD_BTN_TOOLTIP": "Add new action",
|
||||||
|
"DELETE_BTN_TOOLTIP": "Delete Action",
|
||||||
|
"VISIBILITY": {
|
||||||
|
"LABEL": "Macro Visibility",
|
||||||
|
"GLOBAL": {
|
||||||
|
"LABEL": "Public",
|
||||||
|
"DESCRIPTION": "This macro is available publicly for all agents in this account."
|
||||||
|
},
|
||||||
|
"PERSONAL": {
|
||||||
|
"LABEL": "Private",
|
||||||
|
"DESCRIPTION": "This macro will be private to you and not be available to others."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EXECUTE": {
|
||||||
|
"BUTTON_TOOLTIP": "Execute",
|
||||||
|
"PREVIEW": "Preview Macro",
|
||||||
|
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
app/javascript/dashboard/mixins/macrosMixin.js
Normal file
20
app/javascript/dashboard/mixins/macrosMixin.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
getDropdownValues(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'assign_team':
|
||||||
|
case 'send_email_to_team':
|
||||||
|
return this.teams;
|
||||||
|
case 'add_label':
|
||||||
|
return this.labels.map(i => {
|
||||||
|
return {
|
||||||
|
id: i.title,
|
||||||
|
name: i.title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
41
app/javascript/dashboard/mixins/specs/macros.spec.js
Normal file
41
app/javascript/dashboard/mixins/specs/macros.spec.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { createWrapper } from '@vue/test-utils';
|
||||||
|
import macrosMixin from '../macrosMixin';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { teams, labels } from '../../helper/specs/macrosFixtures';
|
||||||
|
describe('webhookMixin', () => {
|
||||||
|
describe('#getEventLabel', () => {
|
||||||
|
it('returns correct i18n translation:', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'MyComponent',
|
||||||
|
mixins: [macrosMixin],
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
teams,
|
||||||
|
labels,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
$t(text) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvedLabels = labels.map(i => {
|
||||||
|
return {
|
||||||
|
id: i.title,
|
||||||
|
name: i.title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const Constructor = Vue.extend(Component);
|
||||||
|
const vm = new Constructor().$mount();
|
||||||
|
const wrapper = createWrapper(vm);
|
||||||
|
expect(wrapper.vm.getDropdownValues('assign_team')).toEqual(teams);
|
||||||
|
expect(wrapper.vm.getDropdownValues('send_email_to_team')).toEqual(teams);
|
||||||
|
expect(wrapper.vm.getDropdownValues('add_label')).toEqual(resolvedLabels);
|
||||||
|
expect(wrapper.vm.getDropdownValues()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -116,6 +116,7 @@ describe('uiSettingsMixin', () => {
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([
|
expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([
|
||||||
{ name: 'conversation_actions' },
|
{ name: 'conversation_actions' },
|
||||||
|
{ name: 'macros' },
|
||||||
{ name: 'conversation_info' },
|
{ name: 'conversation_info' },
|
||||||
{ name: 'contact_attributes' },
|
{ name: 'contact_attributes' },
|
||||||
{ name: 'previous_conversation' },
|
{ name: 'previous_conversation' },
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { mapGetters } from 'vuex';
|
||||||
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
|
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
|
||||||
{ name: 'conversation_actions' },
|
{ name: 'conversation_actions' },
|
||||||
{ name: 'conversation_participants' },
|
{ name: 'conversation_participants' },
|
||||||
|
{ name: 'macros' },
|
||||||
{ name: 'conversation_info' },
|
{ name: 'conversation_info' },
|
||||||
{ name: 'contact_attributes' },
|
{ name: 'contact_attributes' },
|
||||||
{ name: 'previous_conversation' },
|
{ name: 'previous_conversation' },
|
||||||
|
@ -31,7 +32,17 @@ export default {
|
||||||
...mapGetters({ uiSettings: 'getUISettings' }),
|
...mapGetters({ uiSettings: 'getUISettings' }),
|
||||||
conversationSidebarItemsOrder() {
|
conversationSidebarItemsOrder() {
|
||||||
const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings;
|
const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings;
|
||||||
return itemsOrder || DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
// If the sidebar order is not set, use the default order.
|
||||||
|
if (!itemsOrder) {
|
||||||
|
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
||||||
|
}
|
||||||
|
// If the sidebar order doesn't have the new elements, then add them to the list.
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
|
||||||
|
if (!itemsOrder.find(i => i.name === item.name)) {
|
||||||
|
itemsOrder.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return itemsOrder;
|
||||||
},
|
},
|
||||||
contactSidebarItemsOrder() {
|
contactSidebarItemsOrder() {
|
||||||
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
|
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
|
||||||
|
|
|
@ -24,9 +24,9 @@
|
||||||
@on-sort-change="onSortChange"
|
@on-sort-change="onSortChange"
|
||||||
/>
|
/>
|
||||||
<table-footer
|
<table-footer
|
||||||
:on-page-change="onPageChange"
|
|
||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="meta.count"
|
:total-count="meta.count"
|
||||||
|
@page-change="onPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,19 @@
|
||||||
/>
|
/>
|
||||||
</accordion-item>
|
</accordion-item>
|
||||||
</div>
|
</div>
|
||||||
|
<woot-feature-toggle
|
||||||
|
v-else-if="element.name === 'macros'"
|
||||||
|
feature-key="macros"
|
||||||
|
>
|
||||||
|
<accordion-item
|
||||||
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.MACROS')"
|
||||||
|
:is-open="isContactSidebarItemOpen('is_macro_open')"
|
||||||
|
compact
|
||||||
|
@click="value => toggleSidebarUIState('is_macro_open', value)"
|
||||||
|
>
|
||||||
|
<macros-list :conversation-id="conversationId" />
|
||||||
|
</accordion-item>
|
||||||
|
</woot-feature-toggle>
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
@ -131,6 +144,7 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||||
import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue';
|
import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
import MacrosList from './Macros/List';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -143,6 +157,7 @@ export default {
|
||||||
ConversationAction,
|
ConversationAction,
|
||||||
ConversationParticipant,
|
ConversationParticipant,
|
||||||
draggable,
|
draggable,
|
||||||
|
MacrosList,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, uiSettingsMixin],
|
mixins: [alertMixin, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!uiFlags.isFetching && !macros.length"
|
||||||
|
class="macros_list--empty-state"
|
||||||
|
>
|
||||||
|
<p class="no-items-error-message">
|
||||||
|
{{ $t('MACROS.LIST.404') }}
|
||||||
|
</p>
|
||||||
|
<router-link :to="addAccountScoping('settings/macros')">
|
||||||
|
<woot-button
|
||||||
|
variant="smooth"
|
||||||
|
icon="add"
|
||||||
|
size="tiny"
|
||||||
|
class="macros_add-button"
|
||||||
|
>
|
||||||
|
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||||
|
</woot-button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<woot-loading-state
|
||||||
|
v-if="uiFlags.isFetching"
|
||||||
|
:message="$t('MACROS.LOADING')"
|
||||||
|
/>
|
||||||
|
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
|
||||||
|
<macro-item
|
||||||
|
v-for="macro in macros"
|
||||||
|
:key="macro.id"
|
||||||
|
:macro="macro"
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import MacroItem from './MacroItem';
|
||||||
|
import accountMixin from 'dashboard/mixins/account.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MacroItem,
|
||||||
|
},
|
||||||
|
mixins: [accountMixin],
|
||||||
|
props: {
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
macros: ['macros/getMacros'],
|
||||||
|
uiFlags: 'macros/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('macros/get');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macros-list {
|
||||||
|
padding: var(--space-smaller);
|
||||||
|
}
|
||||||
|
.macros_list--empty-state {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.macros_add-button {
|
||||||
|
margin: var(--space-small) auto 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro">
|
||||||
|
<span class="text-truncate">{{ macro.name }}</span>
|
||||||
|
<div class="macros-actions">
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
|
||||||
|
size="tiny"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="info"
|
||||||
|
class="margin-right-smaller"
|
||||||
|
@click="toggleMacroPreview(macro)"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.left-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
|
||||||
|
size="tiny"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="play-circle"
|
||||||
|
:is-loading="isExecuting"
|
||||||
|
@click="executeMacro(macro)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<transition name="menu-slide">
|
||||||
|
<macro-preview
|
||||||
|
v-if="showPreview"
|
||||||
|
v-on-clickaway="closeMacroPreview"
|
||||||
|
:macro="macro"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import MacroPreview from './MacroPreview';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MacroPreview,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, clickaway],
|
||||||
|
props: {
|
||||||
|
macro: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isExecuting: false,
|
||||||
|
showPreview: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async executeMacro(macro) {
|
||||||
|
try {
|
||||||
|
this.isExecuting = true;
|
||||||
|
await this.$store.dispatch('macros/execute', {
|
||||||
|
macroId: macro.id,
|
||||||
|
conversationIds: [this.conversationId],
|
||||||
|
});
|
||||||
|
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('MACROS.ERROR'));
|
||||||
|
} finally {
|
||||||
|
this.isExecuting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleMacroPreview() {
|
||||||
|
this.showPreview = !this.showPreview;
|
||||||
|
},
|
||||||
|
closeMacroPreview() {
|
||||||
|
this.showPreview = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macro {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
color: var(--s-700);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--s-25);
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--w-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macros-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-preview">
|
||||||
|
<p class="macro-title">{{ macro.name }}</p>
|
||||||
|
<div v-for="(action, i) in resolvedMacro" :key="i" class="macro-block">
|
||||||
|
<div v-if="i !== macro.actions.length - 1" class="macro-block-border" />
|
||||||
|
<div class="macro-block-dot" />
|
||||||
|
<p class="macro-action-name">{{ action.actionName }}</p>
|
||||||
|
<p class="macro-action-params">{{ action.actionValue }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
resolveActionName,
|
||||||
|
resolveTeamIds,
|
||||||
|
resolveLabels,
|
||||||
|
} from 'dashboard/routes/dashboard/settings/macros/macroHelper';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
macro: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resolvedMacro() {
|
||||||
|
return this.macro.actions.map(action => {
|
||||||
|
return {
|
||||||
|
actionName: resolveActionName(action.action_name),
|
||||||
|
actionValue: this.getActionValue(
|
||||||
|
action.action_name,
|
||||||
|
action.action_params
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
...mapGetters({
|
||||||
|
labels: 'labels/getLabels',
|
||||||
|
teams: 'teams/getTeams',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getActionValue(key, params) {
|
||||||
|
const actionsMap = {
|
||||||
|
assign_team: resolveTeamIds(this.teams, params),
|
||||||
|
add_label: resolveLabels(this.labels, params),
|
||||||
|
mute_conversation: null,
|
||||||
|
snooze_conversation: null,
|
||||||
|
resolve_conversation: null,
|
||||||
|
send_webhook_event: params[0],
|
||||||
|
send_message: params[0],
|
||||||
|
send_email_transcript: params[0],
|
||||||
|
};
|
||||||
|
return actionsMap[key] || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.macro-preview {
|
||||||
|
position: absolute;
|
||||||
|
max-height: calc(var(--space-giga) * 1.5);
|
||||||
|
min-height: var(--space-jumbo);
|
||||||
|
width: calc(var(--space-giga) + var(--space-large));
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
background-color: var(--white);
|
||||||
|
box-shadow: var(--shadow-dropdown-pane);
|
||||||
|
bottom: calc(var(--space-three) + var(--space-half));
|
||||||
|
right: calc(var(--space-three) + var(--space-half));
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-slab);
|
||||||
|
|
||||||
|
.macro-title {
|
||||||
|
margin-bottom: var(--space-slab);
|
||||||
|
color: var(--s-900);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-block {
|
||||||
|
position: relative;
|
||||||
|
padding-left: var(--space-slab);
|
||||||
|
&:not(:last-child) {
|
||||||
|
padding-bottom: var(--space-slab);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-block-border {
|
||||||
|
top: 0.625rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-minus-half);
|
||||||
|
left: 0;
|
||||||
|
width: 1px;
|
||||||
|
background-color: var(--s-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-block-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: -0.35rem;
|
||||||
|
height: var(--space-small);
|
||||||
|
width: var(--space-small);
|
||||||
|
border: 2px solid var(--s-100);
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: var(--border-radius-full);
|
||||||
|
top: 0.4375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-action-name {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
color: var(--s-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -29,10 +29,10 @@
|
||||||
</table>
|
</table>
|
||||||
<table-footer
|
<table-footer
|
||||||
v-if="articles.length"
|
v-if="articles.length"
|
||||||
:on-page-change="onPageChange"
|
:current-page="currentPage"
|
||||||
:current-page="Number(currentPage)"
|
|
||||||
:total-count="totalCount"
|
:total-count="totalCount"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
|
@page-change="onPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -60,12 +60,12 @@ export default {
|
||||||
},
|
},
|
||||||
pageSize: {
|
pageSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 15,
|
default: 25,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onPageChange(page) {
|
onPageChange(page) {
|
||||||
this.$emit('on-page-change', page);
|
this.$emit('page-change', page);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="row app-wrapper">
|
<div class="row app-wrapper">
|
||||||
<sidebar
|
<sidebar
|
||||||
:route="currentRoute"
|
:route="currentRoute"
|
||||||
|
@toggle-account-modal="toggleAccountModal"
|
||||||
@open-notification-panel="openNotificationPanel"
|
@open-notification-panel="openNotificationPanel"
|
||||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||||
|
@ -16,11 +17,15 @@
|
||||||
:accessible-menu-items="accessibleMenuItems"
|
:accessible-menu-items="accessibleMenuItems"
|
||||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||||
@open-popover="openPortalPopover"
|
@open-popover="openPortalPopover"
|
||||||
@open-modal="onClickOpenAddCatogoryModal"
|
@open-modal="onClickOpenAddCategoryModal"
|
||||||
/>
|
/>
|
||||||
<section class="app-content columns" :class="contentClassName">
|
<section class="app-content columns" :class="contentClassName">
|
||||||
<router-view />
|
<router-view />
|
||||||
<command-bar />
|
<command-bar />
|
||||||
|
<account-selector
|
||||||
|
:show-account-modal="showAccountModal"
|
||||||
|
@close-account-modal="toggleAccountModal"
|
||||||
|
/>
|
||||||
<woot-key-shortcut-modal
|
<woot-key-shortcut-modal
|
||||||
v-if="showShortcutModal"
|
v-if="showShortcutModal"
|
||||||
@close="closeKeyShortcutModal"
|
@close="closeKeyShortcutModal"
|
||||||
|
@ -58,6 +63,7 @@ import PortalPopover from '../components/PortalPopover.vue';
|
||||||
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
||||||
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
||||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
|
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
|
||||||
|
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
|
||||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
|
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import portalMixin from '../mixins/portalMixin';
|
import portalMixin from '../mixins/portalMixin';
|
||||||
|
@ -72,6 +78,7 @@ export default {
|
||||||
NotificationPanel,
|
NotificationPanel,
|
||||||
PortalPopover,
|
PortalPopover,
|
||||||
AddCategory,
|
AddCategory,
|
||||||
|
AccountSelector,
|
||||||
},
|
},
|
||||||
mixins: [portalMixin, uiSettingsMixin],
|
mixins: [portalMixin, uiSettingsMixin],
|
||||||
data() {
|
data() {
|
||||||
|
@ -83,6 +90,7 @@ export default {
|
||||||
showPortalPopover: false,
|
showPortalPopover: false,
|
||||||
showAddCategoryModal: false,
|
showAddCategoryModal: false,
|
||||||
lastActivePortalSlug: '',
|
lastActivePortalSlug: '',
|
||||||
|
showAccountModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -134,14 +142,14 @@ export default {
|
||||||
},
|
},
|
||||||
accessibleMenuItems() {
|
accessibleMenuItems() {
|
||||||
if (!this.selectedPortal) return [];
|
if (!this.selectedPortal) return [];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
meta: {
|
allArticlesCount,
|
||||||
all_articles_count: allArticlesCount,
|
mineArticlesCount,
|
||||||
mine_articles_count: mineArticlesCount,
|
draftArticlesCount,
|
||||||
draft_articles_count: draftArticlesCount,
|
archivedArticlesCount,
|
||||||
archived_articles_count: archivedArticlesCount,
|
} = this.meta;
|
||||||
} = {},
|
|
||||||
} = this.selectedPortal;
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
icon: 'book',
|
icon: 'book',
|
||||||
|
@ -196,6 +204,7 @@ export default {
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
label: 'HELP_CENTER.CATEGORY',
|
label: 'HELP_CENTER.CATEGORY',
|
||||||
hasSubMenu: true,
|
hasSubMenu: true,
|
||||||
|
showNewButton: true,
|
||||||
key: 'category',
|
key: 'category',
|
||||||
children: this.categories.map(category => ({
|
children: this.categories.map(category => ({
|
||||||
id: category.id,
|
id: category.id,
|
||||||
|
@ -216,6 +225,13 @@ export default {
|
||||||
return this.selectedPortal ? this.selectedPortal.name : '';
|
return this.selectedPortal ? this.selectedPortal.name : '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$route.params.portalSlug'() {
|
||||||
|
this.fetchPortalsAndItsCategories();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
this.handleResize();
|
this.handleResize();
|
||||||
|
@ -232,7 +248,7 @@ export default {
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
const slug = this.$route.params.portalSlug;
|
const slug = this.$route.params.portalSlug;
|
||||||
if (slug) {
|
if (slug !== this.lastActivePortalSlug) {
|
||||||
this.lastActivePortalSlug = slug;
|
this.lastActivePortalSlug = slug;
|
||||||
this.updateUISettings({
|
this.updateUISettings({
|
||||||
last_active_portal_slug: slug,
|
last_active_portal_slug: slug,
|
||||||
|
@ -251,12 +267,14 @@ export default {
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
this.isSidebarOpen = !this.isSidebarOpen;
|
this.isSidebarOpen = !this.isSidebarOpen;
|
||||||
},
|
},
|
||||||
fetchPortalsAndItsCategories() {
|
async fetchPortalsAndItsCategories() {
|
||||||
this.$store.dispatch('portals/index').then(() => {
|
await this.$store.dispatch('portals/index');
|
||||||
this.$store.dispatch('categories/index', {
|
const selectedPortalParam = {
|
||||||
portalSlug: this.selectedPortalSlug,
|
portalSlug: this.selectedPortalSlug,
|
||||||
});
|
locale: this.selectedLocaleInPortal,
|
||||||
});
|
};
|
||||||
|
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||||
|
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||||
this.$store.dispatch('agents/get');
|
this.$store.dispatch('agents/get');
|
||||||
},
|
},
|
||||||
toggleKeyShortcutModal() {
|
toggleKeyShortcutModal() {
|
||||||
|
@ -277,12 +295,15 @@ export default {
|
||||||
closePortalPopover() {
|
closePortalPopover() {
|
||||||
this.showPortalPopover = false;
|
this.showPortalPopover = false;
|
||||||
},
|
},
|
||||||
onClickOpenAddCatogoryModal() {
|
onClickOpenAddCategoryModal() {
|
||||||
this.showAddCategoryModal = true;
|
this.showAddCategoryModal = true;
|
||||||
},
|
},
|
||||||
onClickCloseAddCategoryModal() {
|
onClickCloseAddCategoryModal() {
|
||||||
this.showAddCategoryModal = false;
|
this.showAddCategoryModal = false;
|
||||||
},
|
},
|
||||||
|
toggleAccountModal() {
|
||||||
|
this.showAccountModal = !this.showAccountModal;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -376,7 +376,6 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.portal-title {
|
.portal-title {
|
||||||
color: var(--s-900);
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.portal-count {
|
.portal-count {
|
||||||
|
@ -389,14 +388,17 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.portal-locales {
|
.portal-locales {
|
||||||
margin-top: var(--space-medium);
|
margin-bottom: var(--space-large);
|
||||||
margin-bottom: var(--space-small);
|
|
||||||
.locale-title {
|
.locale-title {
|
||||||
color: var(--s-800);
|
color: var(--s-800);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.portal--heading {
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.portal-settings--icon {
|
.portal-settings--icon {
|
||||||
padding: var(--space-smaller);
|
padding: var(--space-smaller);
|
||||||
|
|
|
@ -112,16 +112,8 @@ export default {
|
||||||
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
|
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchPortalsAndItsCategories() {
|
|
||||||
this.$store.dispatch('portals/index').then(() => {
|
|
||||||
this.$store.dispatch('categories/index', {
|
|
||||||
portalSlug: this.portal.slug,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onClick(event, code, portal) {
|
onClick(event, code, portal) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.fetchPortalsAndItsCategories();
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'list_all_locale_articles',
|
name: 'list_all_locale_articles',
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -12,16 +12,20 @@
|
||||||
v-for="menuItem in accessibleMenuItems"
|
v-for="menuItem in accessibleMenuItems"
|
||||||
:key="menuItem.toState"
|
:key="menuItem.toState"
|
||||||
:menu-item="menuItem"
|
:menu-item="menuItem"
|
||||||
:is-help-center-sidebar="true"
|
|
||||||
/>
|
/>
|
||||||
<secondary-nav-item
|
<secondary-nav-item
|
||||||
v-for="menuItem in additionalSecondaryMenuItems"
|
v-for="menuItem in additionalSecondaryMenuItems"
|
||||||
:key="menuItem.key"
|
:key="menuItem.key"
|
||||||
:menu-item="menuItem"
|
:menu-item="menuItem"
|
||||||
:is-help-center-sidebar="true"
|
|
||||||
:is-category-empty="!hasCategory"
|
|
||||||
@open="onClickOpenAddCatogoryModal"
|
@open="onClickOpenAddCatogoryModal"
|
||||||
/>
|
/>
|
||||||
|
<p
|
||||||
|
v-if="!hasCategory"
|
||||||
|
key="empty-category-nessage"
|
||||||
|
class="empty-text text-muted"
|
||||||
|
>
|
||||||
|
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
||||||
|
</p>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -123,4 +127,8 @@ export default {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
padding: var(--space-smaller) var(--space-normal);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -76,7 +76,7 @@ export default {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: var(--space-normal);
|
padding: var(--space-normal);
|
||||||
margin: var(--space-minus-small);
|
margin: var(--space-minus-small);
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-smaller);
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import AddCategoryComponent from '../AddCategory.vue';
|
import AddCategoryComponent from '../../pages/categories/AddCategory.vue';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import ArticleEditor from './ArticleEditor.vue';
|
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/Help Center',
|
title: 'Components/Help Center',
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
||||||
} = this.uiSettings || {};
|
} = this.uiSettings || {};
|
||||||
|
|
||||||
if (lastActivePortalSlug)
|
if (lastActivePortalSlug)
|
||||||
this.$router.push({
|
this.$router.replace({
|
||||||
name: 'list_all_locale_articles',
|
name: 'list_all_locale_articles',
|
||||||
params: {
|
params: {
|
||||||
portalSlug: lastActivePortalSlug,
|
portalSlug: lastActivePortalSlug,
|
||||||
|
@ -22,7 +22,7 @@ export default {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
this.$router.push({
|
this.$router.replace({
|
||||||
name: 'list_all_portals',
|
name: 'list_all_portals',
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,16 +2,15 @@
|
||||||
<div class="container overflow-auto">
|
<div class="container overflow-auto">
|
||||||
<article-header
|
<article-header
|
||||||
:header-title="headerTitle"
|
:header-title="headerTitle"
|
||||||
:count="articleCount"
|
:count="meta.count"
|
||||||
selected-value="Published"
|
selected-value="Published"
|
||||||
@newArticlePage="newArticlePage"
|
@newArticlePage="newArticlePage"
|
||||||
/>
|
/>
|
||||||
<article-table
|
<article-table
|
||||||
:articles="articles"
|
:articles="articles"
|
||||||
:article-count="articles.length"
|
|
||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="articleCount"
|
:total-count="Number(meta.count)"
|
||||||
@on-page-change="onPageChange"
|
@page-change="onPageChange"
|
||||||
/>
|
/>
|
||||||
<div v-if="shouldShowLoader" class="articles--loader">
|
<div v-if="shouldShowLoader" class="articles--loader">
|
||||||
<spinner />
|
<spinner />
|
||||||
|
@ -105,9 +104,6 @@ export default {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
articleCount() {
|
|
||||||
return this.articles ? this.articles.length : 0;
|
|
||||||
},
|
|
||||||
headerTitleInCategoryView() {
|
headerTitleInCategoryView() {
|
||||||
return this.categories && this.categories.length
|
return this.categories && this.categories.length
|
||||||
? this.selectedCategory.name
|
? this.selectedCategory.name
|
||||||
|
@ -128,9 +124,9 @@ export default {
|
||||||
newArticlePage() {
|
newArticlePage() {
|
||||||
this.$router.push({ name: 'new_article' });
|
this.$router.push({ name: 'new_article' });
|
||||||
},
|
},
|
||||||
fetchArticles() {
|
fetchArticles({ pageNumber } = {}) {
|
||||||
this.$store.dispatch('articles/index', {
|
this.$store.dispatch('articles/index', {
|
||||||
pageNumber: this.pageNumber,
|
pageNumber: pageNumber || this.pageNumber,
|
||||||
portalSlug: this.$route.params.portalSlug,
|
portalSlug: this.$route.params.portalSlug,
|
||||||
locale: this.$route.params.locale,
|
locale: this.$route.params.locale,
|
||||||
status: this.status,
|
status: this.status,
|
||||||
|
@ -138,8 +134,8 @@ export default {
|
||||||
category_slug: this.selectedCategorySlug,
|
category_slug: this.selectedCategorySlug,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onPageChange(page) {
|
onPageChange(pageNumber) {
|
||||||
this.fetchArticles({ pageNumber: page });
|
this.fetchArticles({ pageNumber });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.CREATE_BASIC_SETTING_BUTTON'
|
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.CREATE_BASIC_SETTING_BUTTON'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@submit="updateBasicSettings"
|
@submit="createPortal"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async updateBasicSettings(portal) {
|
async createPortal(portal) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('portals/create', {
|
await this.$store.dispatch('portals/create', {
|
||||||
portal,
|
portal,
|
||||||
|
@ -45,16 +45,16 @@ export default {
|
||||||
this.alertMessage = this.$t(
|
this.alertMessage = this.$t(
|
||||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
||||||
);
|
);
|
||||||
|
this.$router.push({
|
||||||
|
name: 'portal_customization',
|
||||||
|
params: { portalSlug: portal.slug },
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertMessage =
|
this.alertMessage =
|
||||||
error?.message ||
|
error?.message ||
|
||||||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_BASIC');
|
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_BASIC');
|
||||||
} finally {
|
} finally {
|
||||||
this.showAlert(this.alertMessage);
|
this.showAlert(this.alertMessage);
|
||||||
this.$router.push({
|
|
||||||
name: 'portal_customization',
|
|
||||||
params: { portalSlug: portal.slug },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
:on-mark-all-done-click="onMarkAllDoneClick"
|
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||||
/>
|
/>
|
||||||
<table-footer
|
<table-footer
|
||||||
:on-page-change="onPageChange"
|
|
||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="meta.count"
|
:total-count="meta.count"
|
||||||
|
@page-change="onPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -225,7 +225,7 @@ export default {
|
||||||
mode === 'EDIT'
|
mode === 'EDIT'
|
||||||
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
|
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
|
||||||
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
|
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
|
||||||
await await this.$store.dispatch(action, payload);
|
await this.$store.dispatch(action, payload);
|
||||||
this.showAlert(this.$t(successMessage));
|
this.showAlert(this.$t(successMessage));
|
||||||
this.hideAddPopup();
|
this.hideAddPopup();
|
||||||
this.hideEditPopup();
|
this.hideEditPopup();
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-tooltip="tooltip"
|
||||||
|
class="macros__action-button"
|
||||||
|
:class="type"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<fluent-icon :icon="icon" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'add',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macros__action-button {
|
||||||
|
height: var(--space-three);
|
||||||
|
width: var(--space-three);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
border-radius: var(--border-radius-rounded);
|
||||||
|
position: relative;
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
|
||||||
|
&.add {
|
||||||
|
background-color: var(--g-100);
|
||||||
|
color: var(--g-600);
|
||||||
|
}
|
||||||
|
&.delete {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--space-three) / -2);
|
||||||
|
right: calc(var(--space-three) / -2);
|
||||||
|
background-color: var(--r-100);
|
||||||
|
color: var(--r-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,11 +1,121 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="column content-box">
|
||||||
Macros
|
<router-link
|
||||||
|
:to="addAccountScoping('settings/macros/new')"
|
||||||
|
class="button success button--fixed-right-top"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="add-circle" />
|
||||||
|
<span class="button__content">
|
||||||
|
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
<div class="row">
|
||||||
|
<div class="small-8 columns with-right-space">
|
||||||
|
<div
|
||||||
|
v-if="!uiFlags.isFetching && !records.length"
|
||||||
|
class="macros__empty-state"
|
||||||
|
>
|
||||||
|
<p class="no-items-error-message">
|
||||||
|
{{ $t('MACROS.LIST.404') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<woot-loading-state
|
||||||
|
v-if="uiFlags.isFetching"
|
||||||
|
:message="$t('MACROS.LOADING')"
|
||||||
|
/>
|
||||||
|
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
||||||
|
<thead>
|
||||||
|
<th
|
||||||
|
v-for="thHeader in $t('MACROS.LIST.TABLE_HEADER')"
|
||||||
|
:key="thHeader"
|
||||||
|
>
|
||||||
|
{{ thHeader }}
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<macros-table-row
|
||||||
|
v-for="(macro, index) in records"
|
||||||
|
:key="index"
|
||||||
|
:macro="macro"
|
||||||
|
@delete="openDeletePopup(macro, index)"
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="small-4 columns">
|
||||||
|
<span v-dompurify-html="$t('MACROS.SIDEBAR_TXT')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<woot-delete-modal
|
||||||
|
:show.sync="showDeleteConfirmationPopup"
|
||||||
|
:on-close="closeDeletePopup"
|
||||||
|
:on-confirm="confirmDeletion"
|
||||||
|
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||||
|
:message="$t('MACROS.DELETE.CONFIRM.MESSAGE')"
|
||||||
|
:message-value="deleteMessage"
|
||||||
|
:confirm-text="$t('MACROS.DELETE.CONFIRM.YES')"
|
||||||
|
:reject-text="$t('MACROS.DELETE.CONFIRM.NO')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {};
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import accountMixin from 'dashboard/mixins/account.js';
|
||||||
|
import MacrosTableRow from './MacrosTableRow';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MacrosTableRow,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, accountMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showDeleteConfirmationPopup: false,
|
||||||
|
selectedResponse: {},
|
||||||
|
loading: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
records: ['macros/getMacros'],
|
||||||
|
uiFlags: 'macros/getUIFlags',
|
||||||
|
}),
|
||||||
|
deleteMessage() {
|
||||||
|
return ` ${this.selectedResponse.name}?`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('macros/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openDeletePopup(response) {
|
||||||
|
this.showDeleteConfirmationPopup = true;
|
||||||
|
this.selectedResponse = response;
|
||||||
|
},
|
||||||
|
closeDeletePopup() {
|
||||||
|
this.showDeleteConfirmationPopup = false;
|
||||||
|
},
|
||||||
|
confirmDeletion() {
|
||||||
|
this.loading[this.selectedResponse.id] = true;
|
||||||
|
this.closeDeletePopup();
|
||||||
|
this.deleteMacro(this.selectedResponse.id);
|
||||||
|
},
|
||||||
|
async deleteMacro(id) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('macros/delete', id);
|
||||||
|
this.showAlert(this.$t('MACROS.DELETE.API.SUCCESS_MESSAGE'));
|
||||||
|
this.loading[this.selectedResponse.id] = false;
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('MACROS.DELETE.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style scoped>
|
||||||
|
.macros__empty-state {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,9 +1,145 @@
|
||||||
<template>
|
<template>
|
||||||
<div>MacrosEditor</div>
|
<div class="column content-box">
|
||||||
|
<woot-loading-state
|
||||||
|
v-if="uiFlags.isFetchingItem"
|
||||||
|
:message="$t('MACROS.EDITOR.LOADING')"
|
||||||
|
/>
|
||||||
|
<macro-form
|
||||||
|
v-if="macro && !uiFlags.isFetchingItem"
|
||||||
|
:macro-data.sync="macro"
|
||||||
|
@submit="saveMacro"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {};
|
import MacroForm from './MacroForm';
|
||||||
|
import { MACRO_ACTION_TYPES } from './constants';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import macrosMixin from 'dashboard/mixins/macrosMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MacroForm,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, macrosMixin],
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
macroActionTypes: this.macroActionTypes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
macro: null,
|
||||||
|
mode: 'CREATE',
|
||||||
|
macroActionTypes: MACRO_ACTION_TYPES,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'macros/getUIFlags',
|
||||||
|
labels: 'labels/getLabels',
|
||||||
|
teams: 'teams/getTeams',
|
||||||
|
}),
|
||||||
|
macroId() {
|
||||||
|
return this.$route.params.macroId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: {
|
||||||
|
handler() {
|
||||||
|
if (this.$route.params.macroId) {
|
||||||
|
this.fetchMacro();
|
||||||
|
} else {
|
||||||
|
this.initNewMacro();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchMacro() {
|
||||||
|
this.mode = 'EDIT';
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
|
this.$store.dispatch('teams/get');
|
||||||
|
this.$store.dispatch('labels/get');
|
||||||
|
this.manifestMacro();
|
||||||
|
},
|
||||||
|
async manifestMacro() {
|
||||||
|
await this.$store.dispatch('macros/getSingleMacro', this.macroId);
|
||||||
|
const singleMacro = this.$store.getters['macros/getMacro'](this.macroId);
|
||||||
|
this.macro = this.formatMacro(singleMacro);
|
||||||
|
},
|
||||||
|
formatMacro(macro) {
|
||||||
|
const formattedActions = macro.actions.map(action => {
|
||||||
|
let actionParams = [];
|
||||||
|
if (action.action_params.length) {
|
||||||
|
const inputType = this.macroActionTypes.find(
|
||||||
|
item => item.key === action.action_name
|
||||||
|
).inputType;
|
||||||
|
if (inputType === 'multi_select') {
|
||||||
|
actionParams = [
|
||||||
|
...this.getDropdownValues(action.action_name, this.$store),
|
||||||
|
].filter(item => [...action.action_params].includes(item.id));
|
||||||
|
} else if (inputType === 'team_message') {
|
||||||
|
actionParams = {
|
||||||
|
team_ids: [
|
||||||
|
...this.getDropdownValues(action.action_name, this.$store),
|
||||||
|
].filter(item =>
|
||||||
|
[...action.action_params[0].team_ids].includes(item.id)
|
||||||
|
),
|
||||||
|
message: action.action_params[0].message,
|
||||||
|
};
|
||||||
|
} else actionParams = [...action.action_params];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
action_params: actionParams,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...macro,
|
||||||
|
actions: formattedActions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
initNewMacro() {
|
||||||
|
this.mode = 'CREATE';
|
||||||
|
this.macro = {
|
||||||
|
name: '',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action_name: 'assign_team',
|
||||||
|
action_params: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
visibility: 'global',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async saveMacro(macro) {
|
||||||
|
try {
|
||||||
|
const action = this.mode === 'EDIT' ? 'macros/update' : 'macros/create';
|
||||||
|
let successMessage =
|
||||||
|
this.mode === 'EDIT'
|
||||||
|
? this.$t('MACROS.EDIT.API.SUCCESS_MESSAGE')
|
||||||
|
: this.$t('MACROS.ADD.API.SUCCESS_MESSAGE');
|
||||||
|
let serializedMacro = JSON.parse(JSON.stringify(macro));
|
||||||
|
serializedMacro.actions = actionQueryGenerator(serializedMacro.actions);
|
||||||
|
await this.$store.dispatch(action, serializedMacro);
|
||||||
|
this.showAlert(successMessage);
|
||||||
|
this.$router.push({ name: 'macros_wrapper' });
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('MACROS.ERROR'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style scoped>
|
||||||
|
.content-box {
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="small-8 columns with-right-space macros-canvas">
|
||||||
|
<macro-nodes
|
||||||
|
v-model="macro.actions"
|
||||||
|
@addNewNode="appendNode"
|
||||||
|
@deleteNode="deleteNode"
|
||||||
|
@resetAction="resetNode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="small-4 columns">
|
||||||
|
<macro-properties
|
||||||
|
:macro-name="macro.name"
|
||||||
|
:macro-visibility="macro.visibility"
|
||||||
|
@update:name="updateName"
|
||||||
|
@update:visibility="updateVisibility"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MacroNodes from './MacroNodes';
|
||||||
|
import MacroProperties from './MacroProperties';
|
||||||
|
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MacroNodes,
|
||||||
|
MacroProperties,
|
||||||
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
$v: this.$v,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
macroData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
macro: this.macroData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: {
|
||||||
|
handler() {
|
||||||
|
this.resetValidation();
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
macroData: {
|
||||||
|
handler() {
|
||||||
|
this.macro = this.macroData;
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
macro: {
|
||||||
|
name: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
visibility: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
required,
|
||||||
|
$each: {
|
||||||
|
action_params: {
|
||||||
|
required: requiredIf(prop => {
|
||||||
|
if (prop.action_name === 'send_email_to_team') return true;
|
||||||
|
return !(
|
||||||
|
prop.action_name === 'mute_conversation' ||
|
||||||
|
prop.action_name === 'snooze_conversation' ||
|
||||||
|
prop.action_name === 'resolve_conversation'
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateName(value) {
|
||||||
|
this.macro.name = value;
|
||||||
|
},
|
||||||
|
updateVisibility(value) {
|
||||||
|
this.macro.visibility = value;
|
||||||
|
},
|
||||||
|
appendNode() {
|
||||||
|
this.macro.actions.push({
|
||||||
|
action_name: 'assign_team',
|
||||||
|
action_params: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteNode(index) {
|
||||||
|
this.macro.actions.splice(index, 1);
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) return;
|
||||||
|
this.$emit('submit', this.macro);
|
||||||
|
},
|
||||||
|
resetNode(index) {
|
||||||
|
this.$v.macro.actions.$each[index].$reset();
|
||||||
|
this.macro.actions[index].action_params = [];
|
||||||
|
},
|
||||||
|
resetValidation() {
|
||||||
|
this.$v.$reset();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.row {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.macros-canvas {
|
||||||
|
background-image: radial-gradient(var(--s-100) 1.2px, transparent 0);
|
||||||
|
background-size: var(--space-normal) var(--space-normal);
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
padding: var(--space-normal) var(--space-three);
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro__node-action-container">
|
||||||
|
<fluent-icon
|
||||||
|
v-if="!singleNode"
|
||||||
|
size="20"
|
||||||
|
icon="navigation"
|
||||||
|
class="macros__node-drag-handle"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="macro__node-action-item"
|
||||||
|
:class="{
|
||||||
|
'has-error': hasError($v.macro.actions.$each[index]),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<action-input
|
||||||
|
v-model="actionData"
|
||||||
|
:action-types="macroActionTypes"
|
||||||
|
:dropdown-values="dropdownValues()"
|
||||||
|
:show-action-input="showActionInput"
|
||||||
|
:show-remove-button="false"
|
||||||
|
:is-macro="true"
|
||||||
|
:v="$v.macro.actions.$each[index]"
|
||||||
|
@resetAction="$emit('resetAction')"
|
||||||
|
/>
|
||||||
|
<macro-action-button
|
||||||
|
v-if="!singleNode"
|
||||||
|
icon="dismiss-circle"
|
||||||
|
class="macro__node macro__node-action-button-delete"
|
||||||
|
type="delete"
|
||||||
|
:tooltip="$t('MACROS.EDITOR.DELETE_BTN_TOOLTIP')"
|
||||||
|
@click="$emit('deleteNode')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ActionInput from 'dashboard/components/widgets/AutomationActionInput';
|
||||||
|
import MacroActionButton from './ActionButton.vue';
|
||||||
|
import macrosMixin from 'dashboard/mixins/macrosMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ActionInput,
|
||||||
|
MacroActionButton,
|
||||||
|
},
|
||||||
|
mixins: [macrosMixin],
|
||||||
|
inject: ['macroActionTypes', '$v'],
|
||||||
|
props: {
|
||||||
|
singleNode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
labels: 'labels/getLabels',
|
||||||
|
teams: 'teams/getTeams',
|
||||||
|
}),
|
||||||
|
actionData: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showActionInput() {
|
||||||
|
if (
|
||||||
|
this.actionData.action_name === 'send_email_to_team' ||
|
||||||
|
this.actionData.action_name === 'send_message'
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
const type = this.macroActionTypes.find(
|
||||||
|
action => action.key === this.actionData.action_name
|
||||||
|
).inputType;
|
||||||
|
return !!type;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
dropdownValues() {
|
||||||
|
return this.getDropdownValues(this.value.action_name, this.$store);
|
||||||
|
},
|
||||||
|
hasError(v) {
|
||||||
|
return !!(v.action_params.$dirty && v.action_params.$error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macro__node-action-container {
|
||||||
|
position: relative;
|
||||||
|
.macros__node-drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-minus-medium);
|
||||||
|
top: var(--space-smaller);
|
||||||
|
cursor: move;
|
||||||
|
color: var(--s-400);
|
||||||
|
}
|
||||||
|
.macro__node-action-item {
|
||||||
|
background-color: var(--white);
|
||||||
|
padding: var(--space-slab);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
box-shadow: rgb(0 0 0 / 3%) 0px 6px 24px 0px,
|
||||||
|
rgb(0 0 0 / 6%) 0px 0px 0px 1px;
|
||||||
|
|
||||||
|
.macro__node-action-button-delete {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.macro__node-action-button-delete {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.has-error {
|
||||||
|
animation: shake 0.3s ease-in-out 0s 2;
|
||||||
|
background-color: var(--r-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(0.375rem);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-0.375rem);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(0.375rem);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<div class="macros__nodes">
|
||||||
|
<macros-pill :label="$t('MACROS.EDITOR.START_FLOW')" class="macro__node" />
|
||||||
|
<draggable
|
||||||
|
:list="actionData"
|
||||||
|
animation="200"
|
||||||
|
ghost-class="ghost"
|
||||||
|
tag="div"
|
||||||
|
class="macros__nodes-draggable"
|
||||||
|
handle=".macros__node-drag-handle"
|
||||||
|
>
|
||||||
|
<div v-for="(action, i) in actionData" :key="i" class="macro__node">
|
||||||
|
<macro-node
|
||||||
|
v-model="actionData[i]"
|
||||||
|
class="macros__node-action"
|
||||||
|
type="add"
|
||||||
|
:index="i"
|
||||||
|
:single-node="actionData.length === 1"
|
||||||
|
@resetAction="$emit('resetAction', i)"
|
||||||
|
@deleteNode="$emit('deleteNode', i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</draggable>
|
||||||
|
<macro-action-button
|
||||||
|
icon="add-circle"
|
||||||
|
class="macro__node"
|
||||||
|
:tooltip="$t('MACROS.EDITOR.ADD_BTN_TOOLTIP')"
|
||||||
|
type="add"
|
||||||
|
@click="$emit('addNewNode')"
|
||||||
|
/>
|
||||||
|
<macros-pill :label="$t('MACROS.EDITOR.END_FLOW')" class="macro__node" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MacrosPill from './Pill.vue';
|
||||||
|
import Draggable from 'vuedraggable';
|
||||||
|
import MacroNode from './MacroNode.vue';
|
||||||
|
import MacroActionButton from './ActionButton.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Draggable,
|
||||||
|
MacrosPill,
|
||||||
|
MacroNode,
|
||||||
|
MacroActionButton,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
actionData: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macros__nodes {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro__node:not(:last-child) {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: var(--space-three);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro__node:not(:last-child):not(.sortable-chosen):after,
|
||||||
|
.macros__nodes-draggable:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: var(--space-three);
|
||||||
|
width: var(--space-smaller);
|
||||||
|
margin-left: var(--space-medium);
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='4' height='30' viewBox='0 0 4 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline x1='1.50098' y1='0.579529' x2='1.50098' y2='30.5795' stroke='%2393afc8' stroke-width='2' stroke-dasharray='5 5'/%3E%3C/svg%3E%0A");
|
||||||
|
}
|
||||||
|
|
||||||
|
.macros__nodes-draggable {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: var(--space-three);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macros__node-action-container {
|
||||||
|
position: relative;
|
||||||
|
.drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-minus-medium);
|
||||||
|
top: var(--space-smaller);
|
||||||
|
cursor: move;
|
||||||
|
color: var(--s-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,183 @@
|
||||||
|
<template>
|
||||||
|
<div class="macros__properties-panel">
|
||||||
|
<div>
|
||||||
|
<woot-input
|
||||||
|
:value="macroName"
|
||||||
|
:label="$t('MACROS.ADD.FORM.NAME.LABEL')"
|
||||||
|
:placeholder="$t('MACROS.ADD.FORM.NAME.PLACEHOLDER')"
|
||||||
|
:error="$v.macro.name.$error ? $t('MACROS.ADD.FORM.NAME.ERROR') : null"
|
||||||
|
:class="{ error: $v.macro.name.$error }"
|
||||||
|
@input="onUpdateName($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="macros__form-visibility-container">
|
||||||
|
<p class="title">{{ $t('MACROS.EDITOR.VISIBILITY.LABEL') }}</p>
|
||||||
|
<div class="macros__form-visibility">
|
||||||
|
<button
|
||||||
|
class="card"
|
||||||
|
:class="isActive('global')"
|
||||||
|
@click="onUpdateVisibility('global')"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="macroVisibility === 'global'"
|
||||||
|
icon="checkmark-circle"
|
||||||
|
type="solid"
|
||||||
|
class="visibility-check"
|
||||||
|
/>
|
||||||
|
<p class="title">
|
||||||
|
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL') }}
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">
|
||||||
|
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="card"
|
||||||
|
:class="isActive('personal')"
|
||||||
|
@click="onUpdateVisibility('personal')"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="macroVisibility === 'personal'"
|
||||||
|
icon="checkmark-circle"
|
||||||
|
type="solid"
|
||||||
|
class="visibility-check"
|
||||||
|
/>
|
||||||
|
<p class="title">
|
||||||
|
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL') }}
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">
|
||||||
|
{{ $t('MACROS.EDITOR.VISIBILITY.PERSONAL.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="macros__info-panel">
|
||||||
|
<fluent-icon icon="info" size="20" />
|
||||||
|
<p>
|
||||||
|
{{ $t('MACROS.ORDER_INFO') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="macros__submit-button">
|
||||||
|
<woot-button
|
||||||
|
size="expanded"
|
||||||
|
color-scheme="success"
|
||||||
|
@click="$emit('submit')"
|
||||||
|
>
|
||||||
|
{{ $t('MACROS.HEADER_BTN_TXT_SAVE') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['$v'],
|
||||||
|
props: {
|
||||||
|
macroName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
macroVisibility: {
|
||||||
|
type: String,
|
||||||
|
default: 'global',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isActive(key) {
|
||||||
|
return { active: this.macroVisibility === key };
|
||||||
|
},
|
||||||
|
onUpdateName(value) {
|
||||||
|
this.$emit('update:name', value);
|
||||||
|
},
|
||||||
|
onUpdateVisibility(value) {
|
||||||
|
this.$emit('update:visibility', value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.macros__properties-panel {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
background-color: var(--white);
|
||||||
|
// full screen height subtracted by the height of the header
|
||||||
|
height: calc(100vh - 5.6rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-left: 1px solid var(--s-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macros__submit-button {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.macros__form-visibility-container {
|
||||||
|
margin-top: var(--space-small);
|
||||||
|
}
|
||||||
|
.macros__form-visibility {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-slab);
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: var(--space-small);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
border: 1px solid var(--s-200);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--w-25);
|
||||||
|
border: 1px solid var(--w-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-check {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--w-500);
|
||||||
|
top: var(--space-small);
|
||||||
|
right: var(--space-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
color: var(--s-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.macros__info-panel {
|
||||||
|
margin-top: var(--space-small);
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--s-50);
|
||||||
|
padding: var(--space-small);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
align-items: flex-start;
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep input[type='text'] {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .error {
|
||||||
|
.message {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td>{{ macro.name }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="avatar-container">
|
||||||
|
<thumbnail :username="macro.created_by.name" size="24px" />
|
||||||
|
<span class="ml-2">{{ macro.created_by.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="avatar-container">
|
||||||
|
<thumbnail :username="macro.updated_by.name" size="24px" />
|
||||||
|
<span class="ml-2">{{ macro.updated_by.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ visibilityLabel }}</td>
|
||||||
|
<td class="button-wrapper">
|
||||||
|
<router-link :to="addAccountScoping(`settings/macros/${macro.id}/edit`)">
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
||||||
|
variant="smooth"
|
||||||
|
size="tiny"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class-names="grey-btn"
|
||||||
|
icon="edit"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip.top="$t('MACROS.DELETE.TOOLTIP')"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="alert"
|
||||||
|
size="tiny"
|
||||||
|
icon="dismiss-circle"
|
||||||
|
class-names="grey-btn"
|
||||||
|
@click="$emit('delete')"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
|
import accountMixin from 'dashboard/mixins/account.js';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
},
|
||||||
|
mixins: [accountMixin],
|
||||||
|
props: {
|
||||||
|
macro: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visibilityLabel() {
|
||||||
|
return this.macro.visibility === 'global'
|
||||||
|
? this.$t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL')
|
||||||
|
: this.$t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.avatar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="macros-item macros-pill">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.macros-pill {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
background-color: var(--w-500);
|
||||||
|
max-width: max-content;
|
||||||
|
color: var(--white);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
border-radius: var(--border-radius-full);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,42 @@
|
||||||
|
export const MACRO_ACTION_TYPES = [
|
||||||
|
{
|
||||||
|
key: 'assign_team',
|
||||||
|
label: 'Assign a team',
|
||||||
|
inputType: 'multi_select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'add_label',
|
||||||
|
label: 'Add a label',
|
||||||
|
inputType: 'multi_select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
label: 'Send an email transcript',
|
||||||
|
inputType: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
label: 'Mute conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_conversation',
|
||||||
|
label: 'Snooze conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_conversation',
|
||||||
|
label: 'Resolve conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_attachment',
|
||||||
|
label: 'Send Attachment',
|
||||||
|
inputType: 'attachment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_message',
|
||||||
|
label: 'Send a message',
|
||||||
|
inputType: 'textarea',
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { MACRO_ACTION_TYPES as macroActionTypes } from 'dashboard/routes/dashboard/settings/macros/constants.js';
|
||||||
|
export const emptyMacro = {
|
||||||
|
name: '',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action_name: 'assign_team',
|
||||||
|
action_params: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
visibility: 'global',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveActionName = key => {
|
||||||
|
return macroActionTypes.find(i => i.key === key).label;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveTeamIds = (teams, ids) => {
|
||||||
|
return ids
|
||||||
|
.map(id => {
|
||||||
|
const team = teams.find(i => i.id === id);
|
||||||
|
return team ? team.name : '';
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveLabels = (labels, ids) => {
|
||||||
|
return ids
|
||||||
|
.map(id => {
|
||||||
|
const label = labels.find(i => i.title === id);
|
||||||
|
return label ? label.title : '';
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
};
|
|
@ -8,10 +8,14 @@ export default {
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/settings/macros'),
|
path: frontendURL('accounts/:accountId/settings/macros'),
|
||||||
component: SettingsContent,
|
component: SettingsContent,
|
||||||
props: {
|
props: params => {
|
||||||
headerTitle: 'MACROS.HEADER',
|
const showBackButton = params.name !== 'macros_wrapper';
|
||||||
icon: 'flash-settings',
|
return {
|
||||||
showNewButton: false,
|
headerTitle: 'MACROS.HEADER',
|
||||||
|
headerButtonText: 'MACROS.HEADER_BTN_TXT',
|
||||||
|
icon: 'flash-settings',
|
||||||
|
showBackButton,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,14 +2,16 @@ import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
|
||||||
import accounts from './modules/accounts';
|
import accounts from './modules/accounts';
|
||||||
import agents from './modules/agents';
|
|
||||||
import agentBots from './modules/agentBots';
|
import agentBots from './modules/agentBots';
|
||||||
|
import agents from './modules/agents';
|
||||||
|
import articles from './modules/helpCenterArticles';
|
||||||
import attributes from './modules/attributes';
|
import attributes from './modules/attributes';
|
||||||
import auth from './modules/auth';
|
import auth from './modules/auth';
|
||||||
import automations from './modules/automations';
|
import automations from './modules/automations';
|
||||||
import bulkActions from './modules/bulkActions';
|
import bulkActions from './modules/bulkActions';
|
||||||
import campaigns from './modules/campaigns';
|
import campaigns from './modules/campaigns';
|
||||||
import cannedResponse from './modules/cannedResponse';
|
import cannedResponse from './modules/cannedResponse';
|
||||||
|
import categories from './modules/helpCenterCategories';
|
||||||
import contactConversations from './modules/contactConversations';
|
import contactConversations from './modules/contactConversations';
|
||||||
import contactLabels from './modules/contactLabels';
|
import contactLabels from './modules/contactLabels';
|
||||||
import contactNotes from './modules/contactNotes';
|
import contactNotes from './modules/contactNotes';
|
||||||
|
@ -30,28 +32,29 @@ import inboxes from './modules/inboxes';
|
||||||
import inboxMembers from './modules/inboxMembers';
|
import inboxMembers from './modules/inboxMembers';
|
||||||
import integrations from './modules/integrations';
|
import integrations from './modules/integrations';
|
||||||
import labels from './modules/labels';
|
import labels from './modules/labels';
|
||||||
|
import macros from './modules/macros';
|
||||||
import notifications from './modules/notifications';
|
import notifications from './modules/notifications';
|
||||||
|
import portals from './modules/helpCenterPortals';
|
||||||
import reports from './modules/reports';
|
import reports from './modules/reports';
|
||||||
import teamMembers from './modules/teamMembers';
|
import teamMembers from './modules/teamMembers';
|
||||||
import teams from './modules/teams';
|
import teams from './modules/teams';
|
||||||
import userNotificationSettings from './modules/userNotificationSettings';
|
import userNotificationSettings from './modules/userNotificationSettings';
|
||||||
import webhooks from './modules/webhooks';
|
import webhooks from './modules/webhooks';
|
||||||
import articles from './modules/helpCenterArticles';
|
|
||||||
import portals from './modules/helpCenterPortals';
|
|
||||||
import categories from './modules/helpCenterCategories';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
accounts,
|
accounts,
|
||||||
agents,
|
|
||||||
agentBots,
|
agentBots,
|
||||||
|
agents,
|
||||||
|
articles,
|
||||||
attributes,
|
attributes,
|
||||||
auth,
|
auth,
|
||||||
automations,
|
automations,
|
||||||
bulkActions,
|
bulkActions,
|
||||||
campaigns,
|
campaigns,
|
||||||
cannedResponse,
|
cannedResponse,
|
||||||
|
categories,
|
||||||
contactConversations,
|
contactConversations,
|
||||||
contactLabels,
|
contactLabels,
|
||||||
contactNotes,
|
contactNotes,
|
||||||
|
@ -72,14 +75,13 @@ export default new Vuex.Store({
|
||||||
inboxMembers,
|
inboxMembers,
|
||||||
integrations,
|
integrations,
|
||||||
labels,
|
labels,
|
||||||
|
macros,
|
||||||
notifications,
|
notifications,
|
||||||
|
portals,
|
||||||
reports,
|
reports,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
teams,
|
teams,
|
||||||
userNotificationSettings,
|
userNotificationSettings,
|
||||||
webhooks,
|
webhooks,
|
||||||
articles,
|
|
||||||
portals,
|
|
||||||
categories,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -252,6 +252,12 @@ const actions = {
|
||||||
meta: { sender },
|
meta: { sender },
|
||||||
} = conversation;
|
} = conversation;
|
||||||
commit(types.UPDATE_CONVERSATION, conversation);
|
commit(types.UPDATE_CONVERSATION, conversation);
|
||||||
|
|
||||||
|
dispatch('conversationLabels/setConversationLabel', {
|
||||||
|
id: conversation.id,
|
||||||
|
data: conversation.labels,
|
||||||
|
});
|
||||||
|
|
||||||
dispatch('contacts/setContact', sender);
|
dispatch('contacts/setContact', sender);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ import categoriesAPI from 'dashboard/api/helpCenter/categories.js';
|
||||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
import types from '../../mutation-types';
|
import types from '../../mutation-types';
|
||||||
export const actions = {
|
export const actions = {
|
||||||
index: async ({ commit }, { portalSlug }) => {
|
index: async ({ commit }, { portalSlug, locale }) => {
|
||||||
try {
|
try {
|
||||||
commit(types.SET_UI_FLAG, { isFetching: true });
|
commit(types.SET_UI_FLAG, { isFetching: true });
|
||||||
if (portalSlug) {
|
if (portalSlug) {
|
||||||
const {
|
const {
|
||||||
data: { payload },
|
data: { payload },
|
||||||
} = await categoriesAPI.get({ portalSlug });
|
} = await categoriesAPI.get({ portalSlug, locale });
|
||||||
commit(types.CLEAR_CATEGORIES);
|
commit(types.CLEAR_CATEGORIES);
|
||||||
const categoryIds = payload.map(category => category.id);
|
const categoryIds = payload.map(category => category.id);
|
||||||
commit(types.ADD_MANY_CATEGORIES, payload);
|
commit(types.ADD_MANY_CATEGORIES, payload);
|
||||||
|
|
|
@ -7,14 +7,12 @@ export const actions = {
|
||||||
try {
|
try {
|
||||||
commit(types.SET_UI_FLAG, { isFetching: true });
|
commit(types.SET_UI_FLAG, { isFetching: true });
|
||||||
const {
|
const {
|
||||||
data: { payload, meta },
|
data: { payload },
|
||||||
} = await portalAPIs.get();
|
} = await portalAPIs.get();
|
||||||
commit(types.CLEAR_PORTALS);
|
commit(types.CLEAR_PORTALS);
|
||||||
const portalSlugs = payload.map(portal => portal.slug);
|
const portalSlugs = payload.map(portal => portal.slug);
|
||||||
commit(types.ADD_MANY_PORTALS_ENTRY, payload);
|
commit(types.ADD_MANY_PORTALS_ENTRY, payload);
|
||||||
commit(types.ADD_MANY_PORTALS_IDS, portalSlugs);
|
commit(types.ADD_MANY_PORTALS_IDS, portalSlugs);
|
||||||
|
|
||||||
commit(types.SET_PORTALS_META, meta);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throwErrorMessage(error);
|
throwErrorMessage(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -22,6 +20,21 @@ export const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
show: async ({ commit }, { portalSlug, locale }) => {
|
||||||
|
commit(types.SET_UI_FLAG, { isFetchingItem: true });
|
||||||
|
try {
|
||||||
|
const response = await portalAPIs.getPortal({ portalSlug, locale });
|
||||||
|
const {
|
||||||
|
data: { meta },
|
||||||
|
} = response;
|
||||||
|
commit(types.SET_PORTALS_META, meta);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_UI_FLAG, { isFetchingItem: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
create: async ({ commit }, params) => {
|
create: async ({ commit }, params) => {
|
||||||
commit(types.SET_UI_FLAG, { isCreating: true });
|
commit(types.SET_UI_FLAG, { isCreating: true });
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -20,7 +20,5 @@ export const getters = {
|
||||||
return portals;
|
return portals;
|
||||||
},
|
},
|
||||||
count: state => state.portals.allIds.length || 0,
|
count: state => state.portals.allIds.length || 0,
|
||||||
getMeta: state => {
|
getMeta: state => state.meta,
|
||||||
return state.meta;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,8 +10,10 @@ export const defaultPortalFlags = {
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
meta: {
|
meta: {
|
||||||
count: 0,
|
allArticlesCount: 0,
|
||||||
currentPage: 1,
|
mineArticlesCount: 0,
|
||||||
|
draftArticlesCount: 0,
|
||||||
|
archivedArticlesCount: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
portals: {
|
portals: {
|
||||||
|
|
|
@ -44,9 +44,16 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_PORTALS_META]: ($state, data) => {
|
[types.SET_PORTALS_META]: ($state, data) => {
|
||||||
const { portals_count: count, current_page: currentPage } = data;
|
const {
|
||||||
Vue.set($state.meta, 'count', count);
|
all_articles_count: allArticlesCount = 0,
|
||||||
Vue.set($state.meta, 'currentPage', currentPage);
|
mine_articles_count: mineArticlesCount = 0,
|
||||||
|
draft_articles_count: draftArticlesCount = 0,
|
||||||
|
archived_articles_count: archivedArticlesCount = 0,
|
||||||
|
} = data;
|
||||||
|
Vue.set($state.meta, 'allArticlesCount', allArticlesCount);
|
||||||
|
Vue.set($state.meta, 'archivedArticlesCount', archivedArticlesCount);
|
||||||
|
Vue.set($state.meta, 'mineArticlesCount', mineArticlesCount);
|
||||||
|
Vue.set($state.meta, 'draftArticlesCount', draftArticlesCount);
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.ADD_PORTAL_ID]($state, portalSlug) {
|
[types.ADD_PORTAL_ID]($state, portalSlug) {
|
||||||
|
|
|
@ -22,7 +22,6 @@ describe('#actions', () => {
|
||||||
[types.CLEAR_PORTALS],
|
[types.CLEAR_PORTALS],
|
||||||
[types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload],
|
[types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload],
|
||||||
[types.ADD_MANY_PORTALS_IDS, ['domain', 'campaign']],
|
[types.ADD_MANY_PORTALS_IDS, ['domain', 'campaign']],
|
||||||
[types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }],
|
|
||||||
[types.SET_UI_FLAG, { isFetching: false }],
|
[types.SET_UI_FLAG, { isFetching: false }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -66,6 +65,36 @@ describe('#actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#show', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.get.mockResolvedValue({
|
||||||
|
data: { meta: { all_articles_count: 1 } },
|
||||||
|
});
|
||||||
|
await actions.show(
|
||||||
|
{ commit },
|
||||||
|
{
|
||||||
|
portalSlug: 'handbook',
|
||||||
|
locale: 'en',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_UI_FLAG, { isFetchingItem: true }],
|
||||||
|
[types.SET_PORTALS_META, { all_articles_count: 1 }],
|
||||||
|
[types.SET_UI_FLAG, { isFetchingItem: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(
|
||||||
|
actions.create({ commit, dispatch, state: { portals: {} } }, {})
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_UI_FLAG, { isCreating: true }],
|
||||||
|
[types.SET_UI_FLAG, { isCreating: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#update', () => {
|
describe('#update', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.patch.mockResolvedValue({ data: apiResponse.payload[1] });
|
axios.patch.mockResolvedValue({ data: apiResponse.payload[1] });
|
||||||
|
|
|
@ -107,12 +107,20 @@ describe('#mutations', () => {
|
||||||
describe('#SET_PORTALS_META', () => {
|
describe('#SET_PORTALS_META', () => {
|
||||||
it('add meta to state', () => {
|
it('add meta to state', () => {
|
||||||
mutations[types.SET_PORTALS_META](state, {
|
mutations[types.SET_PORTALS_META](state, {
|
||||||
portals_count: 10,
|
|
||||||
current_page: 1,
|
|
||||||
});
|
|
||||||
expect(state.meta).toEqual({
|
|
||||||
count: 10,
|
count: 10,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
all_articles_count: 10,
|
||||||
|
archived_articles_count: 10,
|
||||||
|
draft_articles_count: 10,
|
||||||
|
mine_articles_count: 10,
|
||||||
|
});
|
||||||
|
expect(state.meta).toEqual({
|
||||||
|
count: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
allArticlesCount: 10,
|
||||||
|
archivedArticlesCount: 10,
|
||||||
|
draftArticlesCount: 10,
|
||||||
|
mineArticlesCount: 10,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,6 +58,7 @@ describe('#actions', () => {
|
||||||
id: 1,
|
id: 1,
|
||||||
messages: [],
|
messages: [],
|
||||||
meta: { sender: { id: 1, name: 'john-doe' } },
|
meta: { sender: { id: 1, name: 'john-doe' } },
|
||||||
|
labels: ['support'],
|
||||||
};
|
};
|
||||||
actions.updateConversation(
|
actions.updateConversation(
|
||||||
{ commit, rootState: { route: { name: 'home' } }, dispatch },
|
{ commit, rootState: { route: { name: 'home' } }, dispatch },
|
||||||
|
@ -67,6 +68,10 @@ describe('#actions', () => {
|
||||||
[types.UPDATE_CONVERSATION, conversation],
|
[types.UPDATE_CONVERSATION, conversation],
|
||||||
]);
|
]);
|
||||||
expect(dispatch.mock.calls).toEqual([
|
expect(dispatch.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
'conversationLabels/setConversationLabel',
|
||||||
|
{ id: 1, data: ['support'] },
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'contacts/setContact',
|
'contacts/setContact',
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
// a relevant structure within app/javascript and only use these pack files to reference
|
// a relevant structure within app/javascript and only use these pack files to reference
|
||||||
// that code so that it will be compiled.
|
// that code so that it will be compiled.
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
import Turbolinks from 'turbolinks';
|
import Turbolinks from 'turbolinks';
|
||||||
|
import PublicArticleSearch from '../portal/components/PublicArticleSearch.vue';
|
||||||
|
|
||||||
import { navigateToLocalePage } from '../portal/portalHelpers';
|
import { navigateToLocalePage } from '../portal/portalHelpers';
|
||||||
|
|
||||||
|
@ -13,4 +15,21 @@ import '../portal/application.scss';
|
||||||
Rails.start();
|
Rails.start();
|
||||||
Turbolinks.start();
|
Turbolinks.start();
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', navigateToLocalePage);
|
const initPageSetUp = () => {
|
||||||
|
navigateToLocalePage();
|
||||||
|
const isSearchContainerAvailable = document.querySelector('#search-wrap');
|
||||||
|
if (isSearchContainerAvailable) {
|
||||||
|
new Vue({
|
||||||
|
components: { PublicArticleSearch },
|
||||||
|
template: '<PublicArticleSearch />',
|
||||||
|
}).$mount('#search-wrap');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initPageSetUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('turbolinks:load', () => {
|
||||||
|
initPageSetUp();
|
||||||
|
});
|
||||||
|
|
|
@ -18,6 +18,11 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatwootSettings = window.chatwootSettings || {};
|
const chatwootSettings = window.chatwootSettings || {};
|
||||||
|
let locale = chatwootSettings.locale || 'en';
|
||||||
|
if (chatwootSettings.useBrowserLanguage) {
|
||||||
|
locale = window.navigator.language.replace('-', '_');
|
||||||
|
}
|
||||||
|
|
||||||
window.$chatwoot = {
|
window.$chatwoot = {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
hasLoaded: false,
|
hasLoaded: false,
|
||||||
|
@ -25,7 +30,8 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
position: chatwootSettings.position === 'left' ? 'left' : 'right',
|
position: chatwootSettings.position === 'left' ? 'left' : 'right',
|
||||||
websiteToken,
|
websiteToken,
|
||||||
locale: chatwootSettings.locale,
|
locale,
|
||||||
|
useBrowserLanguage: chatwootSettings.useBrowserLanguage || false,
|
||||||
type: getBubbleView(chatwootSettings.type),
|
type: getBubbleView(chatwootSettings.type),
|
||||||
launcherTitle: chatwootSettings.launcherTitle || '',
|
launcherTitle: chatwootSettings.launcherTitle || '',
|
||||||
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
||||||
|
@ -58,7 +64,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
IFrameHelper.events.popoutChatWindow({
|
IFrameHelper.events.popoutChatWindow({
|
||||||
baseUrl: window.$chatwoot.baseUrl,
|
baseUrl: window.$chatwoot.baseUrl,
|
||||||
websiteToken: window.$chatwoot.websiteToken,
|
websiteToken: window.$chatwoot.websiteToken,
|
||||||
locale: window.$chatwoot.locale,
|
locale,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
14
app/javascript/portal/api/article.js
Normal file
14
app/javascript/portal/api/article.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
class ArticlesAPI {
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
searchArticles(portalSlug, locale, query) {
|
||||||
|
let baseUrl = `${this.baseUrl}/hc/${portalSlug}/${locale}/articles.json?query=${query}`;
|
||||||
|
return axios.get(baseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ArticlesAPI();
|
129
app/javascript/portal/components/PublicArticleSearch.vue
Normal file
129
app/javascript/portal/components/PublicArticleSearch.vue
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-on-clickaway="closeSearch"
|
||||||
|
class="mx-auto max-w-md w-full relative my-4"
|
||||||
|
>
|
||||||
|
<public-search-input
|
||||||
|
v-model="searchTerm"
|
||||||
|
:search-placeholder="searchTranslations.searchPlaceholder"
|
||||||
|
@focus="openSearch"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowSearchBox"
|
||||||
|
class="absolute show-search-box w-full"
|
||||||
|
@mouseover="openSearch"
|
||||||
|
>
|
||||||
|
<search-suggestions
|
||||||
|
:items="searchResults"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:empty-placeholder="searchTranslations.emptyPlaceholder"
|
||||||
|
:results-title="searchTranslations.resultsTitle"
|
||||||
|
:loading-placeholder="searchTranslations.loadingPlaceholder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
|
||||||
|
import SearchSuggestions from './SearchSuggestions';
|
||||||
|
import PublicSearchInput from './PublicSearchInput';
|
||||||
|
|
||||||
|
import ArticlesAPI from '../api/article';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PublicSearchInput,
|
||||||
|
SearchSuggestions,
|
||||||
|
},
|
||||||
|
mixins: [clickaway],
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchTerm: '',
|
||||||
|
isLoading: false,
|
||||||
|
showSearchBox: false,
|
||||||
|
searchResults: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
portalSlug() {
|
||||||
|
return window.portalConfig.portalSlug;
|
||||||
|
},
|
||||||
|
localeCode() {
|
||||||
|
return window.portalConfig.localeCode;
|
||||||
|
},
|
||||||
|
shouldShowSearchBox() {
|
||||||
|
return this.searchTerm !== '' && this.showSearchBox;
|
||||||
|
},
|
||||||
|
searchTranslations() {
|
||||||
|
const { searchTranslations = {} } = window.portalConfig;
|
||||||
|
return searchTranslations;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
searchTerm() {
|
||||||
|
if (this.typingTimer) {
|
||||||
|
clearTimeout(this.typingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openSearch();
|
||||||
|
this.isLoading = true;
|
||||||
|
this.typingTimer = setTimeout(() => {
|
||||||
|
this.fetchArticlesByQuery();
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
currentPage() {
|
||||||
|
this.clearSearchTerm();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onChange(e) {
|
||||||
|
this.$emit('input', e.target.value);
|
||||||
|
},
|
||||||
|
onBlur(e) {
|
||||||
|
this.$emit('blur', e.target.value);
|
||||||
|
},
|
||||||
|
openSearch() {
|
||||||
|
this.showSearchBox = true;
|
||||||
|
},
|
||||||
|
closeSearch() {
|
||||||
|
this.showSearchBox = false;
|
||||||
|
},
|
||||||
|
clearSearchTerm() {
|
||||||
|
this.searchTerm = '';
|
||||||
|
},
|
||||||
|
async fetchArticlesByQuery() {
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.searchResults = [];
|
||||||
|
const { data } = await ArticlesAPI.searchArticles(
|
||||||
|
this.portalSlug,
|
||||||
|
this.localeCode,
|
||||||
|
this.searchTerm
|
||||||
|
);
|
||||||
|
this.searchResults = data.payload;
|
||||||
|
this.isLoading = true;
|
||||||
|
} catch (error) {
|
||||||
|
// Show something wrong message
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.show-search-box {
|
||||||
|
top: 4rem;
|
||||||
|
}
|
||||||
|
</style>
|
60
app/javascript/portal/components/PublicSearchInput.vue
Normal file
60
app/javascript/portal/components/PublicSearchInput.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="w-full flex items-center rounded-md border-solid h-16 bg-white px-4 py-2 text-slate-600"
|
||||||
|
:class="{
|
||||||
|
'shadow border-2 border-woot-100': isFocused,
|
||||||
|
'border border-slate-50 shadow-sm': !isFocused,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="search" />
|
||||||
|
<input
|
||||||
|
:value="value"
|
||||||
|
type="text"
|
||||||
|
class="w-full search-input focus:outline-none text-base h-full bg-white px-2 py-2
|
||||||
|
text-slate-700 placeholder-slate-500 sm:text-sm"
|
||||||
|
:placeholder="searchPlaceholder"
|
||||||
|
role="search"
|
||||||
|
@input="onChange"
|
||||||
|
@focus="onFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
FluentIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isFocused: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(e) {
|
||||||
|
this.$emit('input', e.target.value);
|
||||||
|
},
|
||||||
|
onFocus(e) {
|
||||||
|
this.isFocused = true;
|
||||||
|
this.$emit('focus', e.target.value);
|
||||||
|
},
|
||||||
|
onBlur(e) {
|
||||||
|
this.isFocused = false;
|
||||||
|
this.$emit('blur', e.target.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
103
app/javascript/portal/components/SearchSuggestions.vue
Normal file
103
app/javascript/portal/components/SearchSuggestions.vue
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="shadow-md bg-white mt-2 max-h-72 scroll-py-2 p-4 rounded overflow-y-auto text-sm text-slate-700"
|
||||||
|
>
|
||||||
|
<div v-if="isLoading" class="font-medium text-sm text-slate-400">
|
||||||
|
{{ loadingPlaceholder }}
|
||||||
|
</div>
|
||||||
|
<h3 v-if="shouldShowResults" class="font-medium text-sm text-slate-400">
|
||||||
|
{{ resultsTitle }}
|
||||||
|
</h3>
|
||||||
|
<ul
|
||||||
|
v-if="shouldShowResults"
|
||||||
|
class="bg-white mt-2 max-h-72 scroll-py-2 overflow-y-auto text-sm text-slate-700"
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(article, index) in items"
|
||||||
|
:id="article.id"
|
||||||
|
:key="article.id"
|
||||||
|
class="group flex cursor-default select-none items-center rounded-md p-2 mb-1"
|
||||||
|
:class="{ 'bg-slate-25': index === selectedIndex }"
|
||||||
|
role="option"
|
||||||
|
tabindex="-1"
|
||||||
|
@mouseover="onHover(index)"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="generateArticleUrl(article)"
|
||||||
|
class="flex-auto truncate text-base font-medium leading-6 w-full hover:underline"
|
||||||
|
>
|
||||||
|
{{ article.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div v-if="showEmptyResults" class="font-medium text-sm text-slate-400">
|
||||||
|
{{ emptyPlaceholder }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mentionSelectionKeyboardMixin from 'dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [mentionSelectionKeyboardMixin],
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
emptyPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
loadingPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
resultsTitle: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedIndex: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showEmptyResults() {
|
||||||
|
return !this.items.length && !this.isLoading;
|
||||||
|
},
|
||||||
|
shouldShowResults() {
|
||||||
|
return this.items.length && !this.isLoading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
generateArticleUrl(article) {
|
||||||
|
return `/hc/${article.portal.slug}/${article.category.locale}/${article.category.slug}/${article.id}`;
|
||||||
|
},
|
||||||
|
handleKeyboardEvent(e) {
|
||||||
|
this.processKeyDownEvent(e);
|
||||||
|
this.$el.scrollTop = 40 * this.selectedIndex;
|
||||||
|
},
|
||||||
|
onHover(index) {
|
||||||
|
this.selectedIndex = index;
|
||||||
|
},
|
||||||
|
onSelect() {
|
||||||
|
window.location = this.generateArticleUrl(this.items[this.selectedIndex]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,8 +1,13 @@
|
||||||
export const navigateToLocalePage = () => {
|
export const navigateToLocalePage = () => {
|
||||||
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
||||||
|
|
||||||
|
if (!allLocaleSwitcher) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const { portalSlug } = allLocaleSwitcher.dataset;
|
const { portalSlug } = allLocaleSwitcher.dataset;
|
||||||
allLocaleSwitcher.addEventListener('change', event => {
|
allLocaleSwitcher.addEventListener('change', event => {
|
||||||
window.location = `/hc/${portalSlug}/${event.target.value}/`;
|
window.location = `/hc/${portalSlug}/${event.target.value}/`;
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
:root {
|
:root {
|
||||||
// border-radius
|
--border-radius-small: 0.3rem;
|
||||||
--border-radius-small: 0.3rem;
|
--border-radius-normal: 0.5rem;
|
||||||
--border-radius-normal: 0.5rem;
|
--border-radius-medium: 0.7rem;
|
||||||
--border-radius-medium: 0.7rem;
|
--border-radius-large: 0.9rem;
|
||||||
--border-radius-large: 0.9rem;
|
--border-radius-full: 10rem;
|
||||||
--border-radius-rounded: 50%;
|
--border-radius-rounded: 50%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,10 @@
|
||||||
--space-jumbo: 6.4rem;
|
--space-jumbo: 6.4rem;
|
||||||
--space-mega: 10rem;
|
--space-mega: 10rem;
|
||||||
--space-giga: 24rem;
|
--space-giga: 24rem;
|
||||||
|
|
||||||
--space-minus-micro: -0.2rem;
|
--space-minus-micro: -0.2rem;
|
||||||
--space-minus-smaller: -0.4rem;
|
--space-minus-smaller: -0.4rem;
|
||||||
|
--space-minus-half: -0.5rem;
|
||||||
--space-minus-small: -0.8rem;
|
--space-minus-small: -0.8rem;
|
||||||
--space-minus-one: -1rem;
|
--space-minus-one: -1rem;
|
||||||
--space-minus-slab: -1.2rem;
|
--space-minus-slab: -1.2rem;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
:key="action.uri"
|
:key="action.uri"
|
||||||
class="action-button button"
|
class="action-button button"
|
||||||
:href="action.uri"
|
:href="action.uri"
|
||||||
|
:style="{ background: widgetColor, borderColor: widgetColor }"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener nofollow noreferrer"
|
rel="noopener nofollow noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -13,13 +14,14 @@
|
||||||
v-else
|
v-else
|
||||||
:key="action.payload"
|
:key="action.payload"
|
||||||
class="action-button button"
|
class="action-button button"
|
||||||
|
:style="{ borderColor: widgetColor, color: widgetColor }"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
{{ action.text }}
|
{{ action.text }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
|
@ -29,6 +31,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
}),
|
||||||
isLink() {
|
isLink() {
|
||||||
return this.action.type === 'link';
|
return this.action.type === 'link';
|
||||||
},
|
},
|
||||||
|
|
|
@ -107,6 +107,7 @@
|
||||||
"microphone-stop-outline": "M18,18H6V6H18V18Z",
|
"microphone-stop-outline": "M18,18H6V6H18V18Z",
|
||||||
"microphone-pause-outline": "M14,19H18V5H14M6,19H10V5H6V19Z",
|
"microphone-pause-outline": "M14,19H18V5H14M6,19H10V5H6V19Z",
|
||||||
"microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z",
|
"microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z",
|
||||||
|
"navigation-outline": "M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6l18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z",
|
||||||
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
||||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||||
"panel-sidebar-outline": "M4.75 4A2.75 2.75 0 0 0 2 6.75v10.5A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75ZM9 18.5v-13h10.25c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9ZM5.5 3.5h4M5.5 5.5h4M5.5 7.5h4M5.5 9.5h4",
|
"panel-sidebar-outline": "M4.75 4A2.75 2.75 0 0 0 2 6.75v10.5A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75ZM9 18.5v-13h10.25c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9ZM5.5 3.5h4M5.5 5.5h4M5.5 7.5h4M5.5 9.5h4",
|
||||||
|
@ -118,6 +119,7 @@
|
||||||
"person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z",
|
"person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z",
|
||||||
"person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z",
|
"person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z",
|
||||||
"person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z",
|
"person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z",
|
||||||
|
"play-circle-outline": "M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12Zm8.856-3.845A1.25 1.25 0 0 0 9 9.248v5.504a1.25 1.25 0 0 0 1.856 1.093l5.757-3.189a.75.75 0 0 0 0-1.312l-5.757-3.189Z",
|
||||||
"power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z",
|
"power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z",
|
||||||
"quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z",
|
"quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z",
|
||||||
"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",
|
"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",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
|
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
|
||||||
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
||||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||||
|
"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-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",
|
||||||
"sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z","M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z","M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
|
"sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z", "M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z", "M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,30 +4,46 @@ import { IFrameHelper } from 'widget/helpers/utils';
|
||||||
import { showBadgeOnFavicon } from './faviconHelper';
|
import { showBadgeOnFavicon } from './faviconHelper';
|
||||||
|
|
||||||
export const initOnEvents = ['click', 'touchstart', 'keypress', 'keydown'];
|
export const initOnEvents = ['click', 'touchstart', 'keypress', 'keydown'];
|
||||||
|
|
||||||
|
export const getAudioContext = () => {
|
||||||
|
let audioCtx;
|
||||||
|
try {
|
||||||
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
} catch {
|
||||||
|
// AudioContext is not available.
|
||||||
|
}
|
||||||
|
return audioCtx;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => {
|
export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => {
|
||||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
const audioCtx = getAudioContext();
|
||||||
|
|
||||||
const playsound = audioBuffer => {
|
const playsound = audioBuffer => {
|
||||||
window.playAudioAlert = () => {
|
window.playAudioAlert = () => {
|
||||||
const source = audioCtx.createBufferSource();
|
if (audioCtx) {
|
||||||
source.buffer = audioBuffer;
|
const source = audioCtx.createBufferSource();
|
||||||
source.connect(audioCtx.destination);
|
source.buffer = audioBuffer;
|
||||||
source.loop = false;
|
source.connect(audioCtx.destination);
|
||||||
source.start();
|
source.loop = false;
|
||||||
|
source.start();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourceUrl = `${baseUrl}/audio/${type}/ding.mp3`;
|
if (audioCtx) {
|
||||||
const audioRequest = new Request(resourceUrl);
|
const resourceUrl = `${baseUrl}/audio/${type}/ding.mp3`;
|
||||||
|
const audioRequest = new Request(resourceUrl);
|
||||||
|
|
||||||
fetch(audioRequest)
|
fetch(audioRequest)
|
||||||
.then(response => response.arrayBuffer())
|
.then(response => response.arrayBuffer())
|
||||||
.then(buffer => {
|
.then(buffer => {
|
||||||
audioCtx.decodeAudioData(buffer).then(playsound);
|
audioCtx.decodeAudioData(buffer).then(playsound);
|
||||||
return new Promise(res => res());
|
return new Promise(res => res());
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// error
|
// error
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notificationEnabled = (enableAudioAlerts, id, userId) => {
|
export const notificationEnabled = (enableAudioAlerts, id, userId) => {
|
||||||
|
|
|
@ -11,11 +11,7 @@ export const formatBytes = (bytes, decimals = 2) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fileSizeInMegaBytes = bytes => {
|
export const fileSizeInMegaBytes = bytes => {
|
||||||
if (bytes === 0) {
|
return bytes / (1024 * 1024);
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const sizeInMB = (bytes / (1024 * 1024)).toFixed(2);
|
|
||||||
return sizeInMB;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkFileSizeLimit = (file, maximumUploadLimit) => {
|
export const checkFileSizeLimit = (file, maximumUploadLimit) => {
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('#File Helpers', () => {
|
||||||
expect(fileSizeInMegaBytes(0)).toBe(0);
|
expect(fileSizeInMegaBytes(0)).toBe(0);
|
||||||
});
|
});
|
||||||
it('should return 19.07 if 20000000 is passed', () => {
|
it('should return 19.07 if 20000000 is passed', () => {
|
||||||
expect(fileSizeInMegaBytes(20000000)).toBe('19.07');
|
expect(fileSizeInMegaBytes(20000000)).toBeCloseTo(19.07, 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('checkFileSizeLimit', () => {
|
describe('checkFileSizeLimit', () => {
|
||||||
|
|
|
@ -134,10 +134,20 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setLocale(locale) {
|
setLocale(localeWithVariation) {
|
||||||
const { enabledLanguages } = window.chatwootWebChannel;
|
const { enabledLanguages } = window.chatwootWebChannel;
|
||||||
if (enabledLanguages.some(lang => lang.iso_639_1_code === locale)) {
|
const localeWithoutVariation = localeWithVariation.split('_')[0];
|
||||||
this.$root.$i18n.locale = locale;
|
const hasLocaleWithoutVariation = enabledLanguages.some(
|
||||||
|
lang => lang.iso_639_1_code === localeWithoutVariation
|
||||||
|
);
|
||||||
|
const hasLocaleWithVariation = enabledLanguages.some(
|
||||||
|
lang => lang.iso_639_1_code === localeWithVariation
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasLocaleWithVariation) {
|
||||||
|
this.$root.$i18n.locale = localeWithVariation;
|
||||||
|
} else if (hasLocaleWithoutVariation) {
|
||||||
|
this.$root.$i18n.locale = localeWithoutVariation;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
registerUnreadEvents() {
|
registerUnreadEvents() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<file-upload
|
<file-upload
|
||||||
|
ref="upload"
|
||||||
:size="4096 * 2048"
|
:size="4096 * 2048"
|
||||||
:accept="allowedFileTypes"
|
:accept="allowedFileTypes"
|
||||||
:data="{
|
:data="{
|
||||||
|
@ -48,7 +49,23 @@ export default {
|
||||||
return ALLOWED_FILE_TYPES;
|
return ALLOWED_FILE_TYPES;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('paste', this.handleClipboardPaste);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
document.removeEventListener('paste', this.handleClipboardPaste);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleClipboardPaste(e) {
|
||||||
|
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = item.getAsFile();
|
||||||
|
this.$refs.upload.add(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
getFileType(fileType) {
|
getFileType(fileType) {
|
||||||
return fileType.includes('image') ? 'image' : 'file';
|
return fileType.includes('image') ? 'image' : 'file';
|
||||||
},
|
},
|
||||||
|
|
|
@ -191,6 +191,7 @@ export default {
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
right: $space-smaller;
|
right: $space-smaller;
|
||||||
top: -278px;
|
top: -278px;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
right: $space-one;
|
right: $space-one;
|
||||||
|
|
|
@ -52,6 +52,13 @@ class ActionCableListener < BaseListener
|
||||||
broadcast(account, tokens, CONVERSATION_STATUS_CHANGED, conversation.push_event_data)
|
broadcast(account, tokens, CONVERSATION_STATUS_CHANGED, conversation.push_event_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_updated(event)
|
||||||
|
conversation, account = extract_conversation_and_account(event)
|
||||||
|
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
|
||||||
|
|
||||||
|
broadcast(account, tokens, CONVERSATION_UPDATED, conversation.push_event_data)
|
||||||
|
end
|
||||||
|
|
||||||
def conversation_typing_on(event)
|
def conversation_typing_on(event)
|
||||||
conversation = event.data[:conversation]
|
conversation = event.data[:conversation]
|
||||||
account = conversation.account
|
account = conversation.account
|
||||||
|
|
|
@ -7,6 +7,8 @@ class SupportMailbox < ApplicationMailbox
|
||||||
:decorate_mail
|
:decorate_mail
|
||||||
|
|
||||||
def process
|
def process
|
||||||
|
# to turn off spam conversation creation
|
||||||
|
return unless @account.active?
|
||||||
# prevent loop from chatwoot notification emails
|
# prevent loop from chatwoot notification emails
|
||||||
return if notification_email_from_chatwoot?
|
return if notification_email_from_chatwoot?
|
||||||
|
|
||||||
|
|
|
@ -83,11 +83,7 @@ class Article < ApplicationRecord
|
||||||
).search_by_category_locale(params[:locale]).search_by_author(params[:author_id]).search_by_status(params[:status])
|
).search_by_category_locale(params[:locale]).search_by_author(params[:author_id]).search_by_status(params[:status])
|
||||||
|
|
||||||
records = records.text_search(params[:query]) if params[:query].present?
|
records = records.text_search(params[:query]) if params[:query].present?
|
||||||
records.page(current_page(params))
|
records
|
||||||
end
|
|
||||||
|
|
||||||
def self.current_page(params)
|
|
||||||
params[:page] || 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def associate_root_article(associated_article_id)
|
def associate_root_article(associated_article_id)
|
||||||
|
|
|
@ -220,7 +220,7 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
def notify_conversation_updation
|
def notify_conversation_updation
|
||||||
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
||||||
custom_attributes]).present?
|
custom_attributes label_list]).present?
|
||||||
|
|
||||||
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,17 +19,21 @@
|
||||||
# index_macros_on_updated_by_id (updated_by_id)
|
# index_macros_on_updated_by_id (updated_by_id)
|
||||||
#
|
#
|
||||||
class Macro < ApplicationRecord
|
class Macro < ApplicationRecord
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :created_by,
|
belongs_to :created_by,
|
||||||
class_name: :User
|
class_name: :User
|
||||||
belongs_to :updated_by,
|
belongs_to :updated_by,
|
||||||
class_name: :User
|
class_name: :User
|
||||||
|
has_many_attached :files
|
||||||
|
|
||||||
enum visibility: { personal: 0, global: 1 }
|
enum visibility: { personal: 0, global: 1 }
|
||||||
|
|
||||||
validate :json_actions_format
|
validate :json_actions_format
|
||||||
|
|
||||||
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agent send_webhook_event mute_conversation change_status
|
ACTIONS_ATTRS = %w[send_message add_label assign_team assign_best_agent mute_conversation change_status
|
||||||
resolve_conversation snooze_conversation].freeze
|
resolve_conversation snooze_conversation send_email_transcript send_attachment].freeze
|
||||||
|
|
||||||
def set_visibility(user, params)
|
def set_visibility(user, params)
|
||||||
self.visibility = params[:visibility]
|
self.visibility = params[:visibility]
|
||||||
|
@ -47,6 +51,20 @@ class Macro < ApplicationRecord
|
||||||
params[:page] || 1
|
params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def file_base_data
|
||||||
|
files.map do |file|
|
||||||
|
{
|
||||||
|
id: file.id,
|
||||||
|
macro_id: id,
|
||||||
|
file_type: file.content_type,
|
||||||
|
account_id: account_id,
|
||||||
|
file_url: url_for(file),
|
||||||
|
blob_id: file.blob_id,
|
||||||
|
filename: file.filename.to_s
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def json_actions_format
|
def json_actions_format
|
||||||
|
|
|
@ -22,4 +22,8 @@ class MacroPolicy < ApplicationPolicy
|
||||||
def execute?
|
def execute?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_file?
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ class Conversations::EventDataPresenter < SimpleDelegator
|
||||||
id: display_id,
|
id: display_id,
|
||||||
inbox_id: inbox_id,
|
inbox_id: inbox_id,
|
||||||
messages: push_messages,
|
messages: push_messages,
|
||||||
|
labels: label_list,
|
||||||
meta: push_meta,
|
meta: push_meta,
|
||||||
status: status,
|
status: status,
|
||||||
custom_attributes: custom_attributes,
|
custom_attributes: custom_attributes,
|
||||||
|
|
|
@ -39,6 +39,12 @@ class ActionService
|
||||||
@conversation.update!(team_id: team_ids[0])
|
@conversation.update!(team_id: team_ids[0])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_email_transcript(emails)
|
||||||
|
emails.each do |email|
|
||||||
|
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def agent_belongs_to_account?(agent_ids)
|
def agent_belongs_to_account?(agent_ids)
|
||||||
|
|
|
@ -26,21 +26,15 @@ class AutomationRules::ActionService < ActionService
|
||||||
|
|
||||||
return unless @rule.files.attached?
|
return unless @rule.files.attached?
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.find(blob_ids)
|
blobs = ActiveStorage::Blob.where(id: blob_ids)
|
||||||
|
|
||||||
return if blob.blank?
|
return if blobs.blank?
|
||||||
|
|
||||||
params = { content: nil, private: false, attachments: blob }
|
params = { content: nil, private: false, attachments: blobs }
|
||||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||||
mb.perform
|
mb.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_email_transcript(emails)
|
|
||||||
emails.each do |email|
|
|
||||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, email)&.deliver_later
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_webhook_event(webhook_url)
|
def send_webhook_event(webhook_url)
|
||||||
payload = @conversation.webhook_data.merge(event: "automation_event.#{@rule.event_name}")
|
payload = @conversation.webhook_data.merge(event: "automation_event.#{@rule.event_name}")
|
||||||
WebhookJob.perform_later(webhook_url[0], payload)
|
WebhookJob.perform_later(webhook_url[0], payload)
|
||||||
|
|
|
@ -21,18 +21,29 @@ class Macros::ExecutionService < ActionService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def send_webhook_event(webhook_url)
|
|
||||||
payload = @conversation.webhook_data.merge(event: "macro_event.#{@macro.name}")
|
|
||||||
WebhookJob.perform_later(webhook_url[0], payload)
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_message(message)
|
def send_message(message)
|
||||||
return if conversation_a_tweet?
|
return if conversation_a_tweet?
|
||||||
|
|
||||||
params = { content: message[0], private: false, content_attributes: { macro_id: @macro.id } }
|
params = { content: message[0], private: false }
|
||||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
|
||||||
|
# Added reload here to ensure conversation us persistent with the latest updates
|
||||||
|
mb = Messages::MessageBuilder.new(nil, @conversation.reload, params)
|
||||||
mb.perform
|
mb.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_email_to_team(_params); end
|
def send_attachment(blob_ids)
|
||||||
|
return if conversation_a_tweet?
|
||||||
|
|
||||||
|
return unless @macro.files.attached?
|
||||||
|
|
||||||
|
blobs = ActiveStorage::Blob.where(id: blob_ids)
|
||||||
|
|
||||||
|
return if blobs.blank?
|
||||||
|
|
||||||
|
params = { content: nil, private: false, attachments: blobs }
|
||||||
|
|
||||||
|
# Added reload here to ensure conversation us persistent with the latest updates
|
||||||
|
mb = Messages::MessageBuilder.new(nil, @conversation.reload, params)
|
||||||
|
mb.perform
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,12 +13,6 @@ json.category do
|
||||||
json.locale article.category.locale
|
json.locale article.category.locale
|
||||||
end
|
end
|
||||||
|
|
||||||
if article.portal.present?
|
|
||||||
json.portal do
|
|
||||||
json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
json.views article.views
|
json.views article.views
|
||||||
|
|
||||||
if article.author.present?
|
if article.author.present?
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue