Merge branch 'develop' into chore/email-notifications
This commit is contained in:
commit
f29fd3f80d
59 changed files with 1025 additions and 292 deletions
|
@ -184,3 +184,4 @@ AllCops:
|
|||
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
||||
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
||||
- db/migrate/20220809104508_revert_cascading_indexes.rb
|
||||
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -158,6 +158,10 @@ group :test do
|
|||
gem 'webmock'
|
||||
end
|
||||
|
||||
group :development, :test, :staging do
|
||||
gem 'faker'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem 'active_record_query_trace'
|
||||
##--- gems for debugging and error reporting ---##
|
||||
|
@ -167,7 +171,6 @@ group :development, :test do
|
|||
gem 'byebug', platform: :mri
|
||||
gem 'climate_control'
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
|
|
|
@ -58,7 +58,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||
|
||||
def portal_params
|
||||
params.require(:portal).permit(
|
||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, config: { allowed_locales: [] }
|
||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, { config: [:default_locale,
|
||||
{ allowed_locales: [] }] }
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -41,4 +41,9 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
|||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
|
||||
def seed
|
||||
Seeders::AccountSeeder.new(account: requested_resource).perform!
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@ require 'administrate/field/base'
|
|||
|
||||
class AvatarField < Administrate::Field::Base
|
||||
def avatar_url
|
||||
data.presence&.gsub('?d=404', '?d=mp')
|
||||
return data.presence if data.presence
|
||||
|
||||
resource.is_a?(User) ? '/assets/administrate/user/avatar.png' : '/assets/administrate/bot/avatar.png'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,6 +42,10 @@ class ArticlesAPI extends PortalsAPI {
|
|||
category_id,
|
||||
});
|
||||
}
|
||||
|
||||
deleteArticle({ articleId, portalSlug }) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ArticlesAPI();
|
||||
|
|
|
@ -9,6 +9,10 @@ class PortalsAPI extends ApiClient {
|
|||
updatePortal({ portalSlug, params }) {
|
||||
return axios.patch(`${this.url}/${portalSlug}`, params);
|
||||
}
|
||||
|
||||
deletePortal(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortalsAPI;
|
||||
|
|
|
@ -52,4 +52,15 @@ describe('#PortalAPI', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#deleteArticle', () => {
|
||||
articlesAPI.deleteArticle({
|
||||
articleId: 1,
|
||||
portalSlug: 'room-rental',
|
||||
});
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/portals/room-rental/articles/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
|
||||
.modal-footer {
|
||||
@include flex;
|
||||
@include flex-align($x: flex-start, $y: middle);
|
||||
@include flex-align($x: flex-end, $y: middle);
|
||||
padding: $space-small $zero;
|
||||
|
||||
button {
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
</h2>
|
||||
<p v-if="headerContent" class="small-12 column wrap-content">
|
||||
{{ headerContent }}
|
||||
<span v-if="headerContentValue" class="content-value">
|
||||
{{ headerContentValue }}
|
||||
</span>
|
||||
</p>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -22,6 +25,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerContentValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
@ -32,5 +39,8 @@ export default {
|
|||
<style scoped lang="scss">
|
||||
.wrap-content {
|
||||
word-wrap: break-word;
|
||||
.content-value {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</span>
|
||||
<div v-if="isHelpCenterSidebar" class="submenu-icons">
|
||||
<!-- Hidden since this is in V2
|
||||
<div class="submenu-icon">
|
||||
<fluent-icon icon="search" size="16" />
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="submenu-icon" @click="onClickOpen">
|
||||
<fluent-icon icon="add" size="16" />
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
:icon="icon"
|
||||
:icon-size="iconSize"
|
||||
/>
|
||||
<span v-if="$slots.default" class="button__content"><slot /></span>
|
||||
<span v-if="$slots.default" class="button__content">
|
||||
<slot />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header :header-title="title" :header-content="message" />
|
||||
<woot-modal-header
|
||||
:header-title="title"
|
||||
:header-content="message"
|
||||
:header-content-value="messageValue"
|
||||
/>
|
||||
<div class="modal-footer delete-item">
|
||||
<button class="alert button nice text-truncate" @click="onConfirm">
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
<button class="button clear text-truncate" @click="onClose">
|
||||
<woot-button variant="clear" class="action-button" @click="onClose">
|
||||
{{ rejectText }}
|
||||
</button>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="alert"
|
||||
class="action-button"
|
||||
variant="smooth"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -25,8 +34,14 @@ export default {
|
|||
onConfirm: { type: Function, default: () => {} },
|
||||
title: { type: String, default: '' },
|
||||
message: { type: String, default: '' },
|
||||
messageValue: { type: String, default: '' },
|
||||
confirmText: { type: String, default: '' },
|
||||
rejectText: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.action-button {
|
||||
max-width: var(--space-giga);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"NOT_AVAILABLE": "Not Available",
|
||||
"EMAIL_ADDRESS": "Email Address",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"COPY_SUCCESSFUL": "Copied to clipboard successfully",
|
||||
"COMPANY": "Company",
|
||||
"LOCATION": "Location",
|
||||
|
|
|
@ -84,7 +84,8 @@
|
|||
"COUNT_LABEL": "articles",
|
||||
"ADD": "Add locale",
|
||||
"VISIT": "Visit site",
|
||||
"SETTINGS": "Settings"
|
||||
"SETTINGS": "Settings",
|
||||
"DELETE": "Delete"
|
||||
},
|
||||
"PORTAL_CONFIG": {
|
||||
"TITLE": "Portal Configurations",
|
||||
|
@ -109,6 +110,16 @@
|
|||
"DEFAULT_LOCALE": "Default"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_PORTAL": {
|
||||
"TITLE": "Delete portal",
|
||||
"MESSAGE": "Are you sure you want to delete this portal",
|
||||
"YES": "Yes, delete portal",
|
||||
"NO": "No, keep portal",
|
||||
"API": {
|
||||
"DELETE_SUCCESS": "Portal deleted successfully",
|
||||
"DELETE_ERROR": "Error while deleting portal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
|
@ -224,6 +235,26 @@
|
|||
"ERROR": "Error while saving article"
|
||||
}
|
||||
},
|
||||
"ARCHIVE_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR": "Error while archiving article",
|
||||
"SUCCESS": "Article archived successfully"
|
||||
}
|
||||
},
|
||||
"DELETE_ARTICLE": {
|
||||
"MODAL": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete the article?",
|
||||
"YES": "Yes, Delete",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Article deleted successfully",
|
||||
"ERROR_MESSAGE": "Error while deleting article"
|
||||
}
|
||||
},
|
||||
"CREATE_ARTICLE": {
|
||||
"ERROR_MESSAGE": "Please add the article heading and content then only you can update the settings"
|
||||
},
|
||||
|
|
|
@ -48,6 +48,13 @@
|
|||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
/>
|
||||
<contact-info-row
|
||||
v-if="contact.identifier"
|
||||
:value="contact.identifier"
|
||||
icon="contact-identify"
|
||||
emoji="🪪"
|
||||
:title="$t('CONTACT_PANEL.IDENTIFIER')"
|
||||
/>
|
||||
<contact-info-row
|
||||
:value="additionalAttributes.company_name"
|
||||
icon="building-bank"
|
||||
|
@ -131,7 +138,8 @@
|
|||
:on-close="closeDelete"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
|
||||
:message="confirmDeleteMessage"
|
||||
:message="$t('DELETE_CONTACT.CONFIRM.MESSAGE')"
|
||||
:message-value="confirmDeleteMessage"
|
||||
:confirm-text="$t('DELETE_CONTACT.CONFIRM.YES')"
|
||||
:reject-text="$t('DELETE_CONTACT.CONFIRM.NO')"
|
||||
/>
|
||||
|
@ -215,9 +223,7 @@ export default {
|
|||
},
|
||||
// Delete Modal
|
||||
confirmDeleteMessage() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${
|
||||
this.contact.name
|
||||
} ?`;
|
||||
return ` ${this.contact.name}?`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
:on-close="closeDeletePopup"
|
||||
:on-confirm="deleteSavedCustomViews"
|
||||
:title="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:message="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
|
@ -51,9 +52,7 @@ export default {
|
|||
return '';
|
||||
},
|
||||
deleteMessage() {
|
||||
return `${this.$t(
|
||||
'FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.MESSAGE'
|
||||
)} ${this.activeCustomView && this.activeCustomView.name} ?`;
|
||||
return ` ${this.activeCustomView && this.activeCustomView.name}?`;
|
||||
},
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.YES')}`;
|
||||
|
|
|
@ -67,7 +67,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
|
@ -105,11 +104,8 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedPortal: 'portals/getSelectedPortal',
|
||||
}),
|
||||
selectedPortalSlug() {
|
||||
return this.selectedPortal?.slug;
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
nameError() {
|
||||
if (this.$v.name.$error) {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PREVIEW') }}
|
||||
</woot-button>
|
||||
<!-- Hidden since this is in V2
|
||||
<woot-button
|
||||
class-names="article--buttons"
|
||||
icon="add"
|
||||
|
@ -35,7 +36,7 @@
|
|||
@click="onClickAdd"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.ADD_TRANSLATION') }}
|
||||
</woot-button>
|
||||
</woot-button> -->
|
||||
<woot-button
|
||||
v-if="!isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.OPEN_SIDEBAR')"
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<portal-popover
|
||||
v-if="showPortalPopover"
|
||||
:portals="portals"
|
||||
:active-portal="selectedPortal"
|
||||
:active-portal-slug="selectedPortalSlug"
|
||||
@close-popover="closePortalPopover"
|
||||
/>
|
||||
<add-category
|
||||
|
@ -77,18 +77,24 @@ export default {
|
|||
showNotificationPanel: false,
|
||||
showPortalPopover: false,
|
||||
showAddCategoryModal: false,
|
||||
lastActivePortalSlug: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
selectedPortal: 'portals/getSelectedPortal',
|
||||
portals: 'portals/allPortals',
|
||||
categories: 'categories/allCategories',
|
||||
meta: 'portals/getMeta',
|
||||
isFetching: 'portals/isFetchingPortals',
|
||||
}),
|
||||
selectedPortal() {
|
||||
const slug = this.$route.params.portalSlug || this.lastActivePortalSlug;
|
||||
if (slug) return this.$store.getters['portals/portalBySlug'](slug);
|
||||
|
||||
return this.$store.getters['portals/allPortals'][0];
|
||||
},
|
||||
sidebarClassName() {
|
||||
if (this.isOnDesktop) {
|
||||
return '';
|
||||
|
@ -111,12 +117,15 @@ export default {
|
|||
return this.selectedPortal ? this.selectedPortal.name : '';
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.portalSlug || this.selectedPortal?.slug;
|
||||
return this.selectedPortal ? this.selectedPortal?.slug : '';
|
||||
},
|
||||
selectedPortalLocale() {
|
||||
return this.locale || this.selectedPortal?.meta?.default_locale;
|
||||
return this.selectedPortal
|
||||
? this.selectedPortal?.meta?.default_locale
|
||||
: '';
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.selectedPortal) return [];
|
||||
const {
|
||||
meta: {
|
||||
all_articles_count: allArticlesCount,
|
||||
|
@ -192,22 +201,30 @@ export default {
|
|||
];
|
||||
},
|
||||
currentRoute() {
|
||||
return ' ';
|
||||
return ' ';
|
||||
},
|
||||
headerTitle() {
|
||||
return this.selectedPortal.name;
|
||||
return this.selectedPortal ? this.selectedPortal.name : '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) this.lastActivePortalSlug = slug;
|
||||
|
||||
this.fetchPortalsAndItsCategories();
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
updated() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) this.lastActivePortalSlug = slug;
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
if (window.innerWidth > 1200) {
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
<header>
|
||||
<div>
|
||||
<div class="title-status--wrap">
|
||||
<h2 class="portal-title block-title">{{ portal.name }}</h2>
|
||||
<h2 class="portal-title block-title">
|
||||
{{ portal.name }}
|
||||
</h2>
|
||||
<Label
|
||||
:title="status"
|
||||
:color-scheme="labelColor"
|
||||
|
@ -59,6 +61,17 @@
|
|||
color-scheme="secondary"
|
||||
@click="openSettings"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.DELETE')
|
||||
"
|
||||
variant="hollow"
|
||||
color-scheme="alert"
|
||||
size="small"
|
||||
icon="delete"
|
||||
class="header-action-buttons"
|
||||
@click="onClickOpenDeleteModal(portal)"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="portal-locales">
|
||||
|
@ -77,7 +90,9 @@
|
|||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.NAME'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-block-title">{{ portal.name }}</span>
|
||||
<span class="text-block-title">
|
||||
{{ portal.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="configuration-item">
|
||||
<label>{{
|
||||
|
@ -85,7 +100,9 @@
|
|||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.DOMAIN'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-block-title">{{ portal.custom_domain }}</span>
|
||||
<span class="text-block-title">
|
||||
{{ portal.custom_domain }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configuration-items">
|
||||
|
@ -95,7 +112,9 @@
|
|||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SLUG'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-block-title">{{ portal.slug }}</span>
|
||||
<span class="text-block-title">
|
||||
{{ portal.slug }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="configuration-item">
|
||||
<label>{{
|
||||
|
@ -103,7 +122,9 @@
|
|||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.TITLE'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-block-title">{{ portal.page_title }}</span>
|
||||
<span class="text-block-title">
|
||||
{{ portal.page_title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="configuration-items">
|
||||
|
@ -126,7 +147,9 @@
|
|||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-block-title">{{ portal.header_text }}</span>
|
||||
<span class="text-block-title">
|
||||
{{ portal.header_text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -148,6 +171,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="onClickDeletePortal"
|
||||
:title="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.TITLE')"
|
||||
:message="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.MESSAGE')"
|
||||
:message-value="deleteMessageValue"
|
||||
:confirm-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.YES')"
|
||||
:reject-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -155,12 +188,14 @@
|
|||
import thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import Label from 'dashboard/components/ui/Label';
|
||||
import LocaleItemTable from './PortalListItemTable';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
Label,
|
||||
LocaleItemTable,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
|
@ -172,6 +207,13 @@ export default {
|
|||
values: ['archived', 'draft', 'published'],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
alertMessage: '',
|
||||
selectedPortalForDelete: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelColor() {
|
||||
switch (this.status) {
|
||||
|
@ -181,6 +223,10 @@ export default {
|
|||
return 'success';
|
||||
}
|
||||
},
|
||||
// Delete portal modal
|
||||
deleteMessageValue() {
|
||||
return ` ${this.selectedPortalForDelete.name}?`;
|
||||
},
|
||||
|
||||
locales() {
|
||||
return this.portal ? this.portal.config.allowed_locales : [];
|
||||
|
@ -194,7 +240,35 @@ export default {
|
|||
this.$emit('open-site');
|
||||
},
|
||||
openSettings() {
|
||||
this.$emit('open');
|
||||
this.navigateToPortalEdit();
|
||||
},
|
||||
onClickOpenDeleteModal(portal) {
|
||||
this.selectedPortalForDelete = portal;
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
async onClickDeletePortal() {
|
||||
const { slug } = this.selectedPortalForDelete;
|
||||
try {
|
||||
await this.$store.dispatch('portals/delete', {
|
||||
portalSlug: slug,
|
||||
});
|
||||
this.selectedPortalForDelete = {};
|
||||
this.closeDeletePopup();
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
|
||||
);
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR'
|
||||
);
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
swapLocale() {
|
||||
this.$emit('swap');
|
||||
|
@ -202,6 +276,12 @@ export default {
|
|||
deleteLocale() {
|
||||
this.$emit('delete');
|
||||
},
|
||||
navigateToPortalEdit() {
|
||||
this.$router.push({
|
||||
name: 'edit_portal_information',
|
||||
params: { portalSlug: this.portal.slug },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
color-scheme="secondary"
|
||||
icon="settings"
|
||||
size="small"
|
||||
@click="openPortalPage"
|
||||
@click="openPortalArticles"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.PORTAL_SETTINGS') }}
|
||||
</woot-button>
|
||||
|
@ -24,7 +24,7 @@
|
|||
v-for="portal in portals"
|
||||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:active="portal.id === activePortal.id"
|
||||
:active="portal.slug === activePortalSlug"
|
||||
@open-portal-page="openPortalPage"
|
||||
/>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<woot-button variant="link" @click="closePortalPopover">
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
<woot-button @click="() => {}">
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.CHOOSE_LOCALE_BUTTON') }}
|
||||
</woot-button>
|
||||
</footer>
|
||||
|
@ -52,19 +52,26 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activePortal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
closePortalPopover() {
|
||||
this.$emit('close-popover');
|
||||
},
|
||||
openPortalPage() {
|
||||
openPortalArticles({ slug, locale }) {
|
||||
this.$emit('close-popover');
|
||||
const portal = this.portals.find(p => p.slug === slug);
|
||||
this.$store.dispatch('portals/setPortalId', portal.id);
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: slug,
|
||||
locale: locale,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -127,7 +127,7 @@ export default {
|
|||
props: {
|
||||
article: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -27,6 +27,17 @@
|
|||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
@delete-article="openDeletePopup"
|
||||
@archive-article="archiveArticle"
|
||||
/>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.TITLE')"
|
||||
:message="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.MESSAGE')"
|
||||
:confirm-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.YES')"
|
||||
:reject-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -52,13 +63,13 @@ export default {
|
|||
isSaved: false,
|
||||
showArticleSettings: false,
|
||||
alertMessage: '',
|
||||
showDeleteConfirmationPopup: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFetching: 'articles/isFetching',
|
||||
articles: 'articles/articles',
|
||||
selectedPortal: 'portals/getSelectedPortal',
|
||||
}),
|
||||
article() {
|
||||
return this.$store.getters['articles/articleById'](this.articleId);
|
||||
|
@ -67,7 +78,7 @@ export default {
|
|||
return this.$route.params.articleSlug;
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.portalSlug || this.selectedPortal?.slug;
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -83,6 +94,16 @@ export default {
|
|||
portalSlug: this.selectedPortalSlug,
|
||||
});
|
||||
},
|
||||
openDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.closeDeletePopup();
|
||||
this.deleteArticle();
|
||||
},
|
||||
async saveArticle({ ...values }) {
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
|
@ -93,8 +114,7 @@ export default {
|
|||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR_MESSAGE');
|
||||
error?.message || this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR');
|
||||
this.showAlert(this.alertMessage);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
|
@ -103,6 +123,45 @@ export default {
|
|||
}, 1500);
|
||||
}
|
||||
},
|
||||
async deleteArticle() {
|
||||
try {
|
||||
await this.$store.dispatch('articles/delete', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
articleId: this.articleId,
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.DELETE_ARTICLE.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
locale: this.locale,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.DELETE_ARTICLE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
async archiveArticle() {
|
||||
try {
|
||||
await this.$store.dispatch('articles/update', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
articleId: this.articleId,
|
||||
status: 2,
|
||||
});
|
||||
this.alertMessage = this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.SUCCESS');
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message || this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.ERROR');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
openArticleSettings() {
|
||||
this.showArticleSettings = true;
|
||||
},
|
||||
|
|
|
@ -46,7 +46,6 @@ export default {
|
|||
...mapGetters({
|
||||
articles: 'articles/allArticles',
|
||||
categories: 'categories/allCategories',
|
||||
selectedPortal: 'portals/getSelectedPortal',
|
||||
uiFlags: 'articles/uiFlags',
|
||||
meta: 'articles/getMeta',
|
||||
isFetching: 'articles/isFetching',
|
||||
|
@ -64,7 +63,7 @@ export default {
|
|||
return this.isFetching && !this.articles.length;
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.selectedPortal?.slug;
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
selectedCategorySlug() {
|
||||
const { categorySlug } = this.$route.params;
|
||||
|
|
|
@ -46,7 +46,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedPortal: 'portals/getSelectedPortal',
|
||||
currentUserID: 'getCurrentUserID',
|
||||
articles: 'articles/articles',
|
||||
categories: 'categories/allCategories',
|
||||
|
@ -58,7 +57,7 @@ export default {
|
|||
return { title: this.articleTitle, content: this.articleContent };
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.portalSlug || this.selectedPortal?.slug;
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
categoryId() {
|
||||
return this.categories.length ? this.categories[0].id : null;
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import PortalListItem from '../../components/PortalListItem';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState';
|
||||
|
@ -44,6 +45,7 @@ export default {
|
|||
Spinner,
|
||||
AddPortal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
isAddModalOpen: false,
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
}),
|
||||
createdPortalSlug() {
|
||||
const {
|
||||
params: { portal_slug: slug },
|
||||
params: { portalSlug: slug },
|
||||
} = this.$route;
|
||||
return slug;
|
||||
},
|
||||
|
@ -120,9 +120,6 @@ export default {
|
|||
allowed_locales: ['en'],
|
||||
},
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE'
|
||||
);
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
</label>
|
||||
<div class="logo-container">
|
||||
<thumbnail :username="name" size="56" variant="square" />
|
||||
<!-- Hidden since this is in V2
|
||||
<woot-button
|
||||
class="upload-button"
|
||||
variant="smooth"
|
||||
|
@ -25,7 +26,7 @@
|
|||
size="small"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.UPLOAD_BUTTON') }}
|
||||
</woot-button>
|
||||
</woot-button> -->
|
||||
</div>
|
||||
<p class="logo-help--text">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.HELP_TEXT') }}
|
||||
|
@ -145,9 +146,6 @@ export default {
|
|||
custom_domain: this.domain,
|
||||
},
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
||||
);
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
|
|
|
@ -117,7 +117,8 @@
|
|||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('AGENT_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:message="$t('AGENT_MGMT.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
|
@ -167,9 +168,7 @@ export default {
|
|||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.currentAgent.name
|
||||
} ?`;
|
||||
return ` ${this.currentAgent.name}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -98,7 +98,8 @@
|
|||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:message="$t('AUTOMATION.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
|
@ -165,9 +166,7 @@ export default {
|
|||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return `${this.$t('AUTOMATION.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.selectedResponse.name
|
||||
} ?`;
|
||||
return ` ${this.selectedResponse.name}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -99,7 +99,8 @@
|
|||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('CANNED_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:message="$t('CANNED_MGMT.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
|
@ -144,9 +145,7 @@ export default {
|
|||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.selectedResponse.short_code
|
||||
} ?`;
|
||||
return ` ${this.selectedResponse.short_code}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -91,7 +91,8 @@
|
|||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:message="$t('LABEL_MGMT.DELETE.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
|
@ -136,9 +137,7 @@ export default {
|
|||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return `${this.$t('LABEL_MGMT.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.selectedResponse.title
|
||||
} ?`;
|
||||
return ` ${this.selectedResponse.title}?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -100,7 +100,7 @@ export const actions = {
|
|||
});
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, articleId) => {
|
||||
delete: async ({ commit }, { portalSlug, articleId }) => {
|
||||
commit(types.UPDATE_ARTICLE_FLAG, {
|
||||
uiFlags: {
|
||||
isDeleting: true,
|
||||
|
@ -108,8 +108,7 @@ export const actions = {
|
|||
articleId,
|
||||
});
|
||||
try {
|
||||
await articlesAPI.delete(articleId);
|
||||
|
||||
await articlesAPI.deleteArticle({ portalSlug, articleId });
|
||||
commit(types.REMOVE_ARTICLE, articleId);
|
||||
commit(types.REMOVE_ARTICLE_ID, articleId);
|
||||
return articleId;
|
||||
|
|
|
@ -13,9 +13,11 @@ export const getters = {
|
|||
},
|
||||
allArticles: (...getterArguments) => {
|
||||
const [state, _getters] = getterArguments;
|
||||
const articles = state.articles.allIds.map(id => {
|
||||
return _getters.articleById(id);
|
||||
});
|
||||
const articles = state.articles.allIds
|
||||
.map(id => {
|
||||
return _getters.articleById(id);
|
||||
})
|
||||
.filter(article => article !== undefined);
|
||||
return articles;
|
||||
},
|
||||
getMeta: state => {
|
||||
|
|
|
@ -142,7 +142,11 @@ describe('#actions', () => {
|
|||
describe('#delete', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({ data: articleList[0] });
|
||||
await actions.delete({ commit }, articleList[0].id);
|
||||
await actions.delete(
|
||||
{ commit },
|
||||
{ portalSlug: 'test', articleId: articleList[0].id }
|
||||
);
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.default.UPDATE_ARTICLE_FLAG,
|
||||
|
@ -159,7 +163,10 @@ describe('#actions', () => {
|
|||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.delete({ commit }, articleList[0].id)
|
||||
actions.delete(
|
||||
{ commit },
|
||||
{ portalSlug: 'test', articleId: articleList[0].id }
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
|
|
|
@ -3,21 +3,17 @@ import { throwErrorMessage } from 'dashboard/store/utils/api';
|
|||
import { types } from './mutations';
|
||||
const portalAPIs = new PortalAPI();
|
||||
export const actions = {
|
||||
index: async ({ commit, state, dispatch }) => {
|
||||
index: async ({ commit }) => {
|
||||
try {
|
||||
commit(types.SET_UI_FLAG, { isFetching: true });
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await portalAPIs.get();
|
||||
commit(types.CLEAR_PORTALS);
|
||||
const portalIds = payload.map(portal => portal.id);
|
||||
const portalSlugs = payload.map(portal => portal.slug);
|
||||
commit(types.ADD_MANY_PORTALS_ENTRY, payload);
|
||||
commit(types.ADD_MANY_PORTALS_IDS, portalIds);
|
||||
const { selectedPortalId } = state;
|
||||
// Check if selected portal is still in the portals list
|
||||
if (!portalIds.includes(selectedPortalId)) {
|
||||
dispatch('setPortalId', portalIds[0]);
|
||||
}
|
||||
commit(types.ADD_MANY_PORTALS_IDS, portalSlugs);
|
||||
|
||||
commit(types.SET_PORTALS_META, meta);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
|
@ -26,20 +22,13 @@ export const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
create: async ({ commit, state, dispatch }, params) => {
|
||||
create: async ({ commit }, params) => {
|
||||
commit(types.SET_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const { data } = await portalAPIs.create(params);
|
||||
const { id: portalId } = data;
|
||||
const { slug: portalSlug } = data;
|
||||
commit(types.ADD_PORTAL_ENTRY, data);
|
||||
commit(types.ADD_PORTAL_ID, portalId);
|
||||
const {
|
||||
portals: { selectedPortalId },
|
||||
} = state;
|
||||
// Check if there are any selected portal
|
||||
if (!selectedPortalId) {
|
||||
dispatch('setPortalId', portalId);
|
||||
}
|
||||
commit(types.ADD_PORTAL_ID, portalSlug);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
|
@ -47,47 +36,47 @@ export const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
update: async ({ commit }, params) => {
|
||||
const portalId = params.id;
|
||||
const portalSlug = params.slug;
|
||||
update: async ({ commit }, { portalObj }) => {
|
||||
const portalSlug = portalObj.slug;
|
||||
commit(types.SET_HELP_PORTAL_UI_FLAG, {
|
||||
uiFlags: { isUpdating: true },
|
||||
portalId,
|
||||
portalSlug,
|
||||
});
|
||||
try {
|
||||
const { data } = await portalAPIs.updatePortal({ portalSlug, params });
|
||||
const { data } = await portalAPIs.updatePortal({
|
||||
portalSlug,
|
||||
portalObj,
|
||||
});
|
||||
commit(types.UPDATE_PORTAL_ENTRY, data);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_HELP_PORTAL_UI_FLAG, {
|
||||
uiFlags: { isUpdating: false },
|
||||
portalId,
|
||||
portalSlug,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ commit }, portalId) => {
|
||||
delete: async ({ commit }, { portalSlug }) => {
|
||||
commit(types.SET_HELP_PORTAL_UI_FLAG, {
|
||||
uiFlags: { isDeleting: true },
|
||||
portalId,
|
||||
portalSlug,
|
||||
});
|
||||
try {
|
||||
await portalAPIs.delete(portalId);
|
||||
commit(types.REMOVE_PORTAL_ENTRY, portalId);
|
||||
commit(types.REMOVE_PORTAL_ID, portalId);
|
||||
await portalAPIs.delete(portalSlug);
|
||||
commit(types.REMOVE_PORTAL_ENTRY, portalSlug);
|
||||
commit(types.REMOVE_PORTAL_ID, portalSlug);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_HELP_PORTAL_UI_FLAG, {
|
||||
uiFlags: { isDeleting: false },
|
||||
portalId,
|
||||
portalSlug,
|
||||
});
|
||||
}
|
||||
},
|
||||
setPortalId: async ({ commit }, portalId) => {
|
||||
commit(types.SET_SELECTED_PORTAL_ID, portalId);
|
||||
},
|
||||
|
||||
updatePortal: async ({ commit }, portal) => {
|
||||
commit(types.UPDATE_PORTAL_ENTRY, portal);
|
||||
},
|
||||
|
|
|
@ -6,18 +6,16 @@ export const getters = {
|
|||
},
|
||||
|
||||
isFetchingPortals: state => state.uiFlags.isFetching,
|
||||
portalById: (...getterArguments) => portalId => {
|
||||
portalBySlug: (...getterArguments) => portalId => {
|
||||
const [state] = getterArguments;
|
||||
const portal = state.portals.byId[portalId];
|
||||
|
||||
return {
|
||||
...portal,
|
||||
};
|
||||
return portal;
|
||||
},
|
||||
allPortals: (...getterArguments) => {
|
||||
const [state, _getters] = getterArguments;
|
||||
const portals = state.portals.allIds.map(id => {
|
||||
return _getters.portalById(id);
|
||||
return _getters.portalBySlug(id);
|
||||
});
|
||||
return portals;
|
||||
},
|
||||
|
@ -25,9 +23,4 @@ export const getters = {
|
|||
getMeta: state => {
|
||||
return state.meta;
|
||||
},
|
||||
getSelectedPortal: (...getterArguments) => {
|
||||
const [state, _getters] = getterArguments;
|
||||
const { selectedPortalId } = state.portals;
|
||||
return _getters.portalById(selectedPortalId);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,7 +25,6 @@ const state = {
|
|||
meta: {
|
||||
byId: {},
|
||||
},
|
||||
selectedPortalId: null,
|
||||
},
|
||||
uiFlags: {
|
||||
allFetched: false,
|
||||
|
|
|
@ -9,7 +9,6 @@ export const types = {
|
|||
ADD_PORTAL_ID: 'addPortalId',
|
||||
CLEAR_PORTALS: 'clearPortals',
|
||||
ADD_MANY_PORTALS_IDS: 'addManyPortalsIds',
|
||||
SET_SELECTED_PORTAL_ID: 'setSelectedPortalId',
|
||||
UPDATE_PORTAL_ENTRY: 'updatePortalEntry',
|
||||
REMOVE_PORTAL_ENTRY: 'removePortalEntry',
|
||||
REMOVE_PORTAL_ID: 'removePortalId',
|
||||
|
@ -25,7 +24,7 @@ export const mutations = {
|
|||
},
|
||||
|
||||
[types.ADD_PORTAL_ENTRY]($state, portal) {
|
||||
Vue.set($state.portals.byId, portal.id, {
|
||||
Vue.set($state.portals.byId, portal.slug, {
|
||||
...portal,
|
||||
});
|
||||
},
|
||||
|
@ -33,7 +32,7 @@ export const mutations = {
|
|||
[types.ADD_MANY_PORTALS_ENTRY]($state, portals) {
|
||||
const allPortals = { ...$state.portals.byId };
|
||||
portals.forEach(portal => {
|
||||
allPortals[portal.id] = portal;
|
||||
allPortals[portal.slug] = portal;
|
||||
});
|
||||
Vue.set($state.portals, 'byId', allPortals);
|
||||
},
|
||||
|
@ -41,7 +40,7 @@ export const mutations = {
|
|||
[types.CLEAR_PORTALS]: $state => {
|
||||
Vue.set($state.portals, 'byId', {});
|
||||
Vue.set($state.portals, 'allIds', []);
|
||||
Vue.set($state.portals, 'uiFlags.byId', {});
|
||||
Vue.set($state.portals.uiFlags, 'byId', {});
|
||||
},
|
||||
|
||||
[types.SET_PORTALS_META]: ($state, data) => {
|
||||
|
@ -50,41 +49,39 @@ export const mutations = {
|
|||
Vue.set($state.meta, 'currentPage', currentPage);
|
||||
},
|
||||
|
||||
[types.SET_SELECTED_PORTAL_ID]: ($state, portalId) => {
|
||||
Vue.set($state.portals, 'selectedPortalId', portalId);
|
||||
[types.ADD_PORTAL_ID]($state, portalSlug) {
|
||||
$state.portals.allIds.push(portalSlug);
|
||||
},
|
||||
|
||||
[types.ADD_PORTAL_ID]($state, portalId) {
|
||||
$state.portals.allIds.push(portalId);
|
||||
},
|
||||
|
||||
[types.ADD_MANY_PORTALS_IDS]($state, portalIds) {
|
||||
$state.portals.allIds.push(...portalIds);
|
||||
[types.ADD_MANY_PORTALS_IDS]($state, portalSlugs) {
|
||||
$state.portals.allIds.push(...portalSlugs);
|
||||
},
|
||||
|
||||
[types.UPDATE_PORTAL_ENTRY]($state, portal) {
|
||||
const portalId = portal.id;
|
||||
if (!$state.portals.allIds.includes(portalId)) return;
|
||||
const portalSlug = portal.slug;
|
||||
if (!$state.portals.allIds.includes(portalSlug)) return;
|
||||
|
||||
Vue.set($state.portals.byId, portalId, {
|
||||
Vue.set($state.portals.byId, portalSlug, {
|
||||
...portal,
|
||||
});
|
||||
},
|
||||
|
||||
[types.REMOVE_PORTAL_ENTRY]($state, portalId) {
|
||||
if (!portalId) return;
|
||||
[types.REMOVE_PORTAL_ENTRY]($state, portalSlug) {
|
||||
if (!portalSlug) return;
|
||||
|
||||
const { [portalId]: toBeRemoved, ...newById } = $state.portals.byId;
|
||||
const { [portalSlug]: toBeRemoved, ...newById } = $state.portals.byId;
|
||||
Vue.set($state.portals, 'byId', newById);
|
||||
},
|
||||
|
||||
[types.REMOVE_PORTAL_ID]($state, portalId) {
|
||||
$state.portals.allIds = $state.portals.allIds.filter(id => id !== portalId);
|
||||
[types.REMOVE_PORTAL_ID]($state, portalSlug) {
|
||||
$state.portals.allIds = $state.portals.allIds.filter(
|
||||
slug => slug !== portalSlug
|
||||
);
|
||||
},
|
||||
|
||||
[types.SET_HELP_PORTAL_UI_FLAG]($state, { portalId, uiFlags }) {
|
||||
const flags = $state.portals.uiFlags.byId[portalId];
|
||||
Vue.set($state.portals.uiFlags.byId, portalId, {
|
||||
[types.SET_HELP_PORTAL_UI_FLAG]($state, { portalSlug, uiFlags }) {
|
||||
const flags = $state.portals.uiFlags.byId[portalSlug];
|
||||
Vue.set($state.portals.uiFlags.byId, portalSlug, {
|
||||
...defaultPortalFlags,
|
||||
...flags,
|
||||
...uiFlags,
|
||||
|
|
|
@ -15,16 +15,13 @@ describe('#actions', () => {
|
|||
await actions.index({
|
||||
commit,
|
||||
dispatch,
|
||||
state: {
|
||||
selectedPortalId: 4,
|
||||
},
|
||||
state: {},
|
||||
});
|
||||
expect(dispatch.mock.calls).toMatchObject([['setPortalId', 1]]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_UI_FLAG, { isFetching: true }],
|
||||
[types.CLEAR_PORTALS],
|
||||
[types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload],
|
||||
[types.ADD_MANY_PORTALS_IDS, [1, 2]],
|
||||
[types.ADD_MANY_PORTALS_IDS, ['domain', 'campaign']],
|
||||
[types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }],
|
||||
[types.SET_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
|
@ -43,7 +40,7 @@ describe('#actions', () => {
|
|||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: apiResponse.payload[1] });
|
||||
await actions.create(
|
||||
{ commit, dispatch, state: { portals: { selectedPortalId: null } } },
|
||||
{ commit, dispatch, state: { portals: {} } },
|
||||
{
|
||||
color: 'red',
|
||||
custom_domain: 'domain_for_help',
|
||||
|
@ -53,17 +50,14 @@ describe('#actions', () => {
|
|||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_UI_FLAG, { isCreating: true }],
|
||||
[types.ADD_PORTAL_ENTRY, apiResponse.payload[1]],
|
||||
[types.ADD_PORTAL_ID, 2],
|
||||
[types.ADD_PORTAL_ID, 'campaign'],
|
||||
[types.SET_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.create(
|
||||
{ commit, dispatch, state: { portals: { selectedPortalId: null } } },
|
||||
{}
|
||||
)
|
||||
actions.create({ commit, dispatch, state: { portals: {} } }, {})
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_UI_FLAG, { isCreating: true }],
|
||||
|
@ -75,32 +69,32 @@ describe('#actions', () => {
|
|||
describe('#update', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: apiResponse.payload[1] });
|
||||
await actions.update({ commit }, apiResponse.payload[1]);
|
||||
await actions.update({ commit }, { portalObj: apiResponse.payload[1] });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isUpdating: true }, portalId: 2 },
|
||||
{ uiFlags: { isUpdating: true }, portalSlug: 'campaign' },
|
||||
],
|
||||
[types.UPDATE_PORTAL_ENTRY, apiResponse.payload[1]],
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isUpdating: false }, portalId: 2 },
|
||||
{ uiFlags: { isUpdating: false }, portalSlug: 'campaign' },
|
||||
],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.update({ commit }, apiResponse.payload[1])
|
||||
actions.update({ commit }, { portalObj: apiResponse.payload[1] })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isUpdating: true }, portalId: 2 },
|
||||
{ uiFlags: { isUpdating: true }, portalSlug: 'campaign' },
|
||||
],
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isUpdating: false }, portalId: 2 },
|
||||
{ uiFlags: { isUpdating: false }, portalSlug: 'campaign' },
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
@ -109,40 +103,35 @@ describe('#actions', () => {
|
|||
describe('#delete', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({});
|
||||
await actions.delete({ commit }, 2);
|
||||
await actions.delete({ commit }, { portalSlug: 'campaign' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isDeleting: true }, portalId: 2 },
|
||||
{ uiFlags: { isDeleting: true }, portalSlug: 'campaign' },
|
||||
],
|
||||
[types.REMOVE_PORTAL_ENTRY, 2],
|
||||
[types.REMOVE_PORTAL_ID, 2],
|
||||
[types.REMOVE_PORTAL_ENTRY, 'campaign'],
|
||||
[types.REMOVE_PORTAL_ID, 'campaign'],
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isDeleting: false }, portalId: 2 },
|
||||
{ uiFlags: { isDeleting: false }, portalSlug: 'campaign' },
|
||||
],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.delete({ commit }, 2)).rejects.toThrow(Error);
|
||||
await expect(
|
||||
actions.delete({ commit }, { portalSlug: 'campaign' })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isDeleting: true }, portalId: 2 },
|
||||
{ uiFlags: { isDeleting: true }, portalSlug: 'campaign' },
|
||||
],
|
||||
[
|
||||
types.SET_HELP_PORTAL_UI_FLAG,
|
||||
{ uiFlags: { isDeleting: false }, portalId: 2 },
|
||||
{ uiFlags: { isDeleting: false }, portalSlug: 'campaign' },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#setPortalId', () => {
|
||||
it('sends correct actions', async () => {
|
||||
axios.delete.mockResolvedValue({});
|
||||
await actions.setPortalId({ commit }, 1);
|
||||
expect(commit.mock.calls).toEqual([[types.SET_SELECTED_PORTAL_ID, 1]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,7 +40,6 @@ export default {
|
|||
1: { isFetching: false, isUpdating: true, isDeleting: false },
|
||||
},
|
||||
},
|
||||
selectedPortalId: 1,
|
||||
},
|
||||
uiFlags: {
|
||||
allFetched: false,
|
||||
|
|
|
@ -16,9 +16,9 @@ describe('#getters', () => {
|
|||
expect(getters.isFetchingPortals(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it('portalById', () => {
|
||||
it('portalBySlug', () => {
|
||||
const state = portal;
|
||||
expect(getters.portalById(state)(1)).toEqual({
|
||||
expect(getters.portalBySlug(state)(1)).toEqual({
|
||||
id: 1,
|
||||
color: 'red',
|
||||
custom_domain: 'domain_for_help',
|
||||
|
|
|
@ -31,13 +31,13 @@ describe('#mutations', () => {
|
|||
expect(state).toEqual(portal);
|
||||
});
|
||||
it('does adds helpcenter object to state', () => {
|
||||
mutations[types.ADD_PORTAL_ENTRY](state, { id: 3 });
|
||||
expect(state.portals.byId[3]).toEqual({ id: 3 });
|
||||
mutations[types.ADD_PORTAL_ENTRY](state, { slug: 'new' });
|
||||
expect(state.portals.byId.new).toEqual({ slug: 'new' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('[types.ADD_PORTAL_ID]', () => {
|
||||
it('adds helpcenter id to state', () => {
|
||||
it('adds helpcenter slug to state', () => {
|
||||
mutations[types.ADD_PORTAL_ID](state, 12);
|
||||
expect(state.portals.allIds).toEqual([1, 2, 12]);
|
||||
});
|
||||
|
@ -48,13 +48,13 @@ describe('#mutations', () => {
|
|||
mutations[types.UPDATE_PORTAL_ENTRY](state, {});
|
||||
expect(state).toEqual(portal);
|
||||
});
|
||||
it('does not updates if object id is not present ', () => {
|
||||
mutations[types.UPDATE_PORTAL_ENTRY](state, { id: 5 });
|
||||
it('does not updates if object slug is not present ', () => {
|
||||
mutations[types.UPDATE_PORTAL_ENTRY](state, { slug: 5 });
|
||||
expect(state).toEqual(portal);
|
||||
});
|
||||
it(' updates if object with id already present in the state', () => {
|
||||
it(' updates if object with slug already present in the state', () => {
|
||||
mutations[types.UPDATE_PORTAL_ENTRY](state, {
|
||||
id: 2,
|
||||
slug: 2,
|
||||
name: 'Updated name',
|
||||
});
|
||||
expect(state.portals.byId[2].name).toEqual('Updated name');
|
||||
|
@ -62,7 +62,7 @@ describe('#mutations', () => {
|
|||
});
|
||||
|
||||
describe('[types.REMOVE_PORTAL_ENTRY]', () => {
|
||||
it('does not remove object entry if no id is passed', () => {
|
||||
it('does not remove object entry if no slug is passed', () => {
|
||||
mutations[types.REMOVE_PORTAL_ENTRY](state, undefined);
|
||||
expect(state).toEqual({ ...portal });
|
||||
});
|
||||
|
@ -73,7 +73,7 @@ describe('#mutations', () => {
|
|||
});
|
||||
|
||||
describe('[types.REMOVE_PORTAL_ID]', () => {
|
||||
it('removes id from state', () => {
|
||||
it('removes slug from state', () => {
|
||||
mutations[types.REMOVE_PORTAL_ID](state, 2);
|
||||
expect(state.portals.allIds).toEqual([1, 12]);
|
||||
});
|
||||
|
@ -82,12 +82,12 @@ describe('#mutations', () => {
|
|||
describe('[types.SET_HELP_PORTAL_UI_FLAG]', () => {
|
||||
it('sets correct flag in state', () => {
|
||||
mutations[types.SET_HELP_PORTAL_UI_FLAG](state, {
|
||||
portalId: 1,
|
||||
portalSlug: 'domain',
|
||||
uiFlags: { isFetching: true },
|
||||
});
|
||||
expect(state.portals.uiFlags.byId[1]).toEqual({
|
||||
expect(state.portals.uiFlags.byId.domain).toEqual({
|
||||
isFetching: true,
|
||||
isUpdating: true,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
});
|
||||
});
|
||||
|
@ -99,9 +99,7 @@ describe('#mutations', () => {
|
|||
expect(state.portals.allIds).toEqual([]);
|
||||
expect(state.portals.byId).toEqual({});
|
||||
expect(state.portals.uiFlags).toEqual({
|
||||
byId: {
|
||||
'1': { isFetching: true, isUpdating: true, isDeleting: false },
|
||||
},
|
||||
byId: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -118,11 +116,4 @@ describe('#mutations', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_SELECTED_PORTAL_ID', () => {
|
||||
it('set selected portal id', () => {
|
||||
mutations[types.SET_SELECTED_PORTAL_ID](state, 4);
|
||||
expect(state.portals.selectedPortalId).toEqual(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div v-if="globalConfig.brandName" class="px-0 py-3 flex justify-center">
|
||||
<div
|
||||
v-if="globalConfig.brandName && !disableBranding"
|
||||
class="px-0 py-3 flex justify-center"
|
||||
>
|
||||
<a
|
||||
:href="brandRedirectURL"
|
||||
rel="noreferrer noopener nofollow"
|
||||
|
@ -30,6 +33,12 @@ const {
|
|||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
disableBranding: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
globalConfig: {
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
"cloud-outline": "M6.087 9.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 0 8H6a4 4 0 0 1 0-8h.087ZM11.75 6.5a4.25 4.25 0 0 0-4.245 4.037.75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h11.5a2.5 2.5 0 0 0 0-5h-.756a.75.75 0 0 1-.75-.713A4.25 4.25 0 0 0 11.75 6.5Z",
|
||||
"code-outline": "m8.066 18.943 6.5-14.5a.75.75 0 0 1 1.404.518l-.036.096-6.5 14.5a.75.75 0 0 1-1.404-.518l.036-.096 6.5-14.5-6.5 14.5ZM2.22 11.47l4.25-4.25a.75.75 0 0 1 1.133.976l-.073.085L3.81 12l3.72 3.719a.75.75 0 0 1-.976 1.133l-.084-.073-4.25-4.25a.75.75 0 0 1-.073-.976l.073-.084 4.25-4.25-4.25 4.25Zm14.25-4.25a.75.75 0 0 1 .976-.073l.084.073 4.25 4.25a.75.75 0 0 1 .073.976l-.073.085-4.25 4.25a.75.75 0 0 1-1.133-.977l.073-.084L20.19 12l-3.72-3.72a.75.75 0 0 1 0-1.06Z",
|
||||
"contact-card-group-outline": "M18.75 4A3.25 3.25 0 0 1 22 7.25v9.505a3.25 3.25 0 0 1-3.25 3.25H5.25A3.25 3.25 0 0 1 2 16.755V7.25a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5Zm0 1.5H5.25l-.144.006A1.75 1.75 0 0 0 3.5 7.25v9.505c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75V7.25a1.75 1.75 0 0 0-1.75-1.75Zm-9.497 7a.75.75 0 0 1 .75.75v.582c0 1.272-.969 1.918-2.502 1.918S5 15.104 5 13.831v-.581a.75.75 0 0 1 .75-.75h3.503Zm1.58-.001 1.417.001a.75.75 0 0 1 .75.75v.333c0 .963-.765 1.417-1.875 1.417-.116 0-.229-.005-.337-.015a2.85 2.85 0 0 0 .206-.9l.009-.253v-.582c0-.269-.061-.524-.17-.751Zm4.417.001h3a.75.75 0 0 1 .102 1.493L18.25 14h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Zm-7.75-4a1.5 1.5 0 1 1 0 3.001 1.5 1.5 0 0 1 0-3.001Zm3.87.502a1.248 1.248 0 1 1 0 2.496 1.248 1.248 0 0 1 0-2.496Zm3.88.498h3a.75.75 0 0 1 .102 1.493L18.25 11h-3a.75.75 0 0 1-.102-1.493l.102-.007h3-3Z",
|
||||
"contact-card-outline": "M19.75 4A2.25 2.25 0 0 1 22 6.25v11.505a2.25 2.25 0 0 1-2.25 2.25H4.25A2.25 2.25 0 0 1 2 17.755V6.25A2.25 2.25 0 0 1 4.25 4h15.5Zm0 1.5H4.25a.75.75 0 0 0-.75.75v11.505c0 .414.336.75.75.75h15.5a.75.75 0 0 0 .75-.75V6.25a.75.75 0 0 0-.75-.75Zm-10 7a.75.75 0 0 1 .75.75v.493l-.008.108c-.163 1.113-1.094 1.65-2.492 1.65s-2.33-.537-2.492-1.65l-.008-.11v-.491a.75.75 0 0 1 .75-.75h3.5Zm3.502.496h4.498a.75.75 0 0 1 .102 1.493l-.102.007h-4.498a.75.75 0 0 1-.102-1.493l.102-.007h4.498-4.498ZM8 8.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm5.252.998h4.498a.75.75 0 0 1 .102 1.493L17.75 11h-4.498a.75.75 0 0 1-.102-1.493l.102-.007h4.498-4.498Z",
|
||||
"contact-identify-outline": "m11.91 13.998 7.843.002a2.25 2.25 0 0 1 2.25 2.25v.905A3.75 3.75 0 0 1 20.696 20C19.13 21.344 16.89 22 14 22h-.179c.234-.47.242-1.025.026-1.502l.153.003c2.56 0 4.458-.557 5.719-1.64a2.25 2.25 0 0 0 .784-1.706v-.905a.75.75 0 0 0-.75-.75h-7.776a5.565 5.565 0 0 0-.068-1.502ZM6.5 10.5a4.5 4.5 0 0 1 3.46 7.376l2.823 2.814a.75.75 0 0 1-.975 1.135l-.085-.073-2.903-2.896A4.5 4.5 0 1 1 6.5 10.5Zm0 1.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM14 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",
|
||||
"copy-outline": [
|
||||
"M8 3a1 1 0 0 0-1 1v.5a.5.5 0 0 1-1 0V4a2 2 0 0 1 2-2h.5a.5.5 0 0 1 0 1H8z",
|
||||
"M7 12a1 1 0 0 0 1 1h.5a.5.5 0 0 1 0 1H8a2 2 0 0 1-2-2v-.5a.5.5 0 0 1 1 0v.5z",
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
>
|
||||
<router-view />
|
||||
</transition>
|
||||
<branding />
|
||||
<branding :disable-branding="disableBranding" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -70,6 +70,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
showPopoutButton: false,
|
||||
disableBranding: window.chatwootWebChannel.disableBranding || false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -67,7 +67,7 @@ class Portal < ApplicationRecord
|
|||
private
|
||||
|
||||
def config_json_format
|
||||
config['default_locale'] = 'en'
|
||||
config['default_locale'] = default_locale
|
||||
denied_keys = config.keys - CONFIG_JSON_KEYS
|
||||
errors.add(:cofig, "in portal on #{denied_keys.join(',')} is not supported.") if denied_keys.any?
|
||||
end
|
||||
|
|
14
app/views/super_admin/accounts/_seed_data.html.erb
Normal file
14
app/views/super_admin/accounts/_seed_data.html.erb
Normal file
|
@ -0,0 +1,14 @@
|
|||
<% if !Rails.env.production? || ENV.fetch('ENABLE_ACCOUNT_SEEDING', nil) %>
|
||||
<section class="main-content__body">
|
||||
<hr/>
|
||||
<%= form_for([:seed, namespace, page.resource], method: :post, html: { class: "form" }) do |f| %>
|
||||
|
||||
<div class="form-actions">
|
||||
<div><p> Click the button to generate seed data into this account for demos.</p>
|
||||
<p class="text-color-red">Note: This will clear all the existing data in this account.</p>
|
||||
</div>
|
||||
<%= f.submit 'Generate Seed Data' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
|
@ -85,3 +85,5 @@ as well as a link to its edit page.
|
|||
<% end %>
|
||||
|
||||
</section>
|
||||
|
||||
<%= render partial: "seed_data", locals: {page: page} %>
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
workingHours: <%= @web_widget.inbox.working_hours.to_json.html_safe %>,
|
||||
outOfOfficeMessage: <%= @web_widget.inbox.out_of_office_message.to_json.html_safe %>,
|
||||
utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].now.formatted_offset %>',
|
||||
allowMessagesAfterResolved: <%= @web_widget.inbox.allow_messages_after_resolved %>
|
||||
allowMessagesAfterResolved: <%= @web_widget.inbox.allow_messages_after_resolved %>,
|
||||
disableBranding: <%= @web_widget.inbox.account.feature_enabled?('disable_branding') %>
|
||||
}
|
||||
window.chatwootWidgetDefaults = {
|
||||
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,
|
||||
|
|
|
@ -8,4 +8,6 @@
|
|||
- name: channel_twitter
|
||||
enabled: true
|
||||
- name: ip_lookup
|
||||
enabled: false
|
||||
enabled: false
|
||||
- name: disable_branding
|
||||
enabled: false
|
||||
|
|
|
@ -340,7 +340,9 @@ Rails.application.routes.draw do
|
|||
resource :app_config, only: [:show, :create]
|
||||
|
||||
# order of resources affect the order of sidebar navigation in super admin
|
||||
resources :accounts
|
||||
resources :accounts, only: [:index, :new, :create, :show, :edit, :update] do
|
||||
post :seed, on: :member
|
||||
end
|
||||
resources :users, only: [:index, :new, :create, :show, :edit, :update]
|
||||
resources :access_tokens, only: [:index, :show]
|
||||
resources :installation_configs, only: [:index, :new, :create, :show, :edit, :update]
|
||||
|
|
12
db/seeds.rb
12
db/seeds.rb
|
@ -11,6 +11,12 @@ end
|
|||
## Seeds for Local Development
|
||||
unless Rails.env.production?
|
||||
|
||||
# Enables creating additional accounts from dashboard
|
||||
installation_config = InstallationConfig.find_by(name: 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD')
|
||||
installation_config.value = true
|
||||
installation_config.save!
|
||||
GlobalConfig.clear_cache
|
||||
|
||||
account = Account.create!(
|
||||
name: 'Acme Inc'
|
||||
)
|
||||
|
@ -35,12 +41,6 @@ unless Rails.env.production?
|
|||
role: :administrator
|
||||
)
|
||||
|
||||
# Enables creating additional accounts from dashboard
|
||||
installation_config = InstallationConfig.find_by(name: 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD')
|
||||
installation_config.value = true
|
||||
installation_config.save!
|
||||
GlobalConfig.clear_cache
|
||||
|
||||
web_widget = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc')
|
||||
|
||||
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
|
||||
|
|
|
@ -1,89 +1,105 @@
|
|||
## Class to generate sample data for a chatwoot test Account.
|
||||
## Class to generate sample data for a chatwoot test @Account.
|
||||
############################################################
|
||||
### Usage #####
|
||||
#
|
||||
# # Seed an account with all data types in this class
|
||||
# Seeders::AccountSeeder.new(account: account).seed!
|
||||
# Seeders::AccountSeeder.new(account: @Account.find(1)).perform!
|
||||
#
|
||||
# # When you want to seed only a specific type of data
|
||||
# Seeders::AccountSeeder.new(account: account).seed_canned_responses
|
||||
# # Seed specific number of objects
|
||||
# Seeders::AccountSeeder.new(account: account).seed_canned_responses(count: 10)
|
||||
#
|
||||
############################################################
|
||||
|
||||
class Seeders::AccountSeeder
|
||||
pattr_initialize [:account!]
|
||||
def initialize(account:)
|
||||
raise 'Account Seeding is not allowed in production.' if Rails.env.production?
|
||||
|
||||
def seed!
|
||||
@account_data = HashWithIndifferentAccess.new(YAML.safe_load(File.read(Rails.root.join('lib/seeders/seed_data.yml'))))
|
||||
@account = account
|
||||
end
|
||||
|
||||
def perform!
|
||||
set_up_account
|
||||
seed_teams
|
||||
set_up_users
|
||||
seed_labels
|
||||
seed_canned_responses
|
||||
seed_inboxes
|
||||
seed_contacts
|
||||
end
|
||||
|
||||
def set_up_account
|
||||
@account.teams.destroy_all
|
||||
@account.conversations.destroy_all
|
||||
@account.labels.destroy_all
|
||||
@account.inboxes.destroy_all
|
||||
@account.contacts.destroy_all
|
||||
end
|
||||
|
||||
def seed_teams
|
||||
@account_data['teams'].each do |team_name|
|
||||
@account.teams.create!(name: team_name)
|
||||
end
|
||||
end
|
||||
|
||||
def seed_labels
|
||||
@account_data['labels'].each do |label|
|
||||
@account.labels.create!(label)
|
||||
end
|
||||
end
|
||||
|
||||
def set_up_users
|
||||
@account_data['users'].each do |user|
|
||||
user_record = User.create_with(name: user['name'], password: 'Password1!.').find_or_create_by!(email: (user['email']).to_s)
|
||||
user_record.skip_confirmation!
|
||||
user_record.save!
|
||||
Avatar::AvatarFromUrlJob.perform_later(user_record, "https://xsgames.co/randomusers/avatar.php?g=#{user['gender']}")
|
||||
AccountUser.create_with(role: (user['role'] || 'agent')).find_or_create_by!(account_id: @account.id, user_id: user_record.id)
|
||||
next if user['team'].blank?
|
||||
|
||||
add_user_to_teams(user: user_record, teams: user['team'])
|
||||
end
|
||||
end
|
||||
|
||||
def add_user_to_teams(user:, teams:)
|
||||
teams.each do |team|
|
||||
team_record = @account.teams.where('name LIKE ?', "%#{team.downcase}%").first if team.present?
|
||||
TeamMember.find_or_create_by!(team_id: team_record.id, user_id: user.id) unless team_record.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def seed_canned_responses(count: 50)
|
||||
count.times do
|
||||
account.canned_responses.create(content: Faker::Quote.fortune_cookie, short_code: Faker::Alphanumeric.alpha(number: 10))
|
||||
@account.canned_responses.create(content: Faker::Quote.fortune_cookie, short_code: Faker::Alphanumeric.alpha(number: 10))
|
||||
end
|
||||
end
|
||||
|
||||
def seed_contacts
|
||||
@account_data['contacts'].each do |contact_data|
|
||||
contact = @account.contacts.create!(contact_data.slice('name', 'email'))
|
||||
Avatar::AvatarFromUrlJob.perform_later(contact, "https://xsgames.co/randomusers/avatar.php?g=#{contact_data['gender']}")
|
||||
contact_data['conversations'].each do |conversation_data|
|
||||
inbox = @account.inboxes.find_by(channel_type: conversation_data['channel'])
|
||||
contact_inbox = inbox.contact_inboxes.create!(contact: contact, source_id: (conversation_data['source_id'] || SecureRandom.hex))
|
||||
create_conversation(contact_inbox: contact_inbox, conversation_data: conversation_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_conversation(contact_inbox:, conversation_data:)
|
||||
assignee = User.find_by(email: conversation_data['assignee']) if conversation_data['assignee'].present?
|
||||
conversation = contact_inbox.conversations.create!(account: contact_inbox.inbox.account, contact: contact_inbox.contact,
|
||||
inbox: contact_inbox.inbox, assignee: assignee)
|
||||
create_messages(conversation: conversation, messages: conversation_data['messages'])
|
||||
end
|
||||
|
||||
def create_messages(conversation:, messages:)
|
||||
messages.each do |message_data|
|
||||
sender = User.find_by(email: message_data['sender']) if message_data['sender'].present?
|
||||
conversation.messages.create!(message_data.slice('content', 'message_type').merge(account: conversation.inbox.account, sender: sender,
|
||||
inbox: conversation.inbox))
|
||||
end
|
||||
end
|
||||
|
||||
def seed_inboxes
|
||||
seed_website_inbox
|
||||
seed_facebook_inbox
|
||||
seed_twitter_inbox
|
||||
seed_whatsapp_inbox
|
||||
seed_sms_inbox
|
||||
seed_email_inbox
|
||||
seed_api_inbox
|
||||
seed_telegram_inbox
|
||||
seed_line_inbox
|
||||
end
|
||||
|
||||
def seed_website_inbox
|
||||
channel = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Website')
|
||||
end
|
||||
|
||||
def seed_facebook_inbox
|
||||
channel = Channel::FacebookPage.create!(account: account, user_access_token: 'test', page_access_token: 'test', page_id: 'test')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Facebook')
|
||||
end
|
||||
|
||||
def seed_twitter_inbox
|
||||
channel = Channel::TwitterProfile.create!(account: account, twitter_access_token: 'test', twitter_access_token_secret: 'test', profile_id: '123')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Twitter')
|
||||
end
|
||||
|
||||
def seed_whatsapp_inbox
|
||||
channel = Channel::Whatsapp.create!(account: account, phone_number: '+123456789')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Whatsapp')
|
||||
end
|
||||
|
||||
def seed_sms_inbox
|
||||
channel = Channel::Sms.create!(account: account, phone_number: '+123456789')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme SMS')
|
||||
end
|
||||
|
||||
def seed_email_inbox
|
||||
channel = Channel::Email.create!(account: account, email: 'test@acme.inc', forward_to_email: 'test_fwd@acme.inc')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Email')
|
||||
end
|
||||
|
||||
def seed_api_inbox
|
||||
channel = Channel::Api.create!(account: account)
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme API')
|
||||
end
|
||||
|
||||
def seed_telegram_inbox
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
Channel::Telegram.insert({ account_id: account.id, bot_name: 'Acme', bot_token: 'test', created_at: Time.now.utc, updated_at: Time.now.utc },
|
||||
returning: %w[id])
|
||||
channel = Channel::Telegram.find_by(bot_token: 'test')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Telegram')
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def seed_line_inbox
|
||||
channel = Channel::Line.create!(account: account, line_channel_id: 'test', line_channel_secret: 'test', line_channel_token: 'test')
|
||||
Inbox.create!(channel: channel, account: account, name: 'Acme Line')
|
||||
Seeders::InboxSeeder.new(account: @account, company_data: @account_data[:company]).perform!
|
||||
end
|
||||
end
|
||||
|
|
105
lib/seeders/inbox_seeder.rb
Normal file
105
lib/seeders/inbox_seeder.rb
Normal file
|
@ -0,0 +1,105 @@
|
|||
## Class to generate sample inboxes for a chatwoot test @Account.
|
||||
############################################################
|
||||
### Usage #####
|
||||
#
|
||||
# # Seed an account with all data types in this class
|
||||
# Seeders::InboxSeeder.new(account: @Account.find(1), company_data: {name: 'PaperLayer', doamin: 'paperlayer.test'}).perform!
|
||||
#
|
||||
#
|
||||
############################################################
|
||||
|
||||
class Seeders::InboxSeeder
|
||||
def initialize(account:, company_data:)
|
||||
raise 'Inbox Seeding is not allowed in production.' if Rails.env.production?
|
||||
|
||||
@account = account
|
||||
@company_data = company_data
|
||||
end
|
||||
|
||||
def perform!
|
||||
seed_website_inbox
|
||||
seed_facebook_inbox
|
||||
seed_twitter_inbox
|
||||
seed_whatsapp_inbox
|
||||
seed_sms_inbox
|
||||
seed_email_inbox
|
||||
seed_api_inbox
|
||||
seed_telegram_inbox
|
||||
seed_line_inbox
|
||||
end
|
||||
|
||||
def seed_website_inbox
|
||||
channel = Channel::WebWidget.create!(account: @account, website_url: "https://#{@company_data['domain']}")
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Website")
|
||||
end
|
||||
|
||||
def seed_facebook_inbox
|
||||
channel = Channel::FacebookPage.create!(account: @account, user_access_token: SecureRandom.hex, page_access_token: SecureRandom.hex,
|
||||
page_id: SecureRandom.hex)
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Facebook")
|
||||
end
|
||||
|
||||
def seed_twitter_inbox
|
||||
channel = Channel::TwitterProfile.create!(account: @account, twitter_access_token: SecureRandom.hex,
|
||||
twitter_access_token_secret: SecureRandom.hex, profile_id: '123')
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Twitter")
|
||||
end
|
||||
|
||||
def seed_whatsapp_inbox
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
Channel::Whatsapp.insert(
|
||||
{
|
||||
account_id: @account.id,
|
||||
phone_number: Faker::PhoneNumber.cell_phone_in_e164,
|
||||
created_at: Time.now.utc,
|
||||
updated_at: Time.now.utc
|
||||
},
|
||||
returning: %w[id]
|
||||
)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
channel = Channel::Whatsapp.find_by(account_id: @account.id)
|
||||
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Whatsapp")
|
||||
end
|
||||
|
||||
def seed_sms_inbox
|
||||
channel = Channel::Sms.create!(account: @account, phone_number: Faker::PhoneNumber.cell_phone_in_e164)
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Mobile")
|
||||
end
|
||||
|
||||
def seed_email_inbox
|
||||
channel = Channel::Email.create!(account: @account, email: "test#{SecureRandom.hex}@#{@company_data['domain']}",
|
||||
forward_to_email: "test_fwd#{SecureRandom.hex}@#{@company_data['domain']}")
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Email")
|
||||
end
|
||||
|
||||
def seed_api_inbox
|
||||
channel = Channel::Api.create!(account: @account)
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} API")
|
||||
end
|
||||
|
||||
def seed_telegram_inbox
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
bot_token = SecureRandom.hex
|
||||
Channel::Telegram.insert(
|
||||
{
|
||||
account_id: @account.id,
|
||||
bot_name: (@company_data['name']).to_s,
|
||||
bot_token: bot_token,
|
||||
created_at: Time.now.utc,
|
||||
updated_at: Time.now.utc
|
||||
},
|
||||
returning: %w[id]
|
||||
)
|
||||
channel = Channel::Telegram.find_by(bot_token: bot_token)
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Telegram")
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def seed_line_inbox
|
||||
channel = Channel::Line.create!(account: @account, line_channel_id: SecureRandom.hex, line_channel_secret: SecureRandom.hex,
|
||||
line_channel_token: SecureRandom.hex)
|
||||
Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Line")
|
||||
end
|
||||
end
|
367
lib/seeders/seed_data.yml
Normal file
367
lib/seeders/seed_data.yml
Normal file
|
@ -0,0 +1,367 @@
|
|||
company:
|
||||
name: 'PaperLayer'
|
||||
domain: 'paperlayer.test'
|
||||
users:
|
||||
- name: 'Michael Scott'
|
||||
gender: male
|
||||
email: 'michale@paperlayer.test'
|
||||
team:
|
||||
- 'sales'
|
||||
- 'management'
|
||||
- 'administration'
|
||||
- 'warehouse'
|
||||
role: 'administrator'
|
||||
- name: 'David Wallace'
|
||||
gender: male
|
||||
email: 'david@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Deangelo Vickers'
|
||||
gender: male
|
||||
email: 'deangelo@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Jo Bennett'
|
||||
gender: female
|
||||
email: 'jo@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Josh Porter'
|
||||
gender: male
|
||||
email: 'josh@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Charles Miner'
|
||||
gender: male
|
||||
email: 'charles@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Ed Truck'
|
||||
gender: male
|
||||
email: 'ed@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Dan Gore'
|
||||
gender: male
|
||||
email: 'dan@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Craig D'
|
||||
gender: male
|
||||
email: 'craig@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Troy Underbridge'
|
||||
gender: male
|
||||
email: 'troy@paperlayer.test'
|
||||
team:
|
||||
- 'Management'
|
||||
- name: 'Karen Filippelli'
|
||||
gender: female
|
||||
email: 'karn@paperlayer.test'
|
||||
team:
|
||||
- 'Sales'
|
||||
- name: 'Danny Cordray'
|
||||
gender: female
|
||||
email: 'danny@paperlayer.test'
|
||||
team:
|
||||
- 'Sales'
|
||||
- name: 'Ben Nugent'
|
||||
gender: male
|
||||
email: 'ben@paperlayer.test'
|
||||
team:
|
||||
- 'Sales'
|
||||
- name: 'Todd Packer'
|
||||
gender: male
|
||||
email: 'todd@paperlayer.test'
|
||||
team:
|
||||
- 'Sales'
|
||||
- name: 'Cathy Simms'
|
||||
gender: female
|
||||
email: 'cathy@paperlayer.test'
|
||||
team:
|
||||
- 'Administration'
|
||||
- name: 'Hunter Jo'
|
||||
gender: male
|
||||
email: 'hunter@paperlayer.test'
|
||||
team:
|
||||
- 'Administration'
|
||||
- name: 'Rolando Silva'
|
||||
gender: male
|
||||
email: 'rolando@paperlayer.test'
|
||||
team:
|
||||
- 'Administration'
|
||||
- name: 'Stephanie Wilson'
|
||||
gender: female
|
||||
email: 'stephanie@paperlayer.test'
|
||||
team:
|
||||
- 'Administration'
|
||||
- name: 'Jordan Garfield'
|
||||
gender: male
|
||||
email: 'jorodan@paperlayer.test'
|
||||
team:
|
||||
- 'Administration'
|
||||
- name: 'Ronni Carlo'
|
||||
gender: male
|
||||
email: 'ronni@paperlayer.test'
|
||||
team:
|
||||
- 'Administration'
|
||||
- name: 'Lonny Collins'
|
||||
gender: female
|
||||
email: 'lonny@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Madge Madsen'
|
||||
gender: female
|
||||
email: 'madge@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Glenn Max'
|
||||
gender: female
|
||||
email: 'glenn@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Jerry DiCanio'
|
||||
gender: male
|
||||
email: 'jerry@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Phillip Martin'
|
||||
gender: male
|
||||
email: 'phillip@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Michael Josh'
|
||||
gender: male
|
||||
email: 'michale_josh@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Matt Hudson'
|
||||
gender: male
|
||||
email: 'matt@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Gideon'
|
||||
gender: male
|
||||
email: 'gideon@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Bruce'
|
||||
gender: male
|
||||
email: 'bruce@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Frank'
|
||||
gender: male
|
||||
email: 'frank@paperlayer.test'
|
||||
team:
|
||||
- 'Warehouse'
|
||||
- name: 'Louanne Kelley'
|
||||
gender: female
|
||||
email: 'louanne@paperlayer.test'
|
||||
- name: 'Devon White'
|
||||
gender: male
|
||||
email: 'devon@paperlayer.test'
|
||||
- name: 'Kendall'
|
||||
gender: male
|
||||
email: 'kendall@paperlayer.test'
|
||||
- email: 'sadiq@paperlayer.test'
|
||||
name: 'Sadiq'
|
||||
gender: male
|
||||
teams:
|
||||
- '💰 Sales'
|
||||
- '💼 Management'
|
||||
- '👩💼 Administration'
|
||||
- '🚛 Warehouse'
|
||||
labels:
|
||||
- title: 'billing'
|
||||
color: '#28AD21'
|
||||
show_on_sidebar: true
|
||||
- title: 'software'
|
||||
color: '#8F6EF2'
|
||||
show_on_sidebar: true
|
||||
- title: 'delivery'
|
||||
color: '#A2FDD5'
|
||||
show_on_sidebar: true
|
||||
- title: 'ops-handover'
|
||||
color: '#A53326'
|
||||
show_on_sidebar: true
|
||||
- title: 'premium-customer'
|
||||
color: '#6FD4EF'
|
||||
show_on_sidebar: true
|
||||
- title: 'lead'
|
||||
color: '#F161C8'
|
||||
show_on_sidebar: true
|
||||
contacts:
|
||||
- name: "Lorrie Trosdall"
|
||||
email: "ltrosdall0@bravesites.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Tiffanie Cloughton"
|
||||
email: "tcloughton1@newyorker.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::FacebookPage
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Melonie Keatch"
|
||||
email: "mkeatch2@reuters.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::TwitterProfile
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Olin Canniffe"
|
||||
email: "ocanniffe3@feedburner.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Whatsapp
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Viviene Corp"
|
||||
email: "vcorp4@instagram.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::Sms
|
||||
source_id: "+1234567"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Drake Pittway"
|
||||
email: "dpittway5@chron.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Line
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Klaus Crawley"
|
||||
email: "kcrawley6@narod.ru"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Bing Cusworth"
|
||||
email: "bcusworth7@arstechnica.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::TwitterProfile
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Claus Jira"
|
||||
email: "cjira8@comcast.net"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Whatsapp
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Quent Dalliston"
|
||||
email: "qdalliston9@zimbio.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Whatsapp
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Coreen Mewett"
|
||||
email: "cmewetta@home.pl"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::FacebookPage
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Benyamin Janeway"
|
||||
email: "bjanewayb@ustream.tv"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Line
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Cordell Dalinder"
|
||||
email: "cdalinderc@msn.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::Email
|
||||
source_id: "cdalinderc@msn.test"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Merrile Petruk"
|
||||
email: "mpetrukd@wunderground.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::Email
|
||||
source_id: "mpetrukd@wunderground.test"
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: hello world
|
||||
- name: "Nathaniel Vannuchi"
|
||||
email: "nvannuchie@photobucket.test"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::FacebookPage
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: "Hey there,I need some help with billing, my card is not working on the website."
|
||||
- name: "Olia Olenchenko"
|
||||
email: "oolenchenkof@bluehost.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
assignee: michael_scott@paperlayer.test
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: "Billing section is not working, it throws some error."
|
||||
- name: "Elisabeth Derington"
|
||||
email: "ederingtong@printfriendly.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::Whatsapp
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: "Hey \n I didn't get the product delivered, but it shows it is delivered to my address. Please check"
|
||||
- name: "Willy Castelot"
|
||||
email: "wcasteloth@exblog.jp"
|
||||
gender: 'male'
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: "Hey there, \n I need some help with the product, my button is not working on the website."
|
||||
- name: "Ophelia Folkard"
|
||||
email: "ofolkardi@taobao.test"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::WebWidget
|
||||
assignee: michael_scott@paperlayer.test
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: "Hey, \n My card is not working on your website. Please help"
|
||||
- name: "Candice Matherson"
|
||||
email: "cmathersonj@va.gov"
|
||||
gender: 'female'
|
||||
conversations:
|
||||
- channel: Channel::Email
|
||||
source_id: "cmathersonj@va.gov"
|
||||
assignee: michael_scott@paperlayer.test
|
||||
messages:
|
||||
- message_type: incoming
|
||||
content: "Hey, \n I'm looking for some help to figure out if it is the right product for me."
|
||||
- message_type: outgoing
|
||||
content: Welcome to PaperLayer. Our Team will be getting back you shortly.
|
||||
- message_type: outgoing
|
||||
content: How may i help you ?
|
||||
sender: michael_scott@paperlayer.test
|
BIN
public/assets/administrate/bot/avatar.png
Normal file
BIN
public/assets/administrate/bot/avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
public/assets/administrate/user/avatar.png
Normal file
BIN
public/assets/administrate/user/avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
Loading…
Reference in a new issue