Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
4e27a9ff3b
136 changed files with 1554 additions and 542 deletions
|
@ -35,7 +35,13 @@ class Messages::MessageBuilder
|
|||
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
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@articles_count = @portal.articles.count
|
||||
@articles = @portal.articles
|
||||
@articles = @articles.search(list_params) if list_params.present?
|
||||
@portal_articles = @portal.articles
|
||||
@all_articles = @portal_articles.search(list_params)
|
||||
@articles_count = @all_articles.count
|
||||
@articles = @all_articles.page(@current_page)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -37,7 +38,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
||||
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
|
||||
end
|
||||
|
||||
def article_params
|
||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
|||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@current_locale = params[:locale]
|
||||
@categories = @portal.categories.search(params)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||
@portal.members << agents
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@all_articles = @portal.articles
|
||||
@articles = @all_articles.search(locale: params[:locale])
|
||||
end
|
||||
|
||||
def create
|
||||
@portal = Current.account.portals.build(portal_params)
|
||||
|
|
|
@ -3,9 +3,15 @@ class Public::Api::V1::InboxesController < PublicController
|
|||
before_action :set_contact_inbox
|
||||
before_action :set_conversation
|
||||
|
||||
def show
|
||||
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:id])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_inbox_channel
|
||||
return if params[:inbox_id].blank?
|
||||
|
||||
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id])
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,12 @@ module FileTypeHelper
|
|||
:file
|
||||
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)
|
||||
[
|
||||
'image/jpeg',
|
||||
|
|
|
@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI {
|
|||
super('categories', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ portalSlug }) {
|
||||
return axios.get(`${this.url}/${portalSlug}/categories`);
|
||||
get({ portalSlug, locale }) {
|
||||
return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`);
|
||||
}
|
||||
|
||||
create({ portalSlug, categoryObj }) {
|
||||
|
|
|
@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient {
|
|||
super('portals', { accountScoped: true });
|
||||
}
|
||||
|
||||
getPortal({ portalSlug, locale }) {
|
||||
return axios.get(`${this.url}/${portalSlug}?locale=${locale}`);
|
||||
}
|
||||
|
||||
updatePortal({ portalSlug, portalObj }) {
|
||||
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
||||
}
|
||||
|
|
|
@ -5,9 +5,12 @@ import Button from './ui/WootButton';
|
|||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
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 DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import FeatureToggle from './widgets/FeatureToggle';
|
||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import Label from './ui/Label';
|
||||
|
@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton';
|
|||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||
import ContextMenu from './ui/ContextMenu.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
|
@ -31,9 +32,12 @@ const WootUIKit = {
|
|||
Code,
|
||||
ColorPicker,
|
||||
ConfirmDeleteModal,
|
||||
ConfirmModal,
|
||||
ContextMenu,
|
||||
DeleteModal,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
FeatureToggle,
|
||||
HorizontalBar,
|
||||
Input,
|
||||
Label,
|
||||
|
@ -47,8 +51,6 @@ const WootUIKit = {
|
|||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmModal,
|
||||
ContextMenu,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
:class="{ 'text-truncate': shouldTruncate }"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view">
|
||||
<span v-if="showChildCount" class="count-view">
|
||||
{{ childItemCount }}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -76,7 +76,7 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isHelpCenterSidebar: {
|
||||
showChildCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -127,11 +127,16 @@ $label-badge-size: var(--space-slab);
|
|||
color: var(--w-500);
|
||||
border-color: var(--w-25);
|
||||
}
|
||||
&.is-active .count-view {
|
||||
background: var(--w-75);
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex-grow: 1;
|
||||
line-height: var(--space-two);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inbox-icon {
|
||||
|
@ -175,10 +180,6 @@ $label-badge-size: var(--space-slab);
|
|||
font-weight: var(--font-weight-bold);
|
||||
margin-left: var(--space-smaller);
|
||||
padding: var(--space-zero) var(--space-smaller);
|
||||
|
||||
&.is-active {
|
||||
background: var(--w-50);
|
||||
color: var(--w-500);
|
||||
}
|
||||
line-height: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,16 +4,15 @@
|
|||
<span class="secondary-menu--header fs-small">
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</span>
|
||||
<div v-if="isHelpCenterSidebar" class="submenu-icons">
|
||||
<div v-if="menuItem.showNewButton" class="submenu-icons">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
class="submenu-icon"
|
||||
@click="onClickOpen"
|
||||
>
|
||||
<fluent-icon icon="add" size="16" />
|
||||
</woot-button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
|
@ -28,11 +27,7 @@
|
|||
size="14"
|
||||
/>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="isHelpCenterSidebar"
|
||||
class="count-view"
|
||||
:class="computedClass"
|
||||
>
|
||||
<span v-if="showChildCount(menuItem.count)" class="count-view">
|
||||
{{ `${menuItem.count}` }}
|
||||
</span>
|
||||
<span
|
||||
|
@ -55,7 +50,7 @@
|
|||
:should-truncate="child.truncateLabel"
|
||||
:icon="computedInboxClass(child)"
|
||||
:warning-icon="computedInboxErrorClass(child)"
|
||||
:is-help-center-sidebar="isHelpCenterSidebar"
|
||||
:show-child-count="showChildCount(child.count)"
|
||||
:child-item-count="child.count"
|
||||
/>
|
||||
<router-link
|
||||
|
@ -64,10 +59,10 @@
|
|||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li>
|
||||
<li class="menu-item--new">
|
||||
<a
|
||||
:href="href"
|
||||
class="button small clear menu-item--new secondary"
|
||||
class="button small link clear secondary"
|
||||
:class="{ 'is-active': isActive }"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
|
@ -78,9 +73,6 @@
|
|||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
<p v-if="isHelpCenterSidebar && isCategoryEmpty" class="empty-text">
|
||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
||||
</p>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -104,14 +96,6 @@ export default {
|
|||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isHelpCenterSidebar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCategoryEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
@ -161,8 +145,8 @@ export default {
|
|||
this.menuItem.toStateName === 'settings_applications'
|
||||
);
|
||||
},
|
||||
isArticlesView() {
|
||||
return this.$store.state.route.name === this.menuItem.toStateName;
|
||||
isCurrentRoute() {
|
||||
return this.$store.state.route.name.includes(this.menuItem.toStateName);
|
||||
},
|
||||
|
||||
computedClass() {
|
||||
|
@ -181,12 +165,11 @@ export default {
|
|||
}
|
||||
return ' ';
|
||||
}
|
||||
if (this.isHelpCenterSidebar) {
|
||||
if (this.isArticlesView) {
|
||||
return 'is-active';
|
||||
}
|
||||
return ' ';
|
||||
|
||||
if (this.isCurrentRoute) {
|
||||
return 'is-active';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
@ -222,6 +205,9 @@ export default {
|
|||
onClickOpen() {
|
||||
this.$emit('open');
|
||||
},
|
||||
showChildCount(count) {
|
||||
return Number.isInteger(count);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -277,6 +263,11 @@ export default {
|
|||
color: var(--w-500);
|
||||
border-color: var(--w-25);
|
||||
}
|
||||
|
||||
&.is-active .count-view {
|
||||
background: var(--w-75);
|
||||
color: var(--w-600);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-menu--icon {
|
||||
|
@ -306,15 +297,12 @@ export default {
|
|||
top: -1px;
|
||||
}
|
||||
|
||||
.sidebar-item .button.menu-item--new {
|
||||
display: inline-flex;
|
||||
height: var(--space-medium);
|
||||
margin: var(--space-smaller) 0;
|
||||
padding: var(--space-smaller);
|
||||
color: var(--s-500);
|
||||
.sidebar-item .menu-item--new {
|
||||
padding: var(--space-small) 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--w-500);
|
||||
.button {
|
||||
display: inline-flex;
|
||||
color: var(--s-500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,11 +328,6 @@ export default {
|
|||
font-weight: var(--font-weight-bold);
|
||||
margin-left: var(--space-smaller);
|
||||
padding: var(--space-zero) var(--space-smaller);
|
||||
|
||||
&.is-active {
|
||||
background: var(--w-50);
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-icons {
|
||||
|
@ -356,10 +339,4 @@ export default {
|
|||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-small);
|
||||
margin: var(--space-smaller);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
<template>
|
||||
<span class="time-ago">
|
||||
<span> {{ timeAgo }}</span>
|
||||
<span>{{ timeAgo }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ZERO = 0;
|
||||
const MINUTE_IN_MILLI_SECONDS = 60000;
|
||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { differenceInMilliseconds } from 'date-fns';
|
||||
|
||||
export default {
|
||||
name: 'TimeAgo',
|
||||
|
@ -28,51 +26,40 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
timeAgo: '',
|
||||
timeAgo: this.dynamicTime(this.timestamp),
|
||||
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() {
|
||||
const timeDiff = differenceInMilliseconds(
|
||||
new Date(),
|
||||
new Date(this.timestamp * 1000)
|
||||
);
|
||||
const timeDiff = Date.now() - this.timestamp * 1000;
|
||||
if (timeDiff > DAY_IN_MILLI_SECONDS) {
|
||||
return DAY_IN_MILLI_SECONDS;
|
||||
}
|
||||
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
|
||||
return HOUR_IN_MILLI_SECONDS;
|
||||
}
|
||||
if (timeDiff > 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);
|
||||
}
|
||||
|
||||
return MINUTE_IN_MILLI_SECONDS;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -150,7 +150,7 @@ export default {
|
|||
},
|
||||
actionInputStyles() {
|
||||
return {
|
||||
error: this.v.action_params.$dirty && this.v.action_params.$error,
|
||||
'has-error': this.v.action_params.$dirty && this.v.action_params.$error,
|
||||
'is-a-macro': this.isMacro,
|
||||
};
|
||||
},
|
||||
|
@ -187,7 +187,7 @@ export default {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
.filter.has-error {
|
||||
background: var(--r-50);
|
||||
}
|
||||
|
||||
|
|
|
@ -113,5 +113,6 @@ input[type='file'] {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="avatar-container"
|
||||
:style="[style, customStyle]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span>{{ userInitial }}</span>
|
||||
<div class="avatar-container" :style="style" aria-hidden="true">
|
||||
{{ userInitial }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -16,69 +12,26 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#c2e1ff',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#1976cc',
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let style = {
|
||||
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`,
|
||||
return {
|
||||
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() {
|
||||
return this.initials || this.initial(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initial(username) {
|
||||
const parts = username ? username.split(/[ -]/) : [];
|
||||
let initials = '';
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
initials += parts[i].charAt(0);
|
||||
}
|
||||
const parts = this.username.split(/[ -]/);
|
||||
let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
|
||||
|
||||
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
||||
initials = initials.replace(/[a-z]+/g, '');
|
||||
}
|
||||
initials = initials.substring(0, 2).toUpperCase();
|
||||
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
|
@ -88,6 +41,7 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
line-height: 100%;
|
||||
font-weight: 500;
|
||||
align-items: 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>
|
|
@ -2,8 +2,8 @@ import { mount } from '@vue/test-utils';
|
|||
import Avatar from './Avatar.vue';
|
||||
import Thumbnail from './Thumbnail.vue';
|
||||
|
||||
describe(`when there are NO errors loading the thumbnail`, () => {
|
||||
it(`should render the agent thumbnail`, () => {
|
||||
describe('Thumbnail.vue', () => {
|
||||
it('should render the agent thumbnail if valid image is passed', () => {
|
||||
const wrapper = mount(Thumbnail, {
|
||||
propsData: {
|
||||
src: 'https://some_valid_url.com',
|
||||
|
@ -14,14 +14,12 @@ describe(`when there are NO errors loading the thumbnail`, () => {
|
|||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('#image').exists()).toBe(true);
|
||||
expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
|
||||
const avatarComponent = wrapper.findComponent(Avatar);
|
||||
expect(avatarComponent.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there ARE errors loading the thumbnail`, () => {
|
||||
it(`should render the agent avatar`, () => {
|
||||
it('should render the avatar component if invalid image is passed', () => {
|
||||
const wrapper = mount(Thumbnail, {
|
||||
propsData: {
|
||||
src: 'https://some_invalid_url.com',
|
||||
|
@ -32,19 +30,17 @@ describe(`when there ARE errors loading the thumbnail`, () => {
|
|||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('#image').exists()).toBe(false);
|
||||
expect(wrapper.find('.avatar-container').exists()).toBe(true);
|
||||
const avatarComponent = wrapper.findComponent(Avatar);
|
||||
expect(avatarComponent.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when Avatar shows`, () => {
|
||||
it(`initials shold correspond to username`, () => {
|
||||
it('should the initial of the name if no image is passed', () => {
|
||||
const wrapper = mount(Avatar, {
|
||||
propsData: {
|
||||
username: 'Angie Rojas',
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('span').text()).toBe('AR');
|
||||
expect(wrapper.find('div').text()).toBe('AR');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,74 +1,23 @@
|
|||
<template>
|
||||
<div class="user-thumbnail-box" :style="{ height: size, width: size }">
|
||||
<img
|
||||
v-if="!imgError && Boolean(src)"
|
||||
id="image"
|
||||
v-if="!imgError && src"
|
||||
:src="src"
|
||||
:class="thumbnailClass"
|
||||
@error="onImgError()"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<Avatar
|
||||
v-else
|
||||
:username="userNameWithoutEmoji"
|
||||
:class="thumbnailClass"
|
||||
:size="avatarSize"
|
||||
:variant="variant"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'instagram_direct_message'"
|
||||
id="badge"
|
||||
v-if="badgeSrc"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/instagram-dm.png"
|
||||
/>
|
||||
<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"
|
||||
:src="`/integrations/channels/badges/${badgeSrc}.png`"
|
||||
alt="Badge"
|
||||
/>
|
||||
<div
|
||||
v-if="showStatusIndicator"
|
||||
|
@ -83,7 +32,7 @@
|
|||
* Src - source for round image
|
||||
* Size - Size of the thumbnail
|
||||
* Badge - Chat source indication { fb / telegram }
|
||||
* Username - User name for avatar
|
||||
* Username - Username for avatar
|
||||
*/
|
||||
import Avatar from './Avatar';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
|
@ -103,7 +52,7 @@ export default {
|
|||
},
|
||||
badge: {
|
||||
type: String,
|
||||
default: 'fb',
|
||||
default: '',
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
|
@ -142,6 +91,19 @@ export default {
|
|||
avatarSize() {
|
||||
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() {
|
||||
const size = Math.floor(this.avatarSize / 3);
|
||||
const badgeSize = `${size + 2}px`;
|
||||
|
@ -160,12 +122,10 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
src: {
|
||||
handler(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
src(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -229,9 +189,5 @@ export default {
|
|||
.user-online-status--offline {
|
||||
background: var(--s-500);
|
||||
}
|
||||
|
||||
.user-online-status--offline {
|
||||
background: var(--s-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -139,7 +139,6 @@ export default {
|
|||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
getUnreadCount: 'getUnreadCount',
|
||||
loadingChatList: 'getChatListLoadingStatus',
|
||||
conversationLastSeen: 'getConversationLastSeen',
|
||||
}),
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
|
@ -234,7 +233,6 @@ export default {
|
|||
return 'arrow-chevron-left';
|
||||
},
|
||||
getLastSeenAt() {
|
||||
if (this.conversationLastSeen) return this.conversationLastSeen;
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt;
|
||||
},
|
||||
|
|
|
@ -68,8 +68,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
};
|
||||
|
||||
onConversationRead = data => {
|
||||
const { contact_last_seen_at: lastSeen } = data;
|
||||
this.app.$store.dispatch('updateConversationRead', lastSeen);
|
||||
this.app.$store.dispatch('updateConversation', data);
|
||||
};
|
||||
|
||||
onLogout = () => AuthAPI.logout();
|
||||
|
|
|
@ -69,3 +69,26 @@ export const labels = [
|
|||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const files = [
|
||||
{
|
||||
id: 76,
|
||||
macro_id: 77,
|
||||
file_type: 'image/jpeg',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBYUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--aa41b5a779a83c1d86b28475a5cf0bd17f41f0ff/fayaz_cropped.jpeg',
|
||||
blob_id: 88,
|
||||
filename: 'fayaz_cropped.jpeg',
|
||||
},
|
||||
{
|
||||
id: 82,
|
||||
macro_id: 77,
|
||||
file_type: 'image/png',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBZdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--260fda80b77409ffaaac10b96681fba447600545/screenshot.png',
|
||||
blob_id: 94,
|
||||
filename: 'screenshot.png',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { emptyMacro } from '../../routes/dashboard/settings/macros/macroHelper';
|
||||
import {
|
||||
emptyMacro,
|
||||
resolveActionName,
|
||||
resolveLabels,
|
||||
resolveTeamIds,
|
||||
getFileName,
|
||||
} from '../../routes/dashboard/settings/macros/macroHelper';
|
||||
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
||||
import { teams, labels, files } from './macrosFixtures';
|
||||
|
||||
describe('#emptyMacro', () => {
|
||||
const defaultMacro = {
|
||||
|
@ -15,3 +23,45 @@ describe('#emptyMacro', () => {
|
|||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFileName', () => {
|
||||
it('returns the correct file name from the list of files', () => {
|
||||
expect(getFileName(files[0].blob_id, 'send_attachment', files)).toEqual(
|
||||
files[0].filename
|
||||
);
|
||||
expect(getFileName(files[1].blob_id, 'send_attachment', files)).toEqual(
|
||||
files[1].filename
|
||||
);
|
||||
expect(getFileName(files[0].blob_id, 'wrong_action', files)).toEqual('');
|
||||
expect(getFileName(null, 'send_attachment', files)).toEqual('');
|
||||
expect(getFileName(files[0].blob_id, 'send_attachment', [])).toEqual('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
|
||||
"LABEL_IDLE": "Upload Attachment",
|
||||
"LABEL_UPLOADING": "Uploading...",
|
||||
"LABEL_UPLOADED": "Succesfully Uploaded",
|
||||
"LABEL_UPLOADED": "Successfully Uploaded",
|
||||
"LABEL_UPLOAD_FAILED": "Upload Failed"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,7 +208,8 @@
|
|||
"CONVERSATION_LABELS": "Conversation Labels",
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations"
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
|
|
@ -160,8 +160,7 @@
|
|||
}
|
||||
},
|
||||
"ADD": {
|
||||
"CREATE_FLOW": [
|
||||
{
|
||||
"CREATE_FLOW": [{
|
||||
"title": "Help center information",
|
||||
"route": "new_portal_information",
|
||||
"body": "Basic information about portal",
|
||||
|
@ -212,20 +211,19 @@
|
|||
"SLUG": {
|
||||
"LABEL": "Slug",
|
||||
"PLACEHOLDER": "Portal slug for urls",
|
||||
|
||||
"ERROR": "Slug is required"
|
||||
},
|
||||
"DOMAIN": {
|
||||
"LABEL": "Custom Domain",
|
||||
"PLACEHOLDER": "Portal custom domain",
|
||||
"HELP_TEXT": "Add only If you want to use a custom domain for your portals.",
|
||||
"ERROR": "Custom Domain is required"
|
||||
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
|
||||
"ERROR": "Enter a valid domain URL"
|
||||
},
|
||||
"HOME_PAGE_LINK": {
|
||||
"LABEL": "Home Page Link",
|
||||
"PLACEHOLDER": "Portal home page link",
|
||||
"HELP_TEXT": "The link used to return from the portal to the home page.",
|
||||
"ERROR": "Home Page Link is required"
|
||||
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
|
||||
"ERROR": "Enter a valid home page URL"
|
||||
},
|
||||
"THEME_COLOR": {
|
||||
"LABEL": "Portal theme color",
|
||||
|
|
|
@ -24,12 +24,7 @@
|
|||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Created by",
|
||||
"Last updated by",
|
||||
"Visibility"
|
||||
],
|
||||
"TABLE_HEADER": ["Name", "Created by", "Last updated by", "Visibility"],
|
||||
"404": "No macros found"
|
||||
},
|
||||
"DELETE": {
|
||||
|
@ -68,6 +63,11 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,11 +26,4 @@ describe('#conversationMixin', () => {
|
|||
conversationMixin.methods.unReadMessages(conversationFixture.conversation)
|
||||
).toEqual(conversationFixture.unReadMessages);
|
||||
});
|
||||
it('should return the user message read flag', () => {
|
||||
const contactLastSeen = 1649856659;
|
||||
const createdAt = 1649859419;
|
||||
expect(
|
||||
conversationMixin.methods.hasUserReadMessage(createdAt, contactLastSeen)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -116,6 +116,7 @@ describe('uiSettingsMixin', () => {
|
|||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([
|
||||
{ name: 'conversation_actions' },
|
||||
{ name: 'macros' },
|
||||
{ name: 'conversation_info' },
|
||||
{ name: 'contact_attributes' },
|
||||
{ name: 'previous_conversation' },
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
|
||||
{ name: 'conversation_actions' },
|
||||
{ name: 'macros' },
|
||||
{ name: 'conversation_info' },
|
||||
{ name: 'contact_attributes' },
|
||||
{ name: 'previous_conversation' },
|
||||
|
@ -30,7 +31,17 @@ export default {
|
|||
...mapGetters({ uiSettings: 'getUISettings' }),
|
||||
conversationSidebarItemsOrder() {
|
||||
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() {
|
||||
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
@on-sort-change="onSortChange"
|
||||
/>
|
||||
<table-footer
|
||||
:on-page-change="onPageChange"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -93,6 +93,19 @@
|
|||
/>
|
||||
</accordion-item>
|
||||
</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>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
|
@ -112,6 +125,7 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
|||
import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import MacrosList from './Macros/List';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -123,6 +137,7 @@ export default {
|
|||
CustomAttributeSelector,
|
||||
ConversationAction,
|
||||
draggable,
|
||||
MacrosList,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
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>
|
|
@ -2,6 +2,7 @@
|
|||
<div class="row app-wrapper">
|
||||
<sidebar
|
||||
:route="currentRoute"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
|
@ -16,11 +17,15 @@
|
|||
:accessible-menu-items="accessibleMenuItems"
|
||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||
@open-popover="openPortalPopover"
|
||||
@open-modal="onClickOpenAddCatogoryModal"
|
||||
@open-modal="onClickOpenAddCategoryModal"
|
||||
/>
|
||||
<section class="app-content columns" :class="contentClassName">
|
||||
<router-view />
|
||||
<command-bar />
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
/>
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
|
@ -58,6 +63,7 @@ import PortalPopover from '../components/PortalPopover.vue';
|
|||
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
||||
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
||||
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 uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
|
@ -72,6 +78,7 @@ export default {
|
|||
NotificationPanel,
|
||||
PortalPopover,
|
||||
AddCategory,
|
||||
AccountSelector,
|
||||
},
|
||||
mixins: [portalMixin, uiSettingsMixin],
|
||||
data() {
|
||||
|
@ -83,6 +90,7 @@ export default {
|
|||
showPortalPopover: false,
|
||||
showAddCategoryModal: false,
|
||||
lastActivePortalSlug: '',
|
||||
showAccountModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -134,14 +142,14 @@ export default {
|
|||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.selectedPortal) return [];
|
||||
|
||||
const {
|
||||
meta: {
|
||||
all_articles_count: allArticlesCount,
|
||||
mine_articles_count: mineArticlesCount,
|
||||
draft_articles_count: draftArticlesCount,
|
||||
archived_articles_count: archivedArticlesCount,
|
||||
} = {},
|
||||
} = this.selectedPortal;
|
||||
allArticlesCount,
|
||||
mineArticlesCount,
|
||||
draftArticlesCount,
|
||||
archivedArticlesCount,
|
||||
} = this.meta;
|
||||
|
||||
return [
|
||||
{
|
||||
icon: 'book',
|
||||
|
@ -196,6 +204,7 @@ export default {
|
|||
icon: 'folder',
|
||||
label: 'HELP_CENTER.CATEGORY',
|
||||
hasSubMenu: true,
|
||||
showNewButton: true,
|
||||
key: 'category',
|
||||
children: this.categories.map(category => ({
|
||||
id: category.id,
|
||||
|
@ -216,6 +225,13 @@ export default {
|
|||
return this.selectedPortal ? this.selectedPortal.name : '';
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.params.portalSlug'() {
|
||||
this.fetchPortalsAndItsCategories();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
|
@ -232,7 +248,7 @@ export default {
|
|||
},
|
||||
updated() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) {
|
||||
if (slug !== this.lastActivePortalSlug) {
|
||||
this.lastActivePortalSlug = slug;
|
||||
this.updateUISettings({
|
||||
last_active_portal_slug: slug,
|
||||
|
@ -251,12 +267,14 @@ export default {
|
|||
toggleSidebar() {
|
||||
this.isSidebarOpen = !this.isSidebarOpen;
|
||||
},
|
||||
fetchPortalsAndItsCategories() {
|
||||
this.$store.dispatch('portals/index').then(() => {
|
||||
this.$store.dispatch('categories/index', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
});
|
||||
});
|
||||
async fetchPortalsAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
locale: this.selectedLocaleInPortal,
|
||||
};
|
||||
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
toggleKeyShortcutModal() {
|
||||
|
@ -277,12 +295,15 @@ export default {
|
|||
closePortalPopover() {
|
||||
this.showPortalPopover = false;
|
||||
},
|
||||
onClickOpenAddCatogoryModal() {
|
||||
onClickOpenAddCategoryModal() {
|
||||
this.showAddCategoryModal = true;
|
||||
},
|
||||
onClickCloseAddCategoryModal() {
|
||||
this.showAddCategoryModal = false;
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.showAccountModal = !this.showAccountModal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -376,7 +376,6 @@ export default {
|
|||
}
|
||||
}
|
||||
.portal-title {
|
||||
color: var(--s-900);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.portal-count {
|
||||
|
@ -389,14 +388,17 @@ export default {
|
|||
}
|
||||
}
|
||||
.portal-locales {
|
||||
margin-top: var(--space-medium);
|
||||
margin-bottom: var(--space-small);
|
||||
margin-bottom: var(--space-large);
|
||||
.locale-title {
|
||||
color: var(--s-800);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.portal--heading {
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
.portal-settings--icon {
|
||||
padding: var(--space-smaller);
|
||||
|
|
|
@ -35,11 +35,12 @@
|
|||
<div class="form-item">
|
||||
<woot-input
|
||||
v-model.trim="name"
|
||||
:class="{ error: $v.slug.$error }"
|
||||
:class="{ error: $v.name.$error }"
|
||||
:error="nameError"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.PORTAL.ADD.NAME.HELP_TEXT')"
|
||||
@blur="$v.name.$touch"
|
||||
@input="onNameChange"
|
||||
/>
|
||||
</div>
|
||||
|
@ -51,15 +52,18 @@
|
|||
:label="$t('HELP_CENTER.PORTAL.ADD.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.SLUG.PLACEHOLDER')"
|
||||
:help-text="domainHelpText"
|
||||
@input="$v.slug.$touch"
|
||||
@blur="$v.slug.$touch"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<woot-input
|
||||
v-model.trim="domain"
|
||||
:class="{ error: $v.domain.$error }"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.HELP_TEXT')"
|
||||
:error="domainError"
|
||||
@blur="$v.domain.$touch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -77,8 +81,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
|
||||
|
@ -116,6 +120,9 @@ export default {
|
|||
slug: {
|
||||
required,
|
||||
},
|
||||
domain: {
|
||||
url,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
nameError() {
|
||||
|
@ -131,7 +138,10 @@ export default {
|
|||
return '';
|
||||
},
|
||||
domainError() {
|
||||
return this.$v.domain.$error;
|
||||
if (this.$v.domain.$error) {
|
||||
return this.$t('HELP_CENTER.PORTAL.ADD.DOMAIN.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
domainHelpText() {
|
||||
return buildPortalURL(this.slug);
|
||||
|
|
|
@ -42,12 +42,23 @@
|
|||
$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.PLACEHOLDER')
|
||||
"
|
||||
:help-text="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.HELP_TEXT')"
|
||||
:error="
|
||||
$v.homePageLink.$error
|
||||
? $t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.ERROR')
|
||||
: ''
|
||||
"
|
||||
:class="{ error: $v.homePageLink.$error }"
|
||||
@blur="$v.homePageLink.$touch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-end">
|
||||
<woot-button :is-loading="isSubmitting" @click="onSubmitClick">
|
||||
<woot-button
|
||||
:is-loading="isSubmitting"
|
||||
:is-disabled="$v.$invalid"
|
||||
@click="onSubmitClick"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.UPDATE_PORTAL_BUTTON'
|
||||
|
@ -59,6 +70,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { url } from 'vuelidate/lib/validators';
|
||||
import { getRandomColor } from 'dashboard/helper/labelColor';
|
||||
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
@ -85,7 +97,11 @@ export default {
|
|||
alertMessage: '',
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
validations: {
|
||||
homePageLink: {
|
||||
url,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.color = getRandomColor();
|
||||
this.updateDataFromStore();
|
||||
|
@ -102,6 +118,10 @@ export default {
|
|||
}
|
||||
},
|
||||
onSubmitClick() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
const portal = {
|
||||
id: this.portal.id,
|
||||
slug: this.portal.slug,
|
||||
|
|
|
@ -112,16 +112,8 @@ export default {
|
|||
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
|
||||
},
|
||||
methods: {
|
||||
fetchPortalsAndItsCategories() {
|
||||
this.$store.dispatch('portals/index').then(() => {
|
||||
this.$store.dispatch('categories/index', {
|
||||
portalSlug: this.portal.slug,
|
||||
});
|
||||
});
|
||||
},
|
||||
onClick(event, code, portal) {
|
||||
event.preventDefault();
|
||||
this.fetchPortalsAndItsCategories();
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
|
|
|
@ -12,16 +12,20 @@
|
|||
v-for="menuItem in accessibleMenuItems"
|
||||
:key="menuItem.toState"
|
||||
:menu-item="menuItem"
|
||||
:is-help-center-sidebar="true"
|
||||
/>
|
||||
<secondary-nav-item
|
||||
v-for="menuItem in additionalSecondaryMenuItems"
|
||||
:key="menuItem.key"
|
||||
:menu-item="menuItem"
|
||||
:is-help-center-sidebar="true"
|
||||
:is-category-empty="!hasCategory"
|
||||
@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>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -123,4 +127,8 @@ export default {
|
|||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
padding: var(--space-smaller) var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -76,7 +76,7 @@ export default {
|
|||
justify-content: space-between;
|
||||
padding: var(--space-normal);
|
||||
margin: var(--space-minus-small);
|
||||
margin-bottom: var(--space-small);
|
||||
margin-bottom: var(--space-smaller);
|
||||
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';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import ArticleEditor from './ArticleEditor.vue';
|
||||
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Help Center',
|
||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
|||
} = this.uiSettings || {};
|
||||
|
||||
if (lastActivePortalSlug)
|
||||
this.$router.push({
|
||||
this.$router.replace({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: lastActivePortalSlug,
|
||||
|
@ -22,7 +22,7 @@ export default {
|
|||
replace: true,
|
||||
});
|
||||
else
|
||||
this.$router.push({
|
||||
this.$router.replace({
|
||||
name: 'list_all_portals',
|
||||
replace: true,
|
||||
});
|
||||
|
|
|
@ -104,9 +104,6 @@ export default {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
articleCount() {
|
||||
return this.articles ? this.articles.length : 0;
|
||||
},
|
||||
headerTitleInCategoryView() {
|
||||
return this.categories && this.categories.length
|
||||
? this.selectedCategory.name
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.CREATE_BASIC_SETTING_BUTTON'
|
||||
)
|
||||
"
|
||||
@submit="updateBasicSettings"
|
||||
@submit="createPortal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -37,7 +37,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
async updateBasicSettings(portal) {
|
||||
async createPortal(portal) {
|
||||
try {
|
||||
await this.$store.dispatch('portals/create', {
|
||||
portal,
|
||||
|
@ -45,16 +45,16 @@ export default {
|
|||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
||||
);
|
||||
this.$router.push({
|
||||
name: 'portal_customization',
|
||||
params: { portalSlug: portal.slug },
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_BASIC');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
this.$router.push({
|
||||
name: 'portal_customization',
|
||||
params: { portalSlug: portal.slug },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||
/>
|
||||
<table-footer
|
||||
:on-page-change="onPageChange"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
import MacroForm from './MacroForm';
|
||||
import { MACRO_ACTION_TYPES } from './constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { emptyMacro } from './macroHelper';
|
||||
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import macrosMixin from 'dashboard/mixins/macrosMixin';
|
||||
|
@ -107,7 +106,16 @@ export default {
|
|||
},
|
||||
initNewMacro() {
|
||||
this.mode = 'CREATE';
|
||||
this.macro = emptyMacro;
|
||||
this.macro = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
||||
},
|
||||
async saveMacro(macro) {
|
||||
try {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<div class="small-8 columns with-right-space macros-canvas">
|
||||
<macro-nodes
|
||||
v-model="macro.actions"
|
||||
:files="files"
|
||||
@addNewNode="appendNode"
|
||||
@deleteNode="deleteNode"
|
||||
@resetAction="resetNode"
|
||||
|
@ -46,7 +47,19 @@ export default {
|
|||
macro: this.macroData,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
files() {
|
||||
if (this.macro && this.macro.files) return this.macro.files;
|
||||
return [];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler() {
|
||||
this.resetValidation();
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
macroData: {
|
||||
handler() {
|
||||
this.macro = this.macroData;
|
||||
|
@ -79,9 +92,6 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$v.$reset();
|
||||
},
|
||||
methods: {
|
||||
updateName(value) {
|
||||
this.macro.name = value;
|
||||
|
@ -104,8 +114,12 @@ export default {
|
|||
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>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
:show-remove-button="false"
|
||||
:is-macro="true"
|
||||
:v="$v.macro.actions.$each[index]"
|
||||
:initial-file-name="fileName"
|
||||
@resetAction="$emit('resetAction')"
|
||||
/>
|
||||
<macro-action-button
|
||||
|
@ -60,6 +61,10 @@ export default {
|
|||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
tag="div"
|
||||
class="macros__nodes-draggable"
|
||||
handle=".macros__node-drag-handle"
|
||||
@start="dragging = true"
|
||||
@end="dragging = false"
|
||||
>
|
||||
<div v-for="(action, i) in actionData" :key="i" class="macro__node">
|
||||
<macro-node
|
||||
|
@ -17,6 +15,13 @@
|
|||
class="macros__node-action"
|
||||
type="add"
|
||||
:index="i"
|
||||
:file-name="
|
||||
fileName(
|
||||
actionData[i].action_params[0],
|
||||
actionData[i].action_name,
|
||||
files
|
||||
)
|
||||
"
|
||||
:single-node="actionData.length === 1"
|
||||
@resetAction="$emit('resetAction', i)"
|
||||
@deleteNode="$emit('deleteNode', i)"
|
||||
|
@ -39,7 +44,7 @@ import MacrosPill from './Pill.vue';
|
|||
import Draggable from 'vuedraggable';
|
||||
import MacroNode from './MacroNode.vue';
|
||||
import MacroActionButton from './ActionButton.vue';
|
||||
|
||||
import { getFileName } from './macroHelper';
|
||||
export default {
|
||||
components: {
|
||||
Draggable,
|
||||
|
@ -52,11 +57,10 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
};
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
actionData: {
|
||||
|
@ -68,6 +72,11 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fileName() {
|
||||
return getFileName(...arguments);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
@input="onUpdateName($event)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="macros__form-visibility-container">
|
||||
<p class="title">{{ $t('MACROS.EDITOR.VISIBILITY.LABEL') }}</p>
|
||||
<div class="macros__form-visibility">
|
||||
<button
|
||||
|
@ -110,7 +110,9 @@ export default {
|
|||
.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));
|
||||
|
@ -129,20 +131,6 @@ export default {
|
|||
border: 1px solid var(--w-300);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.visibility-check {
|
||||
position: absolute;
|
||||
color: var(--w-500);
|
||||
|
@ -152,6 +140,20 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -164,11 +166,18 @@ export default {
|
|||
}
|
||||
p {
|
||||
margin-left: var(--space-small);
|
||||
margin-bottom: 0;
|
||||
color: var(--s-600);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep input[type='text'] {
|
||||
margin-bottom: var(--space-small);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
::v-deep .error {
|
||||
.message {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { MACRO_ACTION_TYPES as macroActionTypes } from 'dashboard/routes/dashboard/settings/macros/constants.js';
|
||||
export const emptyMacro = {
|
||||
name: '',
|
||||
actions: [
|
||||
|
@ -8,3 +9,34 @@ export const emptyMacro = {
|
|||
],
|
||||
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(', ');
|
||||
};
|
||||
|
||||
export const getFileName = (id, actionType, files) => {
|
||||
if (!id || !files) return '';
|
||||
if (actionType === 'send_attachment') {
|
||||
const file = files.find(item => item.blob_id === id);
|
||||
if (file) return file.filename.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
|
|
@ -199,10 +199,6 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
updateConversationRead({ commit }, timestamp) {
|
||||
commit(types.SET_CONVERSATION_LAST_SEEN, timestamp);
|
||||
},
|
||||
|
||||
updateMessage({ commit }, message) {
|
||||
commit(types.ADD_MESSAGE, message);
|
||||
},
|
||||
|
@ -252,6 +248,12 @@ const actions = {
|
|||
meta: { sender },
|
||||
} = conversation;
|
||||
commit(types.UPDATE_CONVERSATION, conversation);
|
||||
|
||||
dispatch('conversationLabels/setConversationLabel', {
|
||||
id: conversation.id,
|
||||
data: conversation.labels,
|
||||
});
|
||||
|
||||
dispatch('contacts/setContact', sender);
|
||||
},
|
||||
|
||||
|
|
|
@ -91,9 +91,6 @@ const getters = {
|
|||
value => value.id === Number(conversationId)
|
||||
);
|
||||
},
|
||||
getConversationLastSeen: _state => {
|
||||
return _state.conversationLastSeen;
|
||||
},
|
||||
};
|
||||
|
||||
export default getters;
|
||||
|
|
|
@ -13,7 +13,6 @@ const state = {
|
|||
currentInbox: null,
|
||||
selectedChatId: null,
|
||||
appliedFilters: [],
|
||||
conversationLastSeen: null,
|
||||
};
|
||||
|
||||
// mutations
|
||||
|
@ -34,9 +33,6 @@ export const mutations = {
|
|||
_state.allConversations = [];
|
||||
_state.selectedChatId = null;
|
||||
},
|
||||
[types.SET_CONVERSATION_LAST_SEEN](_state, timestamp) {
|
||||
_state.conversationLastSeen = timestamp;
|
||||
},
|
||||
[types.SET_ALL_MESSAGES_LOADED](_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'allMessagesLoaded', true);
|
||||
|
|
|
@ -2,13 +2,13 @@ import categoriesAPI from 'dashboard/api/helpCenter/categories.js';
|
|||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
import types from '../../mutation-types';
|
||||
export const actions = {
|
||||
index: async ({ commit }, { portalSlug }) => {
|
||||
index: async ({ commit }, { portalSlug, locale }) => {
|
||||
try {
|
||||
commit(types.SET_UI_FLAG, { isFetching: true });
|
||||
if (portalSlug) {
|
||||
const {
|
||||
data: { payload },
|
||||
} = await categoriesAPI.get({ portalSlug });
|
||||
} = await categoriesAPI.get({ portalSlug, locale });
|
||||
commit(types.CLEAR_CATEGORIES);
|
||||
const categoryIds = payload.map(category => category.id);
|
||||
commit(types.ADD_MANY_CATEGORIES, payload);
|
||||
|
|
|
@ -7,14 +7,12 @@ export const actions = {
|
|||
try {
|
||||
commit(types.SET_UI_FLAG, { isFetching: true });
|
||||
const {
|
||||
data: { payload, meta },
|
||||
data: { payload },
|
||||
} = await portalAPIs.get();
|
||||
commit(types.CLEAR_PORTALS);
|
||||
const portalSlugs = payload.map(portal => portal.slug);
|
||||
commit(types.ADD_MANY_PORTALS_ENTRY, payload);
|
||||
commit(types.ADD_MANY_PORTALS_IDS, portalSlugs);
|
||||
|
||||
commit(types.SET_PORTALS_META, meta);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} 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) => {
|
||||
commit(types.SET_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
|
|
|
@ -20,7 +20,5 @@ export const getters = {
|
|||
return portals;
|
||||
},
|
||||
count: state => state.portals.allIds.length || 0,
|
||||
getMeta: state => {
|
||||
return state.meta;
|
||||
},
|
||||
getMeta: state => state.meta,
|
||||
};
|
||||
|
|
|
@ -10,8 +10,10 @@ export const defaultPortalFlags = {
|
|||
|
||||
const state = {
|
||||
meta: {
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
allArticlesCount: 0,
|
||||
mineArticlesCount: 0,
|
||||
draftArticlesCount: 0,
|
||||
archivedArticlesCount: 0,
|
||||
},
|
||||
|
||||
portals: {
|
||||
|
|
|
@ -44,9 +44,16 @@ export const mutations = {
|
|||
},
|
||||
|
||||
[types.SET_PORTALS_META]: ($state, data) => {
|
||||
const { portals_count: count, current_page: currentPage } = data;
|
||||
Vue.set($state.meta, 'count', count);
|
||||
Vue.set($state.meta, 'currentPage', currentPage);
|
||||
const {
|
||||
all_articles_count: allArticlesCount = 0,
|
||||
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) {
|
||||
|
|
|
@ -22,7 +22,6 @@ describe('#actions', () => {
|
|||
[types.CLEAR_PORTALS],
|
||||
[types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload],
|
||||
[types.ADD_MANY_PORTALS_IDS, ['domain', 'campaign']],
|
||||
[types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }],
|
||||
[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', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: apiResponse.payload[1] });
|
||||
|
|
|
@ -107,12 +107,20 @@ describe('#mutations', () => {
|
|||
describe('#SET_PORTALS_META', () => {
|
||||
it('add meta to state', () => {
|
||||
mutations[types.SET_PORTALS_META](state, {
|
||||
portals_count: 10,
|
||||
current_page: 1,
|
||||
});
|
||||
expect(state.meta).toEqual({
|
||||
count: 10,
|
||||
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,
|
||||
messages: [],
|
||||
meta: { sender: { id: 1, name: 'john-doe' } },
|
||||
labels: ['support'],
|
||||
};
|
||||
actions.updateConversation(
|
||||
{ commit, rootState: { route: { name: 'home' } }, dispatch },
|
||||
|
@ -67,6 +68,10 @@ describe('#actions', () => {
|
|||
[types.UPDATE_CONVERSATION, conversation],
|
||||
]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[
|
||||
'conversationLabels/setConversationLabel',
|
||||
{ id: 1, data: ['support'] },
|
||||
],
|
||||
[
|
||||
'contacts/setContact',
|
||||
{
|
||||
|
@ -375,15 +380,6 @@ describe('#actions', () => {
|
|||
expect(commit.mock.calls).toEqual([[types.CLEAR_CONVERSATION_FILTERS]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateConversationRead', () => {
|
||||
it('commits the correct mutation and sets the contact_last_seen', () => {
|
||||
actions.updateConversationRead({ commit }, 1649856659);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONVERSATION_LAST_SEEN, 1649856659],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteMessage', () => {
|
||||
|
|
|
@ -132,16 +132,6 @@ describe('#getters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getConversationLastSeen', () => {
|
||||
it('getConversationLastSeen', () => {
|
||||
const timestamp = 1649856659;
|
||||
const state = {
|
||||
conversationLastSeen: timestamp,
|
||||
};
|
||||
expect(getters.getConversationLastSeen(state)).toEqual(timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLastEmailInSelectedChat', () => {
|
||||
it('Returns cc in last email', () => {
|
||||
const state = {};
|
||||
|
|
|
@ -200,18 +200,6 @@ describe('#mutations', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
describe('#SET_CONVERSATION_LAST_SEEN', () => {
|
||||
it('sets conversation last seen timestamp', () => {
|
||||
const state = {
|
||||
conversationLastSeen: null,
|
||||
};
|
||||
|
||||
mutations[types.SET_CONVERSATION_LAST_SEEN](state, 1649856659);
|
||||
|
||||
expect(state.conversationLastSeen).toEqual(1649856659);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES', () => {
|
||||
it('update conversation custom attributes', () => {
|
||||
const custom_attributes = { order_id: 1001 };
|
||||
|
|
|
@ -21,7 +21,6 @@ export default {
|
|||
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||
SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS',
|
||||
CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS',
|
||||
SET_CONVERSATION_LAST_SEEN: 'SET_CONVERSATION_LAST_SEEN',
|
||||
|
||||
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
||||
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
||||
|
|
|
@ -18,6 +18,11 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
}
|
||||
|
||||
const chatwootSettings = window.chatwootSettings || {};
|
||||
let locale = chatwootSettings.locale || 'en';
|
||||
if (chatwootSettings.useBrowserLanguage) {
|
||||
locale = window.navigator.language.replace('-', '_');
|
||||
}
|
||||
|
||||
window.$chatwoot = {
|
||||
baseUrl,
|
||||
hasLoaded: false,
|
||||
|
@ -25,7 +30,8 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
isOpen: false,
|
||||
position: chatwootSettings.position === 'left' ? 'left' : 'right',
|
||||
websiteToken,
|
||||
locale: chatwootSettings.locale,
|
||||
locale,
|
||||
useBrowserLanguage: chatwootSettings.useBrowserLanguage || false,
|
||||
type: getBubbleView(chatwootSettings.type),
|
||||
launcherTitle: chatwootSettings.launcherTitle || '',
|
||||
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
||||
|
@ -58,7 +64,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
IFrameHelper.events.popoutChatWindow({
|
||||
baseUrl: window.$chatwoot.baseUrl,
|
||||
websiteToken: window.$chatwoot.websiteToken,
|
||||
locale: window.$chatwoot.locale,
|
||||
locale,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
--space-jumbo: 6.4rem;
|
||||
--space-mega: 10rem;
|
||||
--space-giga: 24rem;
|
||||
|
||||
|
||||
--space-minus-micro: -0.2rem;
|
||||
--space-minus-smaller: -0.4rem;
|
||||
--space-minus-half: -0.5rem;
|
||||
--space-minus-small: -0.8rem;
|
||||
--space-minus-one: -1rem;
|
||||
--space-minus-slab: -1.2rem;
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
:key="action.uri"
|
||||
class="action-button button"
|
||||
:href="action.uri"
|
||||
:style="{
|
||||
background: widgetColor,
|
||||
borderColor: widgetColor,
|
||||
color: textColor,
|
||||
}"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
>
|
||||
|
@ -13,13 +18,15 @@
|
|||
v-else
|
||||
:key="action.payload"
|
||||
class="action-button button"
|
||||
:style="{ borderColor: widgetColor, color: widgetColor }"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ action.text }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
|
@ -29,6 +36,12 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
isLink() {
|
||||
return this.action.type === 'link';
|
||||
},
|
||||
|
|
|
@ -71,7 +71,11 @@
|
|||
v-if="!submittedValues.length"
|
||||
class="button block"
|
||||
type="submit"
|
||||
:style="{ background: widgetColor, borderColor: widgetColor }"
|
||||
:style="{
|
||||
background: widgetColor,
|
||||
borderColor: widgetColor,
|
||||
color: textColor,
|
||||
}"
|
||||
@click="onSubmitClick"
|
||||
>
|
||||
{{ buttonLabel || $t('COMPONENTS.FORM_BUBBLE.SUBMIT') }}
|
||||
|
@ -83,6 +87,7 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
mixins: [darkModeMixin],
|
||||
|
@ -110,6 +115,9 @@ export default {
|
|||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||
|
|
|
@ -32,7 +32,11 @@
|
|||
<button
|
||||
class="button small"
|
||||
:disabled="isButtonDisabled"
|
||||
:style="{ background: widgetColor, borderColor: widgetColor }"
|
||||
:style="{
|
||||
background: widgetColor,
|
||||
borderColor: widgetColor,
|
||||
color: textColor,
|
||||
}"
|
||||
>
|
||||
<spinner v-if="isUpdating && feedback" />
|
||||
<fluent-icon v-else icon="chevron-right" />
|
||||
|
@ -47,6 +51,7 @@ import Spinner from 'shared/components/Spinner';
|
|||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -89,6 +94,9 @@ export default {
|
|||
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
title() {
|
||||
return this.isRatingSubmitted
|
||||
? this.$t('CSAT.SUBMITTED_TITLE')
|
||||
|
|
|
@ -121,6 +121,7 @@
|
|||
"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-account-outline": "M11 15c0-.35.06-.687.17-1H4.253a2.249 2.249 0 0 0-2.249 2.249v.578c0 .892.319 1.756.899 2.435 1.566 1.834 3.952 2.74 7.098 2.74.397 0 .783-.015 1.156-.044A2.998 2.998 0 0 1 11 21v-.535c-.321.024-.655.036-1 .036-2.738 0-4.704-.746-5.958-2.213a2.25 2.25 0 0 1-.539-1.462v-.577c0-.414.336-.75.75-.75H11V15ZM10 2.005a5 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-7ZM12 15a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2v-6Zm2.5 1a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Z",
|
||||
"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",
|
||||
"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",
|
||||
|
|
|
@ -11,11 +11,7 @@ export const formatBytes = (bytes, decimals = 2) => {
|
|||
};
|
||||
|
||||
export const fileSizeInMegaBytes = bytes => {
|
||||
if (bytes === 0) {
|
||||
return 0;
|
||||
}
|
||||
const sizeInMB = (bytes / (1024 * 1024)).toFixed(2);
|
||||
return sizeInMB;
|
||||
return bytes / (1024 * 1024);
|
||||
};
|
||||
|
||||
export const checkFileSizeLimit = (file, maximumUploadLimit) => {
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('#File Helpers', () => {
|
|||
expect(fileSizeInMegaBytes(0)).toBe(0);
|
||||
});
|
||||
it('should return 19.07 if 20000000 is passed', () => {
|
||||
expect(fileSizeInMegaBytes(20000000)).toBe('19.07');
|
||||
expect(fileSizeInMegaBytes(20000000)).toBeCloseTo(19.07, 2);
|
||||
});
|
||||
});
|
||||
describe('checkFileSizeLimit', () => {
|
||||
|
|
|
@ -134,10 +134,20 @@ export default {
|
|||
});
|
||||
});
|
||||
},
|
||||
setLocale(locale) {
|
||||
setLocale(localeWithVariation) {
|
||||
const { enabledLanguages } = window.chatwootWebChannel;
|
||||
if (enabledLanguages.some(lang => lang.iso_639_1_code === locale)) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
const localeWithoutVariation = localeWithVariation.split('_')[0];
|
||||
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() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<file-upload
|
||||
ref="upload"
|
||||
:size="4096 * 2048"
|
||||
:accept="allowedFileTypes"
|
||||
:data="{
|
||||
|
@ -48,7 +49,23 @@ export default {
|
|||
return ALLOWED_FILE_TYPES;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('paste', this.handleClipboardPaste);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('paste', this.handleClipboardPaste);
|
||||
},
|
||||
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) {
|
||||
return fileType.includes('image') ? 'image' : 'file';
|
||||
},
|
||||
|
|
|
@ -191,6 +191,7 @@ export default {
|
|||
.emoji-dialog {
|
||||
right: $space-smaller;
|
||||
top: -278px;
|
||||
max-width: 100%;
|
||||
|
||||
&::before {
|
||||
right: $space-one;
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
<div
|
||||
v-dompurify-html="formatMessage(message, false)"
|
||||
class="chat-bubble user"
|
||||
:style="{ background: widgetColor }"
|
||||
:style="{ background: widgetColor, color: textColor }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
name: 'UserMessageBubble',
|
||||
|
@ -26,6 +27,11 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
<button
|
||||
class="button small"
|
||||
:disabled="$v.email.$invalid"
|
||||
:style="{ background: widgetColor, borderColor: widgetColor }"
|
||||
:style="{
|
||||
background: widgetColor,
|
||||
borderColor: widgetColor,
|
||||
color: textColor,
|
||||
}"
|
||||
>
|
||||
<fluent-icon v-if="!isUpdating" icon="chevron-right" />
|
||||
<spinner v-else class="mx-2" />
|
||||
|
@ -28,6 +32,7 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { required, email } from 'vuelidate/lib/validators';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
|
@ -59,6 +64,9 @@ export default {
|
|||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
}),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
hasSubmitted() {
|
||||
return (
|
||||
this.messageContentAttributes &&
|
||||
|
|
|
@ -8,7 +8,9 @@ class AutomationRuleListener < BaseListener
|
|||
|
||||
return unless rule_present?('conversation_updated', account)
|
||||
|
||||
@rules.each do |rule|
|
||||
rules = current_account_rules('conversation_updated', account)
|
||||
|
||||
rules.each do |rule|
|
||||
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation, { changed_attributes: changed_attributes }).perform
|
||||
AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
|
||||
end
|
||||
|
@ -23,7 +25,9 @@ class AutomationRuleListener < BaseListener
|
|||
|
||||
return unless rule_present?('conversation_created', account)
|
||||
|
||||
@rules.each do |rule|
|
||||
rules = current_account_rules('conversation_created', account)
|
||||
|
||||
rules.each do |rule|
|
||||
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation, { changed_attributes: changed_attributes }).perform
|
||||
::AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
|
||||
end
|
||||
|
@ -38,7 +42,9 @@ class AutomationRuleListener < BaseListener
|
|||
|
||||
return unless rule_present?('message_created', account)
|
||||
|
||||
@rules.each do |rule|
|
||||
rules = current_account_rules('message_created', account)
|
||||
|
||||
rules.each do |rule|
|
||||
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation,
|
||||
{ message: message, changed_attributes: changed_attributes }).perform
|
||||
::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present?
|
||||
|
@ -48,12 +54,15 @@ class AutomationRuleListener < BaseListener
|
|||
def rule_present?(event_name, account)
|
||||
return if account.blank?
|
||||
|
||||
@rules = AutomationRule.where(
|
||||
current_account_rules(event_name, account).any?
|
||||
end
|
||||
|
||||
def current_account_rules(event_name, account)
|
||||
AutomationRule.where(
|
||||
event_name: event_name,
|
||||
account_id: account.id,
|
||||
active: true
|
||||
)
|
||||
@rules.any?
|
||||
end
|
||||
|
||||
def performed_by_automation?(event_obj)
|
||||
|
|
|
@ -7,6 +7,8 @@ class SupportMailbox < ApplicationMailbox
|
|||
:decorate_mail
|
||||
|
||||
def process
|
||||
# to turn off spam conversation creation
|
||||
return unless @account.active?
|
||||
# prevent loop from chatwoot notification emails
|
||||
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])
|
||||
|
||||
records = records.text_search(params[:query]) if params[:query].present?
|
||||
records.page(current_page(params))
|
||||
end
|
||||
|
||||
def self.current_page(params)
|
||||
params[:page] || 1
|
||||
records
|
||||
end
|
||||
|
||||
def associate_root_article(associated_article_id)
|
||||
|
|
|
@ -210,7 +210,7 @@ class Conversation < ApplicationRecord
|
|||
|
||||
def mark_conversation_pending_if_bot
|
||||
# TODO: make this an inbox config instead of assuming bot conversations should start as pending
|
||||
self.status = :pending if inbox.agent_bot_inbox&.active? || inbox.hooks.pluck(:app_id).include?('dialogflow')
|
||||
self.status = :pending if inbox.active_bot?
|
||||
end
|
||||
|
||||
def notify_conversation_creation
|
||||
|
@ -219,7 +219,7 @@ class Conversation < ApplicationRecord
|
|||
|
||||
def notify_conversation_updation
|
||||
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)
|
||||
end
|
||||
|
|
|
@ -101,6 +101,10 @@ class Inbox < ApplicationRecord
|
|||
channel_type == 'Channel::Whatsapp'
|
||||
end
|
||||
|
||||
def active_bot?
|
||||
agent_bot_inbox&.active? || hooks.pluck(:app_id).include?('dialogflow')
|
||||
end
|
||||
|
||||
def inbox_type
|
||||
channel.name
|
||||
end
|
||||
|
|
|
@ -193,7 +193,18 @@ class Message < ApplicationRecord
|
|||
return if conversation.muted?
|
||||
return unless incoming?
|
||||
|
||||
conversation.open! if conversation.resolved? || conversation.snoozed?
|
||||
conversation.open! if conversation.snoozed?
|
||||
|
||||
reopen_resolved_conversation if conversation.resolved?
|
||||
end
|
||||
|
||||
def reopen_resolved_conversation
|
||||
# mark resolved bot conversation as pending to be reopened by bot processor service
|
||||
if conversation.inbox.active_bot?
|
||||
conversation.pending!
|
||||
else
|
||||
conversation.open!
|
||||
end
|
||||
end
|
||||
|
||||
def execute_message_template_hooks
|
||||
|
|
|
@ -8,6 +8,7 @@ class Conversations::EventDataPresenter < SimpleDelegator
|
|||
id: display_id,
|
||||
inbox_id: inbox_id,
|
||||
messages: push_messages,
|
||||
labels: label_list,
|
||||
meta: push_meta,
|
||||
status: status,
|
||||
custom_attributes: custom_attributes,
|
||||
|
|
|
@ -3,6 +3,7 @@ class Macros::ExecutionService < ActionService
|
|||
super(conversation)
|
||||
@macro = macro
|
||||
@account = macro.account
|
||||
@user = user
|
||||
Current.user = user
|
||||
end
|
||||
|
||||
|
@ -27,7 +28,7 @@ class Macros::ExecutionService < ActionService
|
|||
params = { content: message[0], private: false }
|
||||
|
||||
# Added reload here to ensure conversation us persistent with the latest updates
|
||||
mb = Messages::MessageBuilder.new(nil, @conversation.reload, params)
|
||||
mb = Messages::MessageBuilder.new(@user, @conversation.reload, params)
|
||||
mb.perform
|
||||
end
|
||||
|
||||
|
@ -43,7 +44,7 @@ class Macros::ExecutionService < ActionService
|
|||
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 = Messages::MessageBuilder.new(@user, @conversation.reload, params)
|
||||
mb.perform
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,7 +87,7 @@ class Whatsapp::IncomingMessageBaseService
|
|||
end
|
||||
|
||||
def unprocessable_message_type?
|
||||
%w[reaction contacts].include?(message_type)
|
||||
%w[reaction contacts ephemeral unsupported].include?(message_type)
|
||||
end
|
||||
|
||||
def attach_files
|
||||
|
|
|
@ -13,12 +13,6 @@ json.category do
|
|||
json.locale article.category.locale
|
||||
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
|
||||
|
||||
if article.author.present?
|
||||
|
|
|
@ -8,7 +8,7 @@ json.account_id article.account_id
|
|||
|
||||
if article.portal.present?
|
||||
json.portal do
|
||||
json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal
|
||||
json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal, articles: []
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ json.payload do
|
|||
end
|
||||
|
||||
json.meta do
|
||||
json.current_page @current_page
|
||||
json.articles_count @articles_count
|
||||
json.all_articles_count @portal_articles.size
|
||||
json.archived_articles_count @articles.archived.size
|
||||
json.articles_count @articles_count
|
||||
json.current_page @current_page
|
||||
json.draft_articles_count @all_articles.draft.size
|
||||
json.mine_articles_count @all_articles.search_by_author(current_user.id).size if current_user.present?
|
||||
json.published_count @articles.published.size
|
||||
json.draft_articles_count @articles.draft.size
|
||||
json.mine_articles_count @articles.search_by_author(current_user.id).size if current_user.present?
|
||||
end
|
||||
|
|
|
@ -27,5 +27,5 @@ if category.root_category.present?
|
|||
end
|
||||
|
||||
json.meta do
|
||||
json.articles_count category.articles.size
|
||||
json.articles_count category.articles.search(locale: @current_locale).size
|
||||
end
|
||||
|
|
|
@ -28,11 +28,11 @@ json.portal_members do
|
|||
end
|
||||
|
||||
json.meta do
|
||||
json.all_articles_count portal.articles.size
|
||||
json.archived_articles_count portal.articles.archived.size
|
||||
json.published_count portal.articles.published.size
|
||||
json.draft_articles_count portal.articles.draft.size
|
||||
json.mine_articles_count portal.articles.search_by_author(current_user.id).size if current_user.present?
|
||||
json.categories_count portal.categories.size
|
||||
json.all_articles_count articles.try(:size)
|
||||
json.archived_articles_count articles.try(:archived).try(:size)
|
||||
json.published_count articles.try(:published).try(:size)
|
||||
json.draft_articles_count articles.try(:draft).try(:size)
|
||||
json.mine_articles_count articles.search_by_author(current_user.id).try(:size) if current_user.present? && articles.any?
|
||||
json.categories_count portal.categories.try(:size)
|
||||
json.default_locale portal.default_locale
|
||||
end
|
||||
|
|
|
@ -1 +1 @@
|
|||
json.partial! 'portal', portal: @portal
|
||||
json.partial! 'portal', portal: @portal, articles: []
|
||||
|
|
|
@ -1 +1 @@
|
|||
json.partial! 'portal', portal: @portal
|
||||
json.partial! 'portal', portal: @portal, articles: []
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
json.payload do
|
||||
json.array! @portals, partial: 'portal', as: :portal
|
||||
json.array! @portals.each do |portal|
|
||||
json.partial! 'portal', formats: [:json], portal: portal, articles: []
|
||||
end
|
||||
end
|
||||
|
||||
json.meta do
|
||||
|
|
|
@ -1 +1 @@
|
|||
json.partial! 'portal', portal: @portal
|
||||
json.partial! 'portal', portal: @portal, articles: @articles
|
||||
|
|
|
@ -1 +1 @@
|
|||
json.partial! 'portal', portal: @portal
|
||||
json.partial! 'portal', portal: @portal, articles: []
|
||||
|
|
3
app/views/public/api/v1/inboxes/show.json.jbuilder
Normal file
3
app/views/public/api/v1/inboxes/show.json.jbuilder
Normal file
|
@ -0,0 +1,3 @@
|
|||
json.identifier @inbox_channel.identifier
|
||||
json.identity_validation_enabled @inbox_channel.hmac_mandatory
|
||||
json.partial! 'public/api/v1/models/inbox.json.jbuilder', resource: @inbox_channel.inbox
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue