Merge branch 'develop' into chore/email-notifications

This commit is contained in:
Sojan 2022-09-01 17:20:04 +05:30
commit f29fd3f80d
59 changed files with 1025 additions and 292 deletions

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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;

View file

@ -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'
);
});
});
});

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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",

View file

@ -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"
},

View file

@ -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: {

View file

@ -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')}`;

View file

@ -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) {

View file

@ -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')"

View file

@ -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) {

View file

@ -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>

View file

@ -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,
},
});
},
},

View file

@ -127,7 +127,7 @@ export default {
props: {
article: {
type: Object,
required: true,
default: () => ({}),
},
},
data() {

View file

@ -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;
},

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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 ||

View file

@ -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 ||

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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;

View file

@ -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 => {

View file

@ -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([
[

View file

@ -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);
},

View file

@ -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);
},
};

View file

@ -25,7 +25,6 @@ const state = {
meta: {
byId: {},
},
selectedPortalId: null,
},
uiFlags: {
allFetched: false,

View file

@ -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,

View file

@ -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]]);
});
});
});

View file

@ -40,7 +40,6 @@ export default {
1: { isFetching: false, isUpdating: true, isDeleting: false },
},
},
selectedPortalId: 1,
},
uiFlags: {
allFetched: false,

View file

@ -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',

View file

@ -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);
});
});
});

View file

@ -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: {

View file

@ -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",

View file

@ -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: {

View file

@ -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

View 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 %>

View file

@ -85,3 +85,5 @@ as well as a link to its edit page.
<% end %>
</section>
<%= render partial: "seed_data", locals: {page: page} %>

View file

@ -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)) %>,

View file

@ -8,4 +8,6 @@
- name: channel_twitter
enabled: true
- name: ip_lookup
enabled: false
enabled: false
- name: disable_branding
enabled: false

View file

@ -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]

View file

@ -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')

View file

@ -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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB