Merge branch 'feat/custom-attrs-automations' of github.com:chatwoot/chatwoot into feat/custom-attrs-automations

This commit is contained in:
Fayaz Ahmed 2022-05-19 11:02:00 +05:30
commit 8cb54af92a
66 changed files with 420 additions and 182 deletions

View file

@ -1,4 +1,5 @@
class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase::BaseController class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
before_action :portal
before_action :fetch_category, except: [:index, :create] before_action :fetch_category, except: [:index, :create]
def index def index
@ -24,6 +25,10 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
@category = @portal.categories.find(params[:id]) @category = @portal.categories.find(params[:id])
end end
def portal
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
end
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position :name, :description, :position

View file

@ -5,7 +5,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
RESULTS_PER_PAGE = 25 RESULTS_PER_PAGE = 25
before_action :check_authorization before_action :check_authorization
before_action :set_csat_survey_responses, only: [:index, :metrics] before_action :set_csat_survey_responses, only: [:index, :metrics, :download]
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
before_action :set_current_page_surveys, only: [:index] before_action :set_current_page_surveys, only: [:index]
before_action :set_total_sent_messages_count, only: [:metrics] before_action :set_total_sent_messages_count, only: [:metrics]
@ -19,6 +19,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
@ratings_count = @csat_survey_responses.group(:rating).count @ratings_count = @csat_survey_responses.group(:rating).count
end end
def download
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
end
private private
def set_total_sent_messages_count def set_total_sent_messages_count

View file

@ -1,9 +0,0 @@
class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseController
before_action :portal
private
def portal
@portal ||= Current.account.kbase_portals.find_by(slug: params[:portal_id])
end
end

View file

@ -1,14 +1,14 @@
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
before_action :fetch_portal, except: [:index, :create] before_action :fetch_portal, except: [:index, :create]
def index def index
@portals = Current.account.kbase_portals @portals = Current.account.portals
end end
def show; end def show; end
def create def create
@portal = Current.account.kbase_portals.create!(portal_params) @portal = Current.account.portals.create!(portal_params)
end end
def update def update
@ -23,7 +23,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseContr
private private
def fetch_portal def fetch_portal
@portal = Current.account.kbase_portals.find_by(slug: permitted_params[:id]) @portal = Current.account.portals.find_by(slug: permitted_params[:id])
end end
def permitted_params def permitted_params
@ -32,7 +32,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseContr
def portal_params def portal_params
params.require(:portal).permit( params.require(:portal).permit(
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug :account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived
) )
end end
end end

View file

@ -18,6 +18,17 @@ class CSATReportsAPI extends ApiClient {
}); });
} }
download({ from, to, user_ids } = {}) {
return axios.get(`${this.url}/download`, {
params: {
since: from,
until: to,
sort: '-created_at',
user_ids,
},
});
}
getMetrics({ from, to, user_ids } = {}) { getMetrics({ from, to, user_ids } = {}) {
return axios.get(`${this.url}/metrics`, { return axios.get(`${this.url}/metrics`, {
params: { since: from, until: to, user_ids }, params: { since: from, until: to, user_ids },

View file

@ -33,5 +33,23 @@ describe('#Reports API', () => {
} }
); );
}); });
it('#download', () => {
csatReportsAPI.download({
from: 1622485800,
to: 1623695400,
user_ids: 1,
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/csat_survey_responses/download',
{
params: {
since: 1622485800,
until: 1623695400,
user_ids: 1,
sort: '-created_at',
},
}
);
});
}); });
}); });

View file

@ -1,6 +1,12 @@
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
export const downloadCsvFile = (fileName, fileContent) => { export const downloadCsvFile = (fileName, fileContent) => {
const link = document.createElement('a'); const link = document.createElement('a');
link.download = fileName; link.download = fileName;
link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent); link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent);
link.click(); link.click();
}; };
export const generateFileName = ({ type, to }) =>
`${type}-report-${format(fromUnixTime(to), 'dd-MM-yyyy')}.csv`;

View file

@ -1,4 +1,4 @@
import { downloadCsvFile } from '../downloadCsvFile'; import { downloadCsvFile, generateFileName } from '../downloadHelper';
const fileName = 'test.csv'; const fileName = 'test.csv';
const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes) const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
@ -19,3 +19,11 @@ describe('#downloadCsvFile', () => {
expect(link.click).toHaveBeenCalledTimes(1); expect(link.click).toHaveBeenCalledTimes(1);
}); });
}); });
describe('#generateFileName', () => {
it('should generate the correct file name', () => {
expect(generateFileName({ type: 'csat', to: 1652812199 })).toEqual(
'csat-report-17-05-2022.csv'
);
});
});

View file

@ -398,8 +398,8 @@
"MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك", "MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك",
"INBOX_AGENTS": "موظف الدعم", "INBOX_AGENTS": "موظف الدعم",
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة موظفين من قناة التواصل هذه", "INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة موظفين من قناة التواصل هذه",
"AGENT_ASSIGNMENT": "Conversation Assignment", "AGENT_ASSIGNMENT": "إسناد المحادثات",
"AGENT_ASSIGNMENT_SUB_TEXT": "Update conversation assignment settings", "AGENT_ASSIGNMENT_SUB_TEXT": "تحديث إعدادات إسناد المحادثات",
"UPDATE": "تحديث", "UPDATE": "تحديث",
"ENABLE_EMAIL_COLLECT_BOX": "تفعيل صندوق جمع البريد الإلكتروني", "ENABLE_EMAIL_COLLECT_BOX": "تفعيل صندوق جمع البريد الإلكتروني",
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "تمكين أو تعطيل مربع جمع البريد الإلكتروني في محادثة جديدة", "ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "تمكين أو تعطيل مربع جمع البريد الإلكتروني في محادثة جديدة",

View file

@ -151,7 +151,7 @@
}, },
"SIDEBAR": { "SIDEBAR": {
"CURRENTLY_VIEWING_ACCOUNT": "مشاهدة حاليا:", "CURRENTLY_VIEWING_ACCOUNT": "مشاهدة حاليا:",
"SWITCH": "Switch", "SWITCH": "تبديل",
"CONVERSATIONS": "المحادثات", "CONVERSATIONS": "المحادثات",
"ALL_CONVERSATIONS": "كل المحادثات", "ALL_CONVERSATIONS": "كل المحادثات",
"MENTIONED_CONVERSATIONS": "الإشارات", "MENTIONED_CONVERSATIONS": "الإشارات",

View file

@ -354,6 +354,7 @@
"CSAT_REPORTS": { "CSAT_REPORTS": {
"HEADER": "CSAT Reports", "HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.", "NO_RECORDS": "There are no CSAT survey responses available.",
"DOWNLOAD": "Download CSAT Reports",
"FILTERS": { "FILTERS": {
"AGENTS": { "AGENTS": {
"PLACEHOLDER": "Choose Agents" "PLACEHOLDER": "Choose Agents"

View file

@ -8,7 +8,6 @@
</span> </span>
<contact-info <contact-info
:show-avatar="showAvatar" :show-avatar="showAvatar"
show-new-message
:contact="contact" :contact="contact"
@panel-close="onClose" @panel-close="onClose"
/> />

View file

@ -65,7 +65,6 @@
</div> </div>
<div class="contact-actions"> <div class="contact-actions">
<woot-button <woot-button
v-if="showNewMessage"
v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')" v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')"
title="$t('CONTACT_PANEL.NEW_MESSAGE')" title="$t('CONTACT_PANEL.NEW_MESSAGE')"
class="new-message" class="new-message"
@ -172,10 +171,6 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
showNewMessage: {
type: Boolean,
default: false,
},
showAvatar: { showAvatar: {
type: Boolean, type: Boolean,
default: true, default: true,

View file

@ -52,6 +52,10 @@ export default {
}, },
async onSubmit(contactItem) { async onSubmit(contactItem) {
await this.$store.dispatch('contacts/update', contactItem); await this.$store.dispatch('contacts/update', contactItem);
await this.$store.dispatch(
'contacts/fetchContactableInbox',
this.contact.id
);
}, },
}, },
}; };

View file

@ -3,9 +3,18 @@
<report-filter-selector <report-filter-selector
agents-filter agents-filter
:agents-filter-items-list="agentList" :agents-filter-items-list="agentList"
:show-business-hours-switch="false"
@date-range-change="onDateRangeChange" @date-range-change="onDateRangeChange"
@agents-filter-change="onAgentsFilterChange" @agents-filter-change="onAgentsFilterChange"
/> />
<woot-button
color-scheme="success"
class-names="button--fixed-right-top"
icon="arrow-download"
@click="downloadReports"
>
{{ $t('CSAT_REPORTS.DOWNLOAD') }}
</woot-button>
<csat-metrics /> <csat-metrics />
<csat-table :page-index="pageIndex" @page-change="onPageNumberChange" /> <csat-table :page-index="pageIndex" @page-change="onPageNumberChange" />
</div> </div>
@ -15,6 +24,7 @@ import CsatMetrics from './components/CsatMetrics';
import CsatTable from './components/CsatTable'; import CsatTable from './components/CsatTable';
import ReportFilterSelector from './components/FilterSelector'; import ReportFilterSelector from './components/FilterSelector';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { generateFileName } from '../../../../helper/downloadHelper';
export default { export default {
name: 'CsatResponses', name: 'CsatResponses',
@ -24,7 +34,7 @@ export default {
ReportFilterSelector, ReportFilterSelector,
}, },
data() { data() {
return { pageIndex: 1, from: 0, to: 0, user_ids: [] }; return { pageIndex: 1, from: 0, to: 0, userIds: [] };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -39,7 +49,7 @@ export default {
this.$store.dispatch('csat/getMetrics', { this.$store.dispatch('csat/getMetrics', {
from: this.from, from: this.from,
to: this.to, to: this.to,
user_ids: this.user_ids, user_ids: this.userIds,
}); });
this.getResponses(); this.getResponses();
}, },
@ -48,7 +58,7 @@ export default {
page: this.pageIndex, page: this.pageIndex,
from: this.from, from: this.from,
to: this.to, to: this.to,
user_ids: this.user_ids, user_ids: this.userIds,
}); });
}, },
onPageNumberChange(pageIndex) { onPageNumberChange(pageIndex) {
@ -61,9 +71,18 @@ export default {
this.getAllData(); this.getAllData();
}, },
onAgentsFilterChange(agents) { onAgentsFilterChange(agents) {
this.user_ids = agents.map(el => el.id); this.userIds = agents.map(el => el.id);
this.getAllData(); this.getAllData();
}, },
downloadReports() {
const type = 'csat';
this.$store.dispatch('csat/downloadCSATReports', {
from: this.from,
to: this.to,
user_ids: this.userIds,
fileName: generateFileName({ type, to: this.to }),
});
},
}, },
}; };
</script> </script>

View file

@ -61,7 +61,10 @@
@input="handleAgentsFilterSelection" @input="handleAgentsFilterSelection"
/> />
</div> </div>
<div class="small-12 medium-3 business-hours"> <div
v-if="showBusinessHoursSwitch"
class="small-12 medium-3 business-hours"
>
<span class="business-hours-text margin-right-small"> <span class="business-hours-text margin-right-small">
{{ $t('REPORT.BUSINESS_HOURS') }} {{ $t('REPORT.BUSINESS_HOURS') }}
</span> </span>
@ -105,6 +108,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showBusinessHoursSwitch: {
type: Boolean,
default: true,
},
}, },
data() { data() {
return { return {

View file

@ -61,6 +61,7 @@ import format from 'date-fns/format';
import { GROUP_BY_FILTER, METRIC_CHART } from '../constants'; import { GROUP_BY_FILTER, METRIC_CHART } from '../constants';
import reportMixin from '../../../../../mixins/reportMixin'; import reportMixin from '../../../../../mixins/reportMixin';
import { formatTime } from '@chatwoot/utils'; import { formatTime } from '@chatwoot/utils';
import { generateFileName } from '../../../../../helper/downloadHelper';
const REPORTS_KEYS = { const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count', CONVERSATIONS: 'conversations_count',
@ -250,26 +251,17 @@ export default {
}); });
}, },
downloadReports() { downloadReports() {
const { from, to } = this; const { from, to, type } = this;
const fileName = `${this.type}-report-${format( const dispatchMethods = {
fromUnixTime(to), agent: 'downloadAgentReports',
'dd-MM-yyyy' label: 'downloadLabelReports',
)}.csv`; inbox: 'downloadInboxReports',
switch (this.type) { team: 'downloadTeamReports',
case 'agent': };
this.$store.dispatch('downloadAgentReports', { from, to, fileName }); if (dispatchMethods[type]) {
break; const fileName = generateFileName({ type, to });
case 'label': const params = { from, to, fileName };
this.$store.dispatch('downloadLabelReports', { from, to, fileName }); this.$store.dispatch(dispatchMethods[type], params);
break;
case 'inbox':
this.$store.dispatch('downloadInboxReports', { from, to, fileName });
break;
case 'team':
this.$store.dispatch('downloadTeamReports', { from, to, fileName });
break;
default:
break;
} }
}, },
changeSelection(index) { changeSelection(index) {

View file

@ -1,6 +1,7 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types'; import types from '../mutation-types';
import CSATReports from '../../api/csatReports'; import CSATReports from '../../api/csatReports';
import { downloadCsvFile } from '../../helper/downloadHelper';
const computeDistribution = (value, total) => const computeDistribution = (value, total) =>
((value * 100) / total).toFixed(2); ((value * 100) / total).toFixed(2);
@ -107,6 +108,11 @@ export const actions = {
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false }); commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: false });
} }
}, },
downloadCSATReports(_, params) {
return CSATReports.download(params).then(response => {
downloadCsvFile(params.fileName, response.data);
});
},
}; };
export const mutations = { export const mutations = {

View file

@ -5,7 +5,7 @@ import * as types from '../mutation-types';
import Report from '../../api/reports'; import Report from '../../api/reports';
import Vue from 'vue'; import Vue from 'vue';
import { downloadCsvFile } from '../../helper/downloadCsvFile'; import { downloadCsvFile } from '../../helper/downloadHelper';
const state = { const state = {
fetchingStatus: false, fetchingStatus: false,

View file

@ -39,6 +39,7 @@ import {
} from '../dashboard/helper/scriptHelpers'; } from '../dashboard/helper/scriptHelpers';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import VueDOMPurifyHTML from 'vue-dompurify-html'; import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
Vue.config.env = process.env; Vue.config.env = process.env;
@ -55,7 +56,8 @@ if (window.analyticsConfig) {
api_host: window.analyticsConfig.host, api_host: window.analyticsConfig.host,
}); });
} }
Vue.use(VueDOMPurifyHTML);
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
Vue.use(VueRouter); Vue.use(VueRouter);
Vue.use(VueI18n); Vue.use(VueI18n);
Vue.use(WootUiKit); Vue.use(WootUiKit);

View file

@ -9,9 +9,10 @@ import ActionCableConnector from '../widget/helpers/actionCable';
import i18n from '../widget/i18n'; import i18n from '../widget/i18n';
import { isPhoneE164OrEmpty } from 'shared/helpers/Validators'; import { isPhoneE164OrEmpty } from 'shared/helpers/Validators';
import router from '../widget/router'; import router from '../widget/router';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
Vue.use(VueI18n); Vue.use(VueI18n);
Vue.use(Vuelidate); Vue.use(Vuelidate);
Vue.use(VueDOMPurifyHTML); Vue.use(VueDOMPurifyHTML, domPurifyConfig);
const i18nConfig = new VueI18n({ const i18nConfig = new VueI18n({
locale: 'en', locale: 'en',

View file

@ -6,3 +6,15 @@ export const escapeHtml = (unsafe = '') => {
.replace(/"/g, '&quot;') .replace(/"/g, '&quot;')
.replace(/'/g, '&#039;'); .replace(/'/g, '&#039;');
}; };
export const afterSanitizeAttributes = currentNode => {
if ('target' in currentNode) {
currentNode.setAttribute('target', '_blank');
}
};
export const domPurifyConfig = {
hooks: {
afterSanitizeAttributes,
},
};

View file

@ -1,6 +1,6 @@
import { marked } from 'marked'; import { marked } from 'marked';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { escapeHtml } from './HTMLSanitizer'; import { escapeHtml, afterSanitizeAttributes } from './HTMLSanitizer';
const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g; const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
const TWITTER_USERNAME_REPLACEMENT = const TWITTER_USERNAME_REPLACEMENT =
@ -48,9 +48,7 @@ class MessageFormatter {
const markedDownOutput = marked(withHash); const markedDownOutput = marked(withHash);
return markedDownOutput; return markedDownOutput;
} }
DOMPurify.addHook('afterSanitizeAttributes', node => { DOMPurify.addHook('afterSanitizeAttributes', afterSanitizeAttributes);
if ('target' in node) node.setAttribute('target', '_blank');
});
return DOMPurify.sanitize( return DOMPurify.sanitize(
marked(this.message, { breaks: true, gfm: true }) marked(this.message, { breaks: true, gfm: true })
); );

View file

@ -51,9 +51,9 @@ class Account < ApplicationRecord
has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage' has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage'
has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook' has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook'
has_many :inboxes, dependent: :destroy_async has_many :inboxes, dependent: :destroy_async
has_many :kbase_articles, dependent: :destroy_async, class_name: '::Kbase::Article' has_many :articles, dependent: :destroy_async, class_name: '::Article'
has_many :kbase_categories, dependent: :destroy_async, class_name: '::Kbase::Category' has_many :categories, dependent: :destroy_async, class_name: '::Category'
has_many :kbase_portals, dependent: :destroy_async, class_name: '::Kbase::Portal' has_many :portals, dependent: :destroy_async, class_name: '::Portal'
has_many :labels, dependent: :destroy_async has_many :labels, dependent: :destroy_async
has_many :line_channels, dependent: :destroy_async, class_name: '::Channel::Line' has_many :line_channels, dependent: :destroy_async, class_name: '::Channel::Line'
has_many :mentions, dependent: :destroy_async has_many :mentions, dependent: :destroy_async

View file

@ -1,6 +1,6 @@
# == Schema Information # == Schema Information
# #
# Table name: kbase_articles # Table name: articles
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# content :text # content :text
@ -16,7 +16,7 @@
# folder_id :integer # folder_id :integer
# portal_id :integer not null # portal_id :integer not null
# #
class Kbase::Article < ApplicationRecord class Article < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :category belongs_to :category
belongs_to :portal belongs_to :portal

View file

@ -1,6 +1,6 @@
# == Schema Information # == Schema Information
# #
# Table name: kbase_categories # Table name: categories
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# description :text # description :text
@ -14,9 +14,10 @@
# #
# Indexes # Indexes
# #
# index_kbase_categories_on_locale_and_account_id (locale,account_id) # index_categories_on_locale (locale)
# index_categories_on_locale_and_account_id (locale,account_id)
# #
class Kbase::Category < ApplicationRecord class Category < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :portal belongs_to :portal
has_many :folders, dependent: :destroy_async has_many :folders, dependent: :destroy_async

View file

@ -1,6 +1,6 @@
# == Schema Information # == Schema Information
# #
# Table name: kbase_folders # Table name: folders
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# name :string # name :string
@ -9,7 +9,7 @@
# account_id :integer not null # account_id :integer not null
# category_id :integer not null # category_id :integer not null
# #
class Kbase::Folder < ApplicationRecord class Folder < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :category belongs_to :category
has_many :articles, dependent: :nullify has_many :articles, dependent: :nullify

View file

@ -1,8 +1,9 @@
# == Schema Information # == Schema Information
# #
# Table name: kbase_portals # Table name: portals
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# archived :boolean default(FALSE)
# color :string # color :string
# config :jsonb # config :jsonb
# custom_domain :string # custom_domain :string
@ -17,13 +18,14 @@
# #
# Indexes # Indexes
# #
# index_kbase_portals_on_slug (slug) UNIQUE # index_portals_on_slug (slug) UNIQUE
# #
class Kbase::Portal < ApplicationRecord class Portal < ApplicationRecord
belongs_to :account belongs_to :account
has_many :categories, dependent: :destroy_async has_many :categories, dependent: :destroy_async
has_many :folders, through: :categories has_many :folders, through: :categories
has_many :articles, dependent: :destroy_async has_many :articles, dependent: :destroy_async
has_many :users, through: :portals_members
validates :account_id, presence: true validates :account_id, presence: true
validates :name, presence: true validates :name, presence: true

View file

@ -90,6 +90,7 @@ class User < ApplicationRecord
has_many :notifications, dependent: :destroy_async has_many :notifications, dependent: :destroy_async
has_many :team_members, dependent: :destroy_async has_many :team_members, dependent: :destroy_async
has_many :teams, through: :team_members has_many :teams, through: :team_members
has_many :portals, through: :portals_members
before_validation :set_password_and_uid, on: :create before_validation :set_password_and_uid, on: :create

View file

@ -6,4 +6,8 @@ class CsatSurveyResponsePolicy < ApplicationPolicy
def metrics? def metrics?
@account_user.administrator? @account_user.administrator?
end end
def download?
@account_user.administrator?
end
end end

View file

@ -0,0 +1,38 @@
<%=
CSV.generate_line([
I18n.t('reports.csat.headers.agent_name'),
I18n.t('reports.csat.headers.rating'),
I18n.t('reports.csat.headers.feedback'),
I18n.t('reports.csat.headers.contact_name'),
I18n.t('reports.csat.headers.contact_email_address'),
I18n.t('reports.csat.headers.contact_phone_number'),
I18n.t('reports.csat.headers.link_to_the_conversation'),
I18n.t('reports.csat.headers.recorded_at')
])
-%>
<% @csat_survey_responses.each do |csat_response| %>
<% assigned_agent = csat_response.assigned_agent %>
<% contact = csat_response.contact %>
<% conversation = csat_response.conversation %>
<%=
CSV.generate_line([
assigned_agent ? "#{assigned_agent.name} (#{assigned_agent.email})" : nil,
csat_response.rating,
csat_response.feedback_message.present? ? csat_response.feedback_message : nil,
contact&.name.present? ? contact&.name: nil,
contact&.email.present? ? contact&.email: nil,
contact&.phone_number.present? ? contact&.phone_number: nil,
conversation ? app_account_conversation_url(account_id: Current.account.id, id: conversation.display_id): nil,
csat_response.created_at,
])
-%>
<% end %>
<%=
CSV.generate_line([
I18n.t(
'reports.period',
since: Date.strptime(params[:since], '%s'),
until: Date.strptime(params[:until], '%s')
)
])
-%>

View file

@ -6,4 +6,5 @@ json.homepage_link portal.homepage_link
json.name portal.name json.name portal.name
json.page_title portal.page_title json.page_title portal.page_title
json.slug portal.slug json.slug portal.slug
json.archived portal.archived
json.config portal.config json.config portal.config

View file

@ -1,5 +1,5 @@
shared: &shared shared: &shared
version: '2.4.1' version: '2.5.0'
development: development:
<<: *shared <<: *shared

View file

@ -60,6 +60,16 @@ en:
avg_first_response_time: Avg first response time (Minutes) avg_first_response_time: Avg first response time (Minutes)
avg_resolution_time: Avg resolution time (Minutes) avg_resolution_time: Avg resolution time (Minutes)
default_group_by: day default_group_by: day
csat:
headers:
contact_name: Contact Name
contact_email_address: Contact Email Address
contact_phone_number: Contact Phone Number
link_to_the_conversation: Link to the conversation
agent_name: Agent Name
rating: Rating
feedback: Feedback Comment
recorded_at: Recorded date
notifications: notifications:
notification_title: notification_title:

View file

@ -106,6 +106,7 @@ Rails.application.routes.draw do
resources :csat_survey_responses, only: [:index] do resources :csat_survey_responses, only: [:index] do
collection do collection do
get :metrics get :metrics
get :download
end end
end end
resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy] resources :custom_attribute_definitions, only: [:index, :show, :create, :update, :destroy]
@ -154,16 +155,17 @@ Rails.application.routes.draw do
end end
resources :working_hours, only: [:update] resources :working_hours, only: [:update]
namespace :kbase do
resources :portals do resources :portals do
member do
post :archive
end
resources :categories do resources :categories do
resources :folders resources :folders
end end
end
resources :articles resources :articles
end end
end end
end
end
# end of account scoped api routes # end of account scoped api routes
# ---------------------------------- # ----------------------------------

View file

@ -0,0 +1,5 @@
class ChangeKbasePortalsToPortals < ActiveRecord::Migration[6.1]
def change
rename_table :kbase_portals, :portals
end
end

View file

@ -0,0 +1,9 @@
class CreatePortalsMembersJoinTable < ActiveRecord::Migration[6.1]
def change
create_join_table :portals, :users, table_name: :portals_members do |t|
t.index :portal_id
t.index :user_id
t.index [:portal_id, :user_id], unique: true
end
end
end

View file

@ -0,0 +1,5 @@
class ChangeKbaseCategoriesToCategories < ActiveRecord::Migration[6.1]
def change
rename_table :kbase_categories, :categories
end
end

View file

@ -0,0 +1,5 @@
class ChangeKbaseFoldersToFolders < ActiveRecord::Migration[6.1]
def change
rename_table :kbase_folders, :folders
end
end

View file

@ -0,0 +1,5 @@
class ChangeKbaseArticlesToArticles < ActiveRecord::Migration[6.1]
def change
rename_table :kbase_articles, :articles
end
end

View file

@ -0,0 +1,5 @@
class AddArchiveColumnToPortal < ActiveRecord::Migration[6.1]
def change
add_column :portals, :archived, :boolean, default: false
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_05_06_163839) do ActiveRecord::Schema.define(version: 2022_05_11_072655) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
@ -111,6 +111,21 @@ ActiveRecord::Schema.define(version: 2022_05_06_163839) do
t.index ["account_id"], name: "index_agent_bots_on_account_id" t.index ["account_id"], name: "index_agent_bots_on_account_id"
end end
create_table "articles", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "portal_id", null: false
t.integer "category_id"
t.integer "folder_id"
t.integer "author_id"
t.string "title"
t.text "description"
t.text "content"
t.integer "status"
t.integer "views"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "attachments", id: :serial, force: :cascade do |t| create_table "attachments", id: :serial, force: :cascade do |t|
t.integer "file_type", default: 0 t.integer "file_type", default: 0
t.string "external_url" t.string "external_url"
@ -169,6 +184,19 @@ ActiveRecord::Schema.define(version: 2022_05_06_163839) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "categories", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "portal_id", null: false
t.string "name"
t.text "description"
t.integer "position"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "locale", default: "en"
t.index ["locale", "account_id"], name: "index_categories_on_locale_and_account_id"
t.index ["locale"], name: "index_categories_on_locale"
end
create_table "channel_api", force: :cascade do |t| create_table "channel_api", force: :cascade do |t|
t.integer "account_id", null: false t.integer "account_id", null: false
t.string "webhook_url" t.string "webhook_url"
@ -434,6 +462,14 @@ ActiveRecord::Schema.define(version: 2022_05_06_163839) do
t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true
end end
create_table "folders", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "category_id", null: false
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "inbox_members", id: :serial, force: :cascade do |t| create_table "inbox_members", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "inbox_id", null: false t.integer "inbox_id", null: false
@ -486,56 +522,6 @@ ActiveRecord::Schema.define(version: 2022_05_06_163839) do
t.jsonb "settings", default: {} t.jsonb "settings", default: {}
end end
create_table "kbase_articles", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "portal_id", null: false
t.integer "category_id"
t.integer "folder_id"
t.integer "author_id"
t.string "title"
t.text "description"
t.text "content"
t.integer "status"
t.integer "views"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "kbase_categories", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "portal_id", null: false
t.string "name"
t.text "description"
t.integer "position"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "locale", default: "en"
t.index ["locale", "account_id"], name: "index_kbase_categories_on_locale_and_account_id"
end
create_table "kbase_folders", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "category_id", null: false
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "kbase_portals", force: :cascade do |t|
t.integer "account_id", null: false
t.string "name", null: false
t.string "slug", null: false
t.string "custom_domain"
t.string "color"
t.string "homepage_link"
t.string "page_title"
t.text "header_text"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.jsonb "config", default: {"allowed_locales"=>["en"]}
t.index ["slug"], name: "index_kbase_portals_on_slug", unique: true
end
create_table "labels", force: :cascade do |t| create_table "labels", force: :cascade do |t|
t.string "title" t.string "title"
t.text "description" t.text "description"
@ -653,6 +639,30 @@ ActiveRecord::Schema.define(version: 2022_05_06_163839) do
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
end end
create_table "portals", force: :cascade do |t|
t.integer "account_id", null: false
t.string "name", null: false
t.string "slug", null: false
t.string "custom_domain"
t.string "color"
t.string "homepage_link"
t.string "page_title"
t.text "header_text"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.jsonb "config", default: {"allowed_locales"=>["en"]}
t.boolean "archived", default: false
t.index ["slug"], name: "index_portals_on_slug", unique: true
end
create_table "portals_members", id: false, force: :cascade do |t|
t.bigint "portal_id", null: false
t.bigint "user_id", null: false
t.index ["portal_id", "user_id"], name: "index_portals_members_on_portal_id_and_user_id", unique: true
t.index ["portal_id"], name: "index_portals_members_on_portal_id"
t.index ["user_id"], name: "index_portals_members_on_user_id"
end
create_table "reporting_events", force: :cascade do |t| create_table "reporting_events", force: :cascade do |t|
t.string "name" t.string "name"
t.float "value" t.float "value"

View file

@ -1,6 +1,6 @@
{ {
"name": "@chatwoot/chatwoot", "name": "@chatwoot/chatwoot",
"version": "2.4.1", "version": "2.5.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"eslint": "eslint app/**/*.{js,vue} --fix", "eslint": "eslint app/**/*.{js,vue} --fix",
@ -63,7 +63,7 @@
"vue-chartjs": "3.5.1", "vue-chartjs": "3.5.1",
"vue-clickaway": "~2.1.0", "vue-clickaway": "~2.1.0",
"vue-color": "2.8.1", "vue-color": "2.8.1",
"vue-dompurify-html": "^2.5.1", "vue-dompurify-html": "^2.5.2",
"vue-easytable": "2.5.5", "vue-easytable": "2.5.5",
"vue-i18n": "8.24.3", "vue-i18n": "8.24.3",
"vue-loader": "15.9.6", "vue-loader": "15.9.6",

View file

@ -1,15 +1,15 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Kbase::Categories', type: :request do RSpec.describe 'Api::V1::Accounts::Categories', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
let!(:portal) { create(:kbase_portal, name: 'test_portal', account_id: account.id) } let!(:portal) { create(:portal, name: 'test_portal', account_id: account.id) }
let!(:category) { create(:kbase_category, name: 'category', portal: portal, account_id: account.id) } let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id) }
describe 'POST /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}/categories' do describe 'POST /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories", params: {} post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories", params: {}
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@ -23,7 +23,7 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Categories', type: :request do
position: 1 position: 1
} }
} }
post "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories", post "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
params: category_params, params: category_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
@ -33,10 +33,10 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Categories', type: :request do
end end
end end
describe 'PUT /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}/categories/{category.id}' do describe 'PUT /api/v1/accounts/{account.id}/portals/{portal.slug}/categories/{category.id}' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories/#{category.id}", params: {} put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}", params: {}
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@ -53,7 +53,7 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Categories', type: :request do
expect(category.name).not_to eql(category_params[:category][:name]) expect(category.name).not_to eql(category_params[:category][:name])
put "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories/#{category.id}", put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
params: category_params, params: category_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
@ -63,39 +63,39 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Categories', type: :request do
end end
end end
describe 'DELETE /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}/categories/{category.id}' do describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}/categories/{category.id}' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories/#{category.id}", params: {} delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}", params: {}
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
it 'deletes category' do it 'deletes category' do
delete "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories/#{category.id}", delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories/#{category.id}",
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
deleted_category = Kbase::Category.find_by(id: category.id) deleted_category = Category.find_by(id: category.id)
expect(deleted_category).to be nil expect(deleted_category).to be nil
end end
end end
end end
describe 'GET /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}/categories' do describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}/categories' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories" get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories"
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
it 'get all portals' do it 'get all portals' do
category2 = create(:kbase_category, name: 'test_category_2', portal: portal) category2 = create(:category, name: 'test_category_2', portal: portal)
expect(category2.id).not_to be nil expect(category2.id).not_to be nil
get "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}/categories", get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}/categories",
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)

View file

@ -148,4 +148,38 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do
end end
end end
end end
describe 'GET /api/v1/accounts/{account.id}/csat_survey_responses/download' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/download"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:params) { { since: 5.days.ago.to_time.to_i.to_s, until: Time.zone.tomorrow.to_time.to_i.to_s } }
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/download",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'returns summary' do
get "/api/v1/accounts/#{account.id}/csat_survey_responses/download",
params: params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
content = CSV.parse(response.body)
# Check rating from CSAT Row
expect(content[1][1]).to eq '1'
expect(content.length).to eq 3
end
end
end
end end

View file

@ -1,23 +1,23 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Kbase::Portals', type: :request do RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
let!(:portal) { create(:kbase_portal, slug: 'portal-1', name: 'test_portal', account_id: account.id) } let!(:portal) { create(:portal, slug: 'portal-1', name: 'test_portal', account_id: account.id) }
describe 'GET /api/v1/accounts/{account.id}/kbase/portals' do describe 'GET /api/v1/accounts/{account.id}/portals' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/kbase/portals" get "/api/v1/accounts/#{account.id}/portals"
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
it 'get all portals' do it 'get all portals' do
portal2 = create(:kbase_portal, name: 'test_portal_2', account_id: account.id, slug: 'portal-2') portal2 = create(:portal, name: 'test_portal_2', account_id: account.id, slug: 'portal-2')
expect(portal2.id).not_to be nil expect(portal2.id).not_to be nil
get "/api/v1/accounts/#{account.id}/kbase/portals", get "/api/v1/accounts/#{account.id}/portals",
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
@ -26,17 +26,17 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Portals', type: :request do
end end
end end
describe 'GET /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}' do describe 'GET /api/v1/accounts/{account.id}/portals/{portal.slug}' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/kbase/portals" get "/api/v1/accounts/#{account.id}/portals"
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
it 'get one portals' do it 'get one portals' do
get "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}", get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
@ -45,10 +45,10 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Portals', type: :request do
end end
end end
describe 'POST /api/v1/accounts/{account.id}/kbase/portals' do describe 'POST /api/v1/accounts/{account.id}/portals' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/kbase/portals", params: {} post "/api/v1/accounts/#{account.id}/portals", params: {}
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@ -61,7 +61,7 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Portals', type: :request do
slug: 'test_kbase' slug: 'test_kbase'
} }
} }
post "/api/v1/accounts/#{account.id}/kbase/portals", post "/api/v1/accounts/#{account.id}/portals",
params: portal_params, params: portal_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
@ -71,10 +71,10 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Portals', type: :request do
end end
end end
describe 'PUT /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}' do describe 'PUT /api/v1/accounts/{account.id}/portals/{portal.slug}' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}", params: {} put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}", params: {}
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@ -89,30 +89,50 @@ RSpec.describe 'Api::V1::Accounts::Kbase::Portals', type: :request do
expect(portal.name).to eql('test_portal') expect(portal.name).to eql('test_portal')
put "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}", put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: portal_params, params: portal_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
expect(json_response['name']).to eql(portal_params[:portal][:name]) expect(json_response['name']).to eql(portal_params[:portal][:name])
end end
it 'archive portal' do
portal_params = {
portal: {
archived: true
}
}
expect(portal.archived).to be_falsy
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: portal_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['archived']).to eql(portal_params[:portal][:archived])
portal.reload
expect(portal.archived).to be_truthy
end
end end
end end
describe 'DELETE /api/v1/accounts/{account.id}/kbase/portals/{portal.slug}' do describe 'DELETE /api/v1/accounts/{account.id}/portals/{portal.slug}' do
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}", params: {} delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}", params: {}
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
it 'deletes portal' do it 'deletes portal' do
delete "/api/v1/accounts/#{account.id}/kbase/portals/#{portal.slug}", delete "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
deleted_portal = Kbase::Portal.find_by(id: portal.slug) deleted_portal = Portal.find_by(id: portal.slug)
expect(deleted_portal).to be nil expect(deleted_portal).to be nil
end end
end end

View file

@ -1,5 +1,5 @@
FactoryBot.define do FactoryBot.define do
factory :kbase_article, class: 'Kbase::Article' do factory :article, class: 'Article' do
account_id { 1 } account_id { 1 }
category_id { 1 } category_id { 1 }
folder_id { 1 } folder_id { 1 }

View file

@ -1,6 +1,6 @@
FactoryBot.define do FactoryBot.define do
factory :kbase_category, class: 'Kbase::Category' do factory :category, class: 'Category' do
portal { kbase_portal } portal { portal }
name { 'MyString' } name { 'MyString' }
description { 'MyText' } description { 'MyText' }
position { 1 } position { 1 }

View file

@ -1,5 +1,5 @@
FactoryBot.define do FactoryBot.define do
factory :kbase_folder, class: 'Kbase::Folder' do factory :folder, class: 'Folder' do
account_id { 1 } account_id { 1 }
name { 'MyString' } name { 'MyString' }
description { 'MyText' } description { 'MyText' }

View file

@ -1,5 +1,5 @@
FactoryBot.define do FactoryBot.define do
factory :kbase_portal, class: 'Kbase::Portal' do factory :portal, class: 'Portal' do
account account
name { Faker::Book.name } name { Faker::Book.name }
slug { SecureRandom.hex } slug { SecureRandom.hex }

View file

@ -19,8 +19,8 @@ RSpec.describe Account do
it { is_expected.to have_many(:webhooks).dependent(:destroy_async) } it { is_expected.to have_many(:webhooks).dependent(:destroy_async) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy_async) } it { is_expected.to have_many(:notification_settings).dependent(:destroy_async) }
it { is_expected.to have_many(:reporting_events) } it { is_expected.to have_many(:reporting_events) }
it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) } it { is_expected.to have_many(:portals).dependent(:destroy_async) }
it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) } it { is_expected.to have_many(:categories).dependent(:destroy_async) }
it { is_expected.to have_many(:teams).dependent(:destroy_async) } it { is_expected.to have_many(:teams).dependent(:destroy_async) }
describe 'usage_limits' do describe 'usage_limits' do

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Kbase::Article, type: :model do RSpec.describe Article, type: :model do
context 'with validations' do context 'with validations' do
it { is_expected.to validate_presence_of(:account_id) } it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_presence_of(:category_id) } it { is_expected.to validate_presence_of(:category_id) }

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Kbase::Category, type: :model do RSpec.describe Category, type: :model do
context 'with validations' do context 'with validations' do
it { is_expected.to validate_presence_of(:account_id) } it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Kbase::Folder, type: :model do RSpec.describe Folder, type: :model do
context 'with validations' do context 'with validations' do
it { is_expected.to validate_presence_of(:account_id) } it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_presence_of(:category_id) } it { is_expected.to validate_presence_of(:category_id) }

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Kbase::Portal, type: :model do RSpec.describe Portal, type: :model do
context 'with validations' do context 'with validations' do
it { is_expected.to validate_presence_of(:account_id) } it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_presence_of(:slug) } it { is_expected.to validate_presence_of(:slug) }

View file

@ -15284,10 +15284,10 @@ vue-docgen-loader@^1.5.0:
loader-utils "^1.2.3" loader-utils "^1.2.3"
querystring "^0.2.0" querystring "^0.2.0"
vue-dompurify-html@^2.5.1: vue-dompurify-html@^2.5.2:
version "2.5.1" version "2.5.2"
resolved "https://registry.npmjs.org/vue-dompurify-html/-/vue-dompurify-html-2.5.1.tgz#a754f4ac7b18eb8fe41f461cb2bb1c4956a9bd2d" resolved "https://registry.yarnpkg.com/vue-dompurify-html/-/vue-dompurify-html-2.5.2.tgz#f547d4eacae4640f95eb0e9308e7ef8e223887c6"
integrity sha512-B8rQj2jAPJJhtKHHa6jg5B3/RoKBmmUl/awP/GxWXGu75j4Y7+MHqv0DG52v0Uz0taEpHyZun34KEYMAfrPWnA== integrity sha512-G6I135+BhlACJ9xftqK7fvhXyjNrgHCI594qHnUW5e2Bmp8BOTV1kz7cxwI37b4BJnHkj9IY10RwMPOtJqw+pw==
dependencies: dependencies:
dompurify "^2.3.4" dompurify "^2.3.4"