Merge branch 'develop' into feat/add-locale
This commit is contained in:
commit
91bfc60e8f
36 changed files with 837 additions and 238 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'
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -242,6 +242,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"
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
@ -195,19 +204,27 @@ export default {
|
|||
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) {
|
||||
|
|
|
@ -194,7 +194,7 @@ export default {
|
|||
this.$emit('open-site');
|
||||
},
|
||||
openSettings() {
|
||||
this.$emit('open');
|
||||
this.navigateToPortalEdit();
|
||||
},
|
||||
swapLocale() {
|
||||
this.$emit('swap');
|
||||
|
@ -202,6 +202,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;
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
}),
|
||||
createdPortalSlug() {
|
||||
const {
|
||||
params: { portal_slug: slug },
|
||||
params: { portalSlug: slug },
|
||||
} = this.$route;
|
||||
return slug;
|
||||
},
|
||||
|
|
|
@ -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 => {
|
||||
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,46 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
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? %>
|
||||
<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} %>
|
||||
|
|
|
@ -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