Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23230e0143 | ||
|
|
f92cea144c | ||
|
|
e6cf8c39b7 | ||
|
|
c5c0845151 | ||
|
|
5d9fb55370 | ||
|
|
721a2f5052 | ||
|
|
7320957405 | ||
|
|
e4d072c79c | ||
|
|
71ee10c889 | ||
|
|
978a8a4cb2 | ||
|
|
cd06b2b337 | ||
|
|
6eb06377cc | ||
|
|
94892e7168 | ||
|
|
fe744abe26 | ||
|
|
a902b49bc5 | ||
|
|
9c07b6dd46 | ||
|
|
3b7b06dbec | ||
|
|
597a4164e8 | ||
|
|
678c00f254 | ||
|
|
7298002da7 |
@@ -49,6 +49,11 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
end
|
||||
|
||||
# This endpoint is used to bulk create agents during onboarding
|
||||
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
|
||||
Current.account.custom_attributes.delete('onboarding_step')
|
||||
Current.account.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def export
|
||||
column_names = params['column_names']
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email)
|
||||
head :ok, message: I18n.t('errors.contacts.export.success')
|
||||
end
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@notifications = notification_finder.notifications
|
||||
@unread_count = notification_finder.unread_count
|
||||
@notifications = notification_finder.perform
|
||||
@count = notification_finder.count
|
||||
end
|
||||
|
||||
@@ -54,7 +54,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def snooze
|
||||
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
|
||||
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil)
|
||||
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
def update
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
|
||||
@account.save!
|
||||
end
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class Api::V2::AccountsController < Api::BaseController
|
||||
).perform
|
||||
|
||||
fetch_account_and_user_info
|
||||
update_account_info if @account.present?
|
||||
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
@@ -33,6 +34,18 @@ class Api::V2::AccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def account_attributes
|
||||
{
|
||||
custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' })
|
||||
}
|
||||
end
|
||||
|
||||
def update_account_info
|
||||
@account.update!(
|
||||
account_attributes
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_account_and_user_info; end
|
||||
|
||||
def fetch_account
|
||||
|
||||
@@ -55,7 +55,7 @@ class DashboardController < ActionController::Base
|
||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||
FACEBOOK_API_VERSION: 'v14.0',
|
||||
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
|
||||
GIT_SHA: GIT_HASH
|
||||
|
||||
@@ -34,7 +34,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
def allowed_configs
|
||||
@allowed_configs = case @config
|
||||
when 'facebook'
|
||||
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
||||
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
||||
when 'email'
|
||||
['MAILER_INBOUND_EMAIL_DOMAIN']
|
||||
else
|
||||
|
||||
@@ -169,6 +169,12 @@ class ConversationFinder
|
||||
)
|
||||
|
||||
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
@conversations = @conversations.send(sort_by, sort_order)
|
||||
|
||||
if params[:updated_within].present?
|
||||
@conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds)
|
||||
else
|
||||
@conversations.page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,8 +10,8 @@ class NotificationFinder
|
||||
set_up
|
||||
end
|
||||
|
||||
def perform
|
||||
notifications
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order)
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@@ -26,27 +26,31 @@ class NotificationFinder
|
||||
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_by_read_status
|
||||
filter_by_status
|
||||
filter_snoozed_notifications
|
||||
fitler_read_notifications
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
|
||||
def filter_snoozed_notifications
|
||||
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
|
||||
end
|
||||
|
||||
def filter_by_read_status
|
||||
@notifications = @notifications.where.not(read_at: nil) if params[:type] == 'read'
|
||||
def fitler_read_notifications
|
||||
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
|
||||
end
|
||||
|
||||
def type_included?(type)
|
||||
(params[:includes] || []).include?(type)
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: params[:sort_order] || :desc)
|
||||
def sort_order
|
||||
params[:sort_order] || :desc
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class Agents extends ApiClient {
|
||||
constructor() {
|
||||
super('agents', { accountScoped: true });
|
||||
}
|
||||
|
||||
bulkInvite({ emails }) {
|
||||
return axios.post(`${this.url}/bulk_create`, {
|
||||
emails,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Agents();
|
||||
|
||||
@@ -7,12 +7,13 @@ class NotificationsAPI extends ApiClient {
|
||||
}
|
||||
|
||||
get({ page, status, type, sortOrder }) {
|
||||
const includesFilter = [status, type].filter(value => !!value);
|
||||
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
status,
|
||||
type,
|
||||
sort_order: sortOrder,
|
||||
includes: includesFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,4 +10,29 @@ describe('#AgentAPI', () => {
|
||||
expect(agents).toHaveProperty('update');
|
||||
expect(agents).toHaveProperty('delete');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#bulkInvite', () => {
|
||||
agents.bulkInvite({ emails: ['hello@hi.com'] });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/agents/bulk_create',
|
||||
{
|
||||
emails: ['hello@hi.com'],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,20 +27,36 @@ describe('#NotificationAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'Conversation',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
describe('#get', () => {
|
||||
it('generates the API call if both params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'Conversation',
|
||||
sort_order: 'desc',
|
||||
},
|
||||
status: 'snoozed',
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['snoozed', 'read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates the API call if one of the params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -753,7 +753,7 @@ export default {
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
@apply z-50 bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800;
|
||||
@apply z-[9999] bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800 shadow-lg;
|
||||
|
||||
h5 {
|
||||
@apply dark:text-slate-25 text-slate-800;
|
||||
|
||||
@@ -79,7 +79,13 @@
|
||||
"TITLE": "Export Contacts",
|
||||
"DESC": "Export contacts to a CSV file.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
"ERROR_MESSAGE": "There was an error, please try again",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Export Contacts",
|
||||
"MESSAGE": "Are you sure you want to export all contacts?",
|
||||
"YES": "Yes, Export",
|
||||
"NO": "No, Cancel"
|
||||
}
|
||||
},
|
||||
"DELETE_NOTE": {
|
||||
"CONFIRM": {
|
||||
|
||||
@@ -113,6 +113,13 @@
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<woot-confirm-modal
|
||||
ref="confirmExportContactsDialog"
|
||||
:title="$t('EXPORT_CONTACTS.CONFIRM.TITLE')"
|
||||
:description="$t('EXPORT_CONTACTS.CONFIRM.MESSAGE')"
|
||||
:confirm-label="$t('EXPORT_CONTACTS.CONFIRM.YES')"
|
||||
:cancel-label="$t('EXPORT_CONTACTS.CONFIRM.NO')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -175,8 +182,13 @@ export default {
|
||||
toggleImport() {
|
||||
this.$emit('on-toggle-import');
|
||||
},
|
||||
submitExport() {
|
||||
this.$emit('on-export-submit');
|
||||
async submitExport() {
|
||||
const ok =
|
||||
await this.$refs.confirmExportContactsDialog.showConfirmation();
|
||||
|
||||
if (ok) {
|
||||
this.$emit('on-export-submit');
|
||||
}
|
||||
},
|
||||
submitSearch() {
|
||||
this.$emit('on-search-submit');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { applyInboxPageFilters, sortComparator } from './helpers';
|
||||
import { sortComparator } from './helpers';
|
||||
|
||||
export const getters = {
|
||||
getNotifications($state) {
|
||||
@@ -6,11 +6,8 @@ export const getters = {
|
||||
},
|
||||
getFilteredNotifications: $state => filters => {
|
||||
const sortOrder = filters.sortOrder === 'desc' ? 'newest' : 'oldest';
|
||||
const filteredNotifications = Object.values($state.records).filter(
|
||||
notification => applyInboxPageFilters(notification, filters)
|
||||
);
|
||||
const sortedNotifications = filteredNotifications.sort((a, b) =>
|
||||
sortComparator(a, b, sortOrder)
|
||||
const sortedNotifications = Object.values($state.records).sort((n1, n2) =>
|
||||
sortComparator(n1, n2, sortOrder)
|
||||
);
|
||||
return sortedNotifications;
|
||||
},
|
||||
|
||||
@@ -1,31 +1,3 @@
|
||||
export const filterByStatus = (snoozedUntil, filterStatus) =>
|
||||
filterStatus === 'snoozed' ? !!snoozedUntil : !snoozedUntil;
|
||||
|
||||
export const filterByType = (readAt, filterType) =>
|
||||
filterType === 'read' ? !!readAt : !readAt;
|
||||
|
||||
export const filterByTypeAndStatus = (
|
||||
readAt,
|
||||
snoozedUntil,
|
||||
filterType,
|
||||
filterStatus
|
||||
) => {
|
||||
const shouldFilterByStatus = filterByStatus(snoozedUntil, filterStatus);
|
||||
const shouldFilterByType = filterByType(readAt, filterType);
|
||||
return shouldFilterByStatus && shouldFilterByType;
|
||||
};
|
||||
|
||||
export const applyInboxPageFilters = (notification, filters) => {
|
||||
const { status, type } = filters;
|
||||
const { read_at: readAt, snoozed_until: snoozedUntil } = notification;
|
||||
|
||||
if (status && type)
|
||||
return filterByTypeAndStatus(readAt, snoozedUntil, type, status);
|
||||
if (status && !type) return filterByStatus(snoozedUntil, status);
|
||||
if (!status && type) return filterByType(readAt, type);
|
||||
return true;
|
||||
};
|
||||
|
||||
const INBOX_SORT_OPTIONS = {
|
||||
newest: 'desc',
|
||||
oldest: 'asc',
|
||||
|
||||
@@ -34,6 +34,8 @@ describe('#getters', () => {
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
expect(getters.getFilteredNotifications(state)(filters)).toEqual([
|
||||
{ id: 1, read_at: '2024-02-07T11:42:39.988Z', snoozed_until: null },
|
||||
{ id: 2, read_at: null, snoozed_until: null },
|
||||
{
|
||||
id: 3,
|
||||
read_at: '2024-02-07T11:42:39.988Z',
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
filterByStatus,
|
||||
filterByType,
|
||||
filterByTypeAndStatus,
|
||||
applyInboxPageFilters,
|
||||
sortComparator,
|
||||
} from '../../notifications/helpers';
|
||||
import { sortComparator } from '../../notifications/helpers';
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
@@ -45,126 +39,6 @@ const notifications = [
|
||||
},
|
||||
];
|
||||
|
||||
describe('#filterByStatus', () => {
|
||||
it('returns the notifications with snoozed status', () => {
|
||||
const filters = { status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(
|
||||
filterByStatus(notification.snoozed_until, filters.status)
|
||||
).toEqual(notification.snoozed_until !== null);
|
||||
});
|
||||
});
|
||||
it('returns true if the notification is snoozed', () => {
|
||||
const filters = { status: 'snoozed' };
|
||||
expect(
|
||||
filterByStatus(notifications[3].snoozed_until, filters.status)
|
||||
).toEqual(true);
|
||||
});
|
||||
it('returns false if the notification is not snoozed', () => {
|
||||
const filters = { status: 'snoozed' };
|
||||
expect(
|
||||
filterByStatus(notifications[2].snoozed_until, filters.status)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#filterByType', () => {
|
||||
it('returns the notifications with read status', () => {
|
||||
const filters = { type: 'read' };
|
||||
notifications.forEach(notification => {
|
||||
expect(filterByType(notification.read_at, filters.type)).toEqual(
|
||||
notification.read_at !== null
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns true if the notification is read', () => {
|
||||
const filters = { type: 'read' };
|
||||
expect(filterByType(notifications[0].read_at, filters.type)).toEqual(true);
|
||||
});
|
||||
it('returns false if the notification is not read', () => {
|
||||
const filters = { type: 'read' };
|
||||
expect(filterByType(notifications[1].read_at, filters.type)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#filterByTypeAndStatus', () => {
|
||||
it('returns the notifications with type and status', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(
|
||||
filterByTypeAndStatus(
|
||||
notification.read_at,
|
||||
notification.snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
).toEqual(
|
||||
notification.read_at !== null && notification.snoozed_until !== null
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns true if the notification is read and snoozed', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
expect(
|
||||
filterByTypeAndStatus(
|
||||
notifications[4].read_at,
|
||||
notifications[4].snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
).toEqual(true);
|
||||
});
|
||||
it('returns false if the notification is not read and snoozed', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
expect(
|
||||
filterByTypeAndStatus(
|
||||
notifications[3].read_at,
|
||||
notifications[3].snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#applyInboxPageFilters', () => {
|
||||
it('returns the notifications with type and status', () => {
|
||||
const filters = { type: 'read', status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(
|
||||
filterByTypeAndStatus(
|
||||
notification.read_at,
|
||||
notification.snoozed_until,
|
||||
filters.type,
|
||||
filters.status
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns the notifications with type only', () => {
|
||||
const filters = { type: 'read', status: null };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(
|
||||
filterByType(notification.read_at, filters.type)
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns the notifications with status only', () => {
|
||||
const filters = { type: null, status: 'snoozed' };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(
|
||||
filterByStatus(notification.snoozed_until, filters.status)
|
||||
);
|
||||
});
|
||||
});
|
||||
it('returns true if there are no filters', () => {
|
||||
const filters = { type: null, status: null };
|
||||
notifications.forEach(notification => {
|
||||
expect(applyInboxPageFilters(notification, filters)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sortComparator', () => {
|
||||
it('returns the notifications sorted by newest', () => {
|
||||
const sortOrder = 'newest';
|
||||
|
||||
@@ -6,24 +6,29 @@
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<template #rightOfLabel>
|
||||
<slot />
|
||||
</template>
|
||||
<input
|
||||
:id="name"
|
||||
:name="name"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
:tabindex="tabindex"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:class="{
|
||||
'focus:ring-red-600 ring-red-600': hasError,
|
||||
'dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200':
|
||||
'focus:outline-red-600 outline-red-600 dark:focus:outline-red-600 dark:outline-red-600':
|
||||
hasError,
|
||||
'outline-slate-200 dark:outline-slate-600 dark:focus:outline-woot-500 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'px-3 py-3': spacing === 'base',
|
||||
'px-3 py-2 mb-0': spacing === 'compact',
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full border-none shadow-sm appearance-none rounded-xl outline outline-1 outline-slate-200 dark:outline-slate-200/20 focus:outline-none focus:outline-0 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:ring-2 focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
class="block w-full border-none shadow-sm appearance-none rounded-xl outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
@@ -48,6 +53,10 @@ export default {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<textarea
|
||||
:id="name"
|
||||
:name="name"
|
||||
autocomplete="off"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:rows="rows"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600': hasError,
|
||||
'dark:outline-slate-600 dark:focus:outline-woot-500 outline-slate-200 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'resize-none': !allowResize,
|
||||
}"
|
||||
class="block w-full p-3 border-none rounded-xl shadow-sm appearance-none outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 bg-white dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</with-label>
|
||||
</template>
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
allowResize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dataTestid: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -9,6 +9,7 @@
|
||||
<slot name="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
<slot name="rightOfLabel" />
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center relative w-full">
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
name="email_address"
|
||||
type="text"
|
||||
data-testid="email_input"
|
||||
:tabindex="1"
|
||||
required
|
||||
:label="$t('LOGIN.EMAIL.LABEL')"
|
||||
:placeholder="$t('LOGIN.EMAIL.PLACEHOLDER')"
|
||||
@@ -58,19 +59,25 @@
|
||||
name="password"
|
||||
data-testid="password_input"
|
||||
required
|
||||
:tabindex="2"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('LOGIN.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.password.$error"
|
||||
@input="$v.credentials.password.$touch"
|
||||
>
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link to="auth/reset/password" class="text-link">
|
||||
<router-link
|
||||
to="auth/reset/password"
|
||||
class="text-link text-sm"
|
||||
tabindex="4"
|
||||
>
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</form-input>
|
||||
<submit-button
|
||||
:disabled="loginApi.showLoading"
|
||||
:tabindex="3"
|
||||
:button-text="$t('LOGIN.SUBMIT')"
|
||||
:loading="loginApi.showLoading"
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
class Account::ContactsExportJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(account_id, column_names)
|
||||
def perform(account_id, column_names, email_to)
|
||||
account = Account.find(account_id)
|
||||
headers = valid_headers(column_names)
|
||||
generate_csv(account, headers)
|
||||
file_url = account_contact_export_url(account)
|
||||
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url)&.deliver_later
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url, email_to)&.deliver_later
|
||||
end
|
||||
|
||||
def generate_csv(account, headers)
|
||||
|
||||
@@ -2,9 +2,24 @@ class Notification::ReopenSnoozedNotificationsJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
Notification.where(snoozed_until: 3.days.ago..Time.current)
|
||||
.update_all(snoozed_until: nil, updated_at: Time.current, last_activity_at: Time.current)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
Notification.where(snoozed_until: 3.days.ago..Time.current).find_in_batches(batch_size: 100) do |notifications_batch|
|
||||
notifications_batch.each do |notification|
|
||||
update_notification(notification)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_notification(notification)
|
||||
updated_meta = (notification.meta || {}).merge('last_snoozed_at' => notification.snoozed_until)
|
||||
|
||||
notification.update!(
|
||||
snoozed_until: nil,
|
||||
updated_at: Time.current,
|
||||
last_activity_at: Time.current,
|
||||
meta: updated_meta,
|
||||
read_at: nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,12 +60,13 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def contact_export_complete(file_url)
|
||||
def contact_export_complete(file_url, email_to)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@action_url = file_url
|
||||
subject = "Your contact's export file is available to download."
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
|
||||
send_mail_with_liquid(to: email_to, subject: subject) and return
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -111,11 +111,12 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def inbound_email_domain
|
||||
domain || GlobalConfig.get('MAILER_INBOUND_EMAIL_DOMAIN')['MAILER_INBOUND_EMAIL_DOMAIN'] || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN', false)
|
||||
domain.presence || GlobalConfig.get('MAILER_INBOUND_EMAIL_DOMAIN')['MAILER_INBOUND_EMAIL_DOMAIN'] || ENV.fetch('MAILER_INBOUND_EMAIL_DOMAIN',
|
||||
false)
|
||||
end
|
||||
|
||||
def support_email
|
||||
super || ENV.fetch('MAILER_SENDER_EMAIL') { GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] }
|
||||
super.presence || ENV.fetch('MAILER_SENDER_EMAIL') { GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] }
|
||||
end
|
||||
|
||||
def usage_limits
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# last_activity_at :datetime
|
||||
# meta :jsonb
|
||||
# notification_type :integer not null
|
||||
# primary_actor_type :string not null
|
||||
# read_at :datetime
|
||||
@@ -63,24 +64,17 @@ class Notification < ApplicationRecord
|
||||
created_at: created_at.to_i,
|
||||
last_activity_at: last_activity_at.to_i,
|
||||
snoozed_until: snoozed_until,
|
||||
meta: meta,
|
||||
account_id: account_id
|
||||
|
||||
}
|
||||
if primary_actor.present?
|
||||
payload[:primary_actor] = primary_actor_data
|
||||
payload[:primary_actor] = primary_actor&.push_event_data
|
||||
payload[:push_message_title] = push_message_title
|
||||
end
|
||||
payload
|
||||
end
|
||||
|
||||
def primary_actor_data
|
||||
{
|
||||
id: primary_actor.push_event_data[:id],
|
||||
meta: primary_actor.push_event_data[:meta],
|
||||
inbox_id: primary_actor.push_event_data[:inbox_id]
|
||||
}
|
||||
end
|
||||
|
||||
def fcm_push_data
|
||||
{
|
||||
id: id,
|
||||
|
||||
@@ -3,6 +3,7 @@ class ActionService
|
||||
|
||||
def initialize(conversation)
|
||||
@conversation = conversation.reload
|
||||
@account = @conversation.account
|
||||
end
|
||||
|
||||
def mute_conversation(_params)
|
||||
|
||||
@@ -21,6 +21,7 @@ json.data do
|
||||
json.created_at notification.created_at.to_i
|
||||
json.last_activity_at notification.last_activity_at.to_i
|
||||
json.snoozed_until notification.snoozed_until
|
||||
json.meta notification.meta
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,11 @@ if resource.custom_attributes.present?
|
||||
json.subscribed_quantity resource.custom_attributes['subscribed_quantity']
|
||||
json.subscription_status resource.custom_attributes['subscription_status']
|
||||
json.subscription_ends_on resource.custom_attributes['subscription_ends_on']
|
||||
json.industry resource.custom_attributes['industry'] if resource.custom_attributes['industry'].present?
|
||||
json.company_size resource.custom_attributes['company_size'] if resource.custom_attributes['company_size'].present?
|
||||
json.timezone resource.custom_attributes['timezone'] if resource.custom_attributes['timezone'].present?
|
||||
json.logo resource.custom_attributes['logo'] if resource.custom_attributes['logo'].present?
|
||||
json.onboarding_step resource.custom_attributes['onboarding_step'] if resource.custom_attributes['onboarding_step'].present?
|
||||
end
|
||||
end
|
||||
json.domain @account.domain
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '3.5.2'
|
||||
version: '3.6.0'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -110,6 +110,11 @@
|
||||
display_title: 'Instagram Verify Token'
|
||||
description: 'The verify token used for Instagram Webhook'
|
||||
locked: false
|
||||
- name: FACEBOOK_API_VERSION
|
||||
display_title: 'Facebook API Version'
|
||||
description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`'
|
||||
value: 'v17.0'
|
||||
locked: false
|
||||
- name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT
|
||||
display_title: 'Enable human agent'
|
||||
value: false
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddMetaToNotifications < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :notifications, :meta, :jsonb, default: {}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
class AddUniqueIndexToAppliedSlas < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_index :applied_slas,
|
||||
[:account_id, :sla_policy_id, :conversation_id],
|
||||
unique: true,
|
||||
name: 'index_applied_slas_on_account_sla_policy_conversation'
|
||||
end
|
||||
end
|
||||
+3
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_02_07_103014) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_02_16_055809) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -122,6 +122,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_07_103014) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "sla_status", default: 0
|
||||
t.index ["account_id", "sla_policy_id", "conversation_id"], name: "index_applied_slas_on_account_sla_policy_conversation", unique: true
|
||||
t.index ["account_id"], name: "index_applied_slas_on_account_id"
|
||||
t.index ["conversation_id"], name: "index_applied_slas_on_conversation_id"
|
||||
t.index ["sla_policy_id"], name: "index_applied_slas_on_sla_policy_id"
|
||||
@@ -750,6 +751,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_07_103014) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "snoozed_until"
|
||||
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }
|
||||
t.jsonb "meta", default: {}
|
||||
t.index ["account_id"], name: "index_notifications_on_account_id"
|
||||
t.index ["last_activity_at"], name: "index_notifications_on_last_activity_at"
|
||||
t.index ["primary_actor_type", "primary_actor_id"], name: "uniq_primary_actor_per_account_notifications"
|
||||
|
||||
@@ -2,12 +2,11 @@ module Enterprise::Api::V2::AccountsController
|
||||
private
|
||||
|
||||
def fetch_account_and_user_info
|
||||
data = fetch_from_clearbit
|
||||
@data = fetch_from_clearbit
|
||||
|
||||
return if data.blank?
|
||||
return if @data.blank?
|
||||
|
||||
update_user_info(data)
|
||||
update_account_info(data)
|
||||
update_user_info
|
||||
end
|
||||
|
||||
def fetch_from_clearbit
|
||||
@@ -17,19 +16,25 @@ module Enterprise::Api::V2::AccountsController
|
||||
nil
|
||||
end
|
||||
|
||||
def update_user_info(data)
|
||||
@user.update!(name: data[:name])
|
||||
def update_user_info
|
||||
@user.update!(name: @data[:name]) if @data[:name].present?
|
||||
end
|
||||
|
||||
def update_account_info(data)
|
||||
@account.update!(
|
||||
name: data[:company_name],
|
||||
custom_attributes: @account.custom_attributes.merge(
|
||||
'industry' => data[:industry],
|
||||
'company_size' => data[:company_size],
|
||||
'timezone' => data[:timezone],
|
||||
'logo' => data[:logo]
|
||||
)
|
||||
def data_from_clearbit
|
||||
return {} if @data.blank?
|
||||
|
||||
{ name: @data[:company_name],
|
||||
custom_attributes: {
|
||||
'industry' => @data[:industry],
|
||||
'company_size' => @data[:company_size],
|
||||
'timezone' => @data[:timezone],
|
||||
'logo' => @data[:logo]
|
||||
} }
|
||||
end
|
||||
|
||||
def account_attributes
|
||||
super.deep_merge(
|
||||
data_from_clearbit
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class Sla::TriggerSlasForAccountsJob < ApplicationJob
|
||||
queue_as :scheduled_jobs
|
||||
|
||||
def perform
|
||||
Account.find_each do |account|
|
||||
Account.joins(:sla_policies).distinct.find_each do |account|
|
||||
Rails.logger.info "Enqueuing ProcessAccountAppliedSlasJob for account #{account.id}"
|
||||
Sla::ProcessAccountAppliedSlasJob.perform_later(account)
|
||||
end
|
||||
|
||||
@@ -12,14 +12,17 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_applied_slas_on_account_id (account_id)
|
||||
# index_applied_slas_on_conversation_id (conversation_id)
|
||||
# index_applied_slas_on_sla_policy_id (sla_policy_id)
|
||||
# index_applied_slas_on_account_id (account_id)
|
||||
# index_applied_slas_on_account_sla_policy_conversation (account_id,sla_policy_id,conversation_id) UNIQUE
|
||||
# index_applied_slas_on_conversation_id (conversation_id)
|
||||
# index_applied_slas_on_sla_policy_id (sla_policy_id)
|
||||
#
|
||||
class AppliedSla < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :sla_policy
|
||||
belongs_to :conversation
|
||||
|
||||
validates :account_id, uniqueness: { scope: %i[sla_policy_id conversation_id] }
|
||||
|
||||
enum sla_status: { active: 0, hit: 1, missed: 2 }
|
||||
end
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
module Enterprise::ActionService
|
||||
def add_sla(sla_policy)
|
||||
def add_sla(sla_policy_id)
|
||||
return if sla_policy_id.blank?
|
||||
|
||||
sla_policy = @account.sla_policies.find_by(id: sla_policy_id.first)
|
||||
return if sla_policy.nil?
|
||||
return if @conversation.sla_policy.present?
|
||||
|
||||
Rails.logger.info "SLA:: Adding SLA #{sla_policy.id} to conversation: #{@conversation.id}"
|
||||
@conversation.update!(sla_policy_id: sla_policy.id)
|
||||
create_applied_sla(sla_policy)
|
||||
end
|
||||
|
||||
def create_applied_sla(sla_policy)
|
||||
Rails.logger.info "SLA:: Creating Applied SLA for conversation: #{@conversation.id}"
|
||||
AppliedSla.create!(
|
||||
account_id: @conversation.account_id,
|
||||
sla_policy_id: sla_policy.id,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "3.5.2",
|
||||
"version": "3.6.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
|
||||
@@ -50,8 +50,11 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['payload'].first['email']).to eq(contact.email)
|
||||
expect(response_body['payload'].first['contact_inboxes'].blank?).to be(true)
|
||||
|
||||
contact_emails = response_body['payload'].pluck('email')
|
||||
contact_inboxes = response_body['payload'].pluck('contact_inboxes').flatten.compact
|
||||
expect(contact_emails).to include(contact.email)
|
||||
expect(contact_inboxes).to eq([])
|
||||
end
|
||||
|
||||
it 'returns all contacts with company name desc order' do
|
||||
@@ -194,7 +197,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'enqueues a contact export job' do
|
||||
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, nil).once
|
||||
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, nil, admin.email).once
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/contacts/export",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -204,7 +207,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||
end
|
||||
|
||||
it 'enqueues a contact export job with sent_columns' do
|
||||
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, %w[phone_number email]).once
|
||||
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, %w[phone_number email], admin.email).once
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/contacts/export",
|
||||
headers: admin.create_new_auth_token,
|
||||
|
||||
@@ -29,6 +29,7 @@ RSpec.describe 'Notifications API', type: :request do
|
||||
expect(response_json['data']['meta']['count']).to eq 2
|
||||
# notification appear in descending order
|
||||
expect(response_json['data']['payload'].first['id']).to eq notification2.id
|
||||
expect(response_json['data']['payload'].first['primary_actor']).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -178,6 +179,7 @@ RSpec.describe 'Notifications API', type: :request do
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(notification.reload.snoozed_until).not_to eq('')
|
||||
expect(notification.reload.meta['last_snoozed_at']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -213,6 +213,25 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates onboarding step to invite_team if onboarding step is present in account custom attributes' do
|
||||
account.update(custom_attributes: { onboarding_step: 'account_update' })
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team')
|
||||
end
|
||||
|
||||
it 'will not update onboarding step if onboarding step is not present in account custom attributes' do
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to be_nil
|
||||
end
|
||||
|
||||
it 'Throws error 422' do
|
||||
params[:name] = 'test' * 999
|
||||
|
||||
|
||||
@@ -30,6 +30,20 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the onboarding step in custom attributes' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
|
||||
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to eq('profile_update')
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls ChatwootCaptcha' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
captcha = double
|
||||
|
||||
@@ -41,5 +41,15 @@ RSpec.describe 'Agents API', type: :request do
|
||||
expect(response.body).to include('Account limit exceeded. Please purchase more licenses')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when onboarding step is present in account custom attributes' do
|
||||
it 'removes onboarding step from account custom attributes' do
|
||||
account.update(custom_attributes: { onboarding_step: 'completed' })
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token
|
||||
|
||||
expect(account.reload.custom_attributes).not_to include('onboarding_step')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,6 +51,22 @@ RSpec.describe Enterprise::Api::V2::AccountsController, type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the onboarding step in custom attributes' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
params = { email: email, user: nil, locale: nil, password: 'Password1!' }
|
||||
|
||||
post api_v2_accounts_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
custom_attributes = account.custom_attributes
|
||||
|
||||
expect(custom_attributes['onboarding_step']).to eq('profile_update')
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles errors when fetching data from clearbit' do
|
||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Sla::TriggerSlasForAccountsJob do
|
||||
context 'when perform is called' do
|
||||
let(:account) { create(:account) }
|
||||
let(:account_with_sla) { create(:account) }
|
||||
let(:account_without_sla) { create(:account) }
|
||||
|
||||
before do
|
||||
create(:sla_policy, account: account_with_sla)
|
||||
end
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||
.on_queue('scheduled_jobs')
|
||||
end
|
||||
|
||||
it 'calls the ProcessAccountAppliedSlasJob' do
|
||||
expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account).and_call_original
|
||||
it 'calls the ProcessAccountAppliedSlasJob for accounts with SLA' do
|
||||
expect(Sla::ProcessAccountAppliedSlasJob).to receive(:perform_later).with(account_with_sla).and_call_original
|
||||
described_class.perform_now
|
||||
end
|
||||
|
||||
it 'does not call the ProcessAccountAppliedSlasJob for accounts without SLA' do
|
||||
expect(Sla::ProcessAccountAppliedSlasJob).not_to receive(:perform_later).with(account_without_sla)
|
||||
described_class.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,16 +8,42 @@ describe ActionService do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
it 'adds the sla policy to the conversation and create applied_sla entry' do
|
||||
action_service.add_sla(sla_policy)
|
||||
expect(conversation.reload.sla_policy_id).to eq(sla_policy.id)
|
||||
context 'when sla_policy_id is present' do
|
||||
it 'adds the sla policy to the conversation and create applied_sla entry' do
|
||||
action_service.add_sla([sla_policy.id])
|
||||
expect(conversation.reload.sla_policy_id).to eq(sla_policy.id)
|
||||
|
||||
# check if appliedsla table entry is created with matching attributes
|
||||
applied_sla = AppliedSla.last
|
||||
expect(applied_sla.account_id).to eq(account.id)
|
||||
expect(applied_sla.sla_policy_id).to eq(sla_policy.id)
|
||||
expect(applied_sla.conversation_id).to eq(conversation.id)
|
||||
expect(applied_sla.sla_status).to eq('active')
|
||||
# check if appliedsla table entry is created with matching attributes
|
||||
applied_sla = AppliedSla.last
|
||||
expect(applied_sla.account_id).to eq(account.id)
|
||||
expect(applied_sla.sla_policy_id).to eq(sla_policy.id)
|
||||
expect(applied_sla.conversation_id).to eq(conversation.id)
|
||||
expect(applied_sla.sla_status).to eq('active')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sla_policy_id is not present' do
|
||||
it 'does not add the sla policy to the conversation' do
|
||||
action_service.add_sla(nil)
|
||||
expect(conversation.reload.sla_policy_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation already has a sla policy' do
|
||||
it 'does not add the new sla policy to the conversation' do
|
||||
existing_sla_policy = sla_policy
|
||||
new_sla_policy = create(:sla_policy, account: account)
|
||||
conversation.update!(sla_policy_id: existing_sla_policy.id)
|
||||
action_service.add_sla([new_sla_policy.id])
|
||||
expect(conversation.reload.sla_policy_id).to eq(existing_sla_policy.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sla_policy is not found' do
|
||||
it 'does not add the sla policy to the conversation' do
|
||||
action_service.add_sla([sla_policy.id + 1])
|
||||
expect(conversation.reload.sla_policy_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,5 +6,15 @@ FactoryBot.define do
|
||||
notification_type { 'conversation_assignment' }
|
||||
user
|
||||
account
|
||||
read_at { nil }
|
||||
snoozed_until { nil }
|
||||
end
|
||||
|
||||
trait :read do
|
||||
read_at { DateTime.now.utc - 3.days }
|
||||
end
|
||||
|
||||
trait :snoozed do
|
||||
snoozed_until { DateTime.now.utc + 3.days }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -146,6 +146,30 @@ describe ConversationFinder do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with updated_within' do
|
||||
let(:params) { { updated_within: 20, assignee_type: 'unassigned', sort_by: 'created_at_asc' } }
|
||||
|
||||
it 'filters based on params, sort order but returns all conversations without pagination with in time range' do
|
||||
# value of updated_within is in seconds
|
||||
# write spec based on that
|
||||
conversations = create_list(:conversation, 50, account: account,
|
||||
inbox: inbox, assignee: nil,
|
||||
updated_at: Time.now.utc - 30.seconds,
|
||||
created_at: Time.now.utc - 30.seconds)
|
||||
# update updated_at of 27 conversations to be with in 20 seconds
|
||||
conversations[0..27].each do |conversation|
|
||||
conversation.update(updated_at: Time.now.utc - 10.seconds)
|
||||
end
|
||||
result = conversation_finder.perform
|
||||
# pagination is not applied
|
||||
# filters are applied
|
||||
# modified conversations + 1 conversation created during set up
|
||||
expect(result[:conversations].length).to be 29
|
||||
# ensure that the conversations are sorted by created_at
|
||||
expect(result[:conversations].first.created_at).to be < result[:conversations].last.created_at
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
let(:params) { { status: 'open', assignee_type: 'me', page: 1 } }
|
||||
|
||||
|
||||
@@ -1,103 +1,79 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe NotificationFinder do
|
||||
subject(:notification_finder) { described_class.new(user, account, params) }
|
||||
|
||||
RSpec.describe NotificationFinder do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let(:notification_finder) { described_class.new(user, account, params) }
|
||||
|
||||
before do
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 1.day, last_activity_at: DateTime.now.utc + 1.day,
|
||||
read_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, snoozed_until: DateTime.now.utc + 3.days, updated_at: DateTime.now.utc,
|
||||
last_activity_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 2.days, last_activity_at: DateTime.now.utc + 2.days)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 4.days, last_activity_at: DateTime.now.utc + 4.days,
|
||||
notification_type: :conversation_creation, read_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 5.days, last_activity_at: DateTime.now.utc + 5.days,
|
||||
notification_type: :conversation_mention)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 6.days, last_activity_at: DateTime.now.utc + 6.days,
|
||||
notification_type: :participating_conversation_new_message)
|
||||
create(:notification, :snoozed, account: account, user: user)
|
||||
create_list(:notification, 2, :read, account: account, user: user)
|
||||
create_list(:notification, 3, account: account, user: user)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when params are empty' do
|
||||
describe '#notifications' do
|
||||
subject { notification_finder.notifications }
|
||||
|
||||
context 'with default params (empty)' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'returns all the notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 6
|
||||
end
|
||||
|
||||
it 'orders notifications by last activity at' do
|
||||
result = notification_finder.perform
|
||||
expect(result.first.last_activity_at).to be > result.last.last_activity_at
|
||||
end
|
||||
|
||||
it 'returns unread count' do
|
||||
result = notification_finder.unread_count
|
||||
expect(result).to be 4
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 6
|
||||
it 'returns all unread and unsnoozed notifications, ordered by last activity' do
|
||||
expect(subject.size).to eq(3)
|
||||
expect(subject).to match_array(subject.sort_by(&:last_activity_at).reverse)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when snoozed param is passed' do
|
||||
let(:params) { { status: 'snoozed' } }
|
||||
context 'with params including read and snoozed statuses' do
|
||||
let(:params) { { includes: %w[read snoozed] } }
|
||||
|
||||
it 'returns only snoozed notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 1
|
||||
end
|
||||
|
||||
it 'returns unread count' do
|
||||
result = notification_finder.unread_count
|
||||
expect(result).to be 1
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 1
|
||||
it 'returns all notifications, including read and snoozed' do
|
||||
expect(subject.size).to eq(6)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type read param is passed' do
|
||||
let(:params) { { type: 'read' } }
|
||||
context 'with params including only read status' do
|
||||
let(:params) { { includes: ['read'] } }
|
||||
|
||||
it 'returns only read notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 2
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 2
|
||||
it 'returns all notifications expect the snoozed' do
|
||||
expect(subject.size).to eq(5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type read and snoozed param is passed' do
|
||||
let(:params) { { type: 'read', status: 'snoozed' } }
|
||||
context 'with params including only snoozed status' do
|
||||
let(:params) { { includes: ['snoozed'] } }
|
||||
|
||||
it 'returns only read notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 0
|
||||
end
|
||||
|
||||
it 'returns count' do
|
||||
result = notification_finder.count
|
||||
expect(result).to be 0
|
||||
it 'rreturns all notifications only expect the read' do
|
||||
expect(subject.size).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sort order is passed' do
|
||||
context 'with ascending sort order' do
|
||||
let(:params) { { sort_order: :asc } }
|
||||
|
||||
it 'returns notifications in ascending order' do
|
||||
result = notification_finder.perform
|
||||
expect(result.first.last_activity_at).to be < result.last.last_activity_at
|
||||
it 'returns notifications in ascending order by last activity' do
|
||||
expect(subject.first.last_activity_at).to be < subject.last.last_activity_at
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'counts' do
|
||||
subject { notification_finder }
|
||||
|
||||
context 'without specific filters' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'correctly reports unread and total counts' do
|
||||
expect(subject.unread_count).to eq(3)
|
||||
expect(subject.count).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filters applied' do
|
||||
let(:params) { { includes: %w[read snoozed] } }
|
||||
|
||||
it 'adjusts counts based on included statuses' do
|
||||
expect(subject.unread_count).to eq(4)
|
||||
expect(subject.count).to eq(6)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,17 +24,17 @@ RSpec.describe Account::ContactsExportJob do
|
||||
allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).with(account: account).and_return(mailer)
|
||||
allow(mailer).to receive(:contact_export_complete)
|
||||
|
||||
described_class.perform_now(account.id, [])
|
||||
described_class.perform_now(account.id, [], 'test@test.com')
|
||||
|
||||
file_url = Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export)
|
||||
|
||||
expect(account.contacts_export).to be_present
|
||||
expect(file_url).to be_present
|
||||
expect(mailer).to have_received(:contact_export_complete).with(file_url)
|
||||
expect(mailer).to have_received(:contact_export_complete).with(file_url, 'test@test.com')
|
||||
end
|
||||
|
||||
it 'generates valid data export file' do
|
||||
described_class.perform_now(account.id, [])
|
||||
described_class.perform_now(account.id, [], 'test@test.com')
|
||||
|
||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||
emails = csv_data.pluck('email')
|
||||
|
||||
@@ -14,9 +14,13 @@ RSpec.describe Notification::ReopenSnoozedNotificationsJob do
|
||||
it 'reopens snoozed notifications whose snooze until has passed' do
|
||||
described_class.perform_now
|
||||
|
||||
snoozed_until = snoozed_till_5_minutes_ago.reload.snoozed_until
|
||||
|
||||
expect(snoozed_till_5_minutes_ago.reload.snoozed_until).to be_nil
|
||||
expect(snoozed_till_tomorrow.reload.snoozed_until.to_date).to eq 1.day.from_now.to_date
|
||||
expect(snoozed_indefinitely.reload.snoozed_until).to be_nil
|
||||
expect(snoozed_indefinitely.reload.read_at).to be_nil
|
||||
expect(snoozed_until).to eq(snoozed_till_5_minutes_ago.reload.meta['snoozed_until'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,6 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do
|
||||
before do
|
||||
allow(described_class).to receive(:new).and_return(class_instance)
|
||||
allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
Account::ContactsExportJob.perform_now(account.id, [])
|
||||
end
|
||||
|
||||
describe 'slack_disconnect' do
|
||||
@@ -92,8 +91,8 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do
|
||||
end
|
||||
|
||||
describe 'contact_export_complete' do
|
||||
let!(:file_url) { Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export) }
|
||||
let(:mail) { described_class.with(account: account).contact_export_complete(file_url).deliver_now }
|
||||
let!(:file_url) { 'http://test.com/test' }
|
||||
let(:mail) { described_class.with(account: account).contact_export_complete(file_url, administrator.email).deliver_now }
|
||||
|
||||
it 'renders the subject' do
|
||||
expect(mail.subject).to eq("Your contact's export file is available to download.")
|
||||
|
||||
@@ -47,6 +47,56 @@ RSpec.describe Account do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'inbound_email_domain' do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
it 'returns the domain from inbox if inbox value is present' do
|
||||
account.update(domain: 'test.com')
|
||||
with_modified_env MAILER_INBOUND_EMAIL_DOMAIN: 'test2.com' do
|
||||
expect(account.inbound_email_domain).to eq('test.com')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the domain from ENV if inbox value is nil' do
|
||||
account.update(domain: nil)
|
||||
with_modified_env MAILER_INBOUND_EMAIL_DOMAIN: 'test.com' do
|
||||
expect(account.inbound_email_domain).to eq('test.com')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the domain from ENV if inbox value is empty string' do
|
||||
account.update(domain: '')
|
||||
with_modified_env MAILER_INBOUND_EMAIL_DOMAIN: 'test.com' do
|
||||
expect(account.inbound_email_domain).to eq('test.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'support_email' do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
it 'returns the support email from inbox if inbox value is present' do
|
||||
account.update(support_email: 'support@chatwoot.com')
|
||||
with_modified_env MAILER_SENDER_EMAIL: 'hello@chatwoot.com' do
|
||||
expect(account.support_email).to eq('support@chatwoot.com')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the support email from ENV if inbox value is nil' do
|
||||
account.update(support_email: nil)
|
||||
with_modified_env MAILER_SENDER_EMAIL: 'hello@chatwoot.com' do
|
||||
expect(account.support_email).to eq('hello@chatwoot.com')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the support email from ENV if inbox value is empty string' do
|
||||
account.update(support_email: '')
|
||||
with_modified_env MAILER_SENDER_EMAIL: 'hello@chatwoot.com' do
|
||||
expect(account.support_email).to eq('hello@chatwoot.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when after_destroy is called' do
|
||||
it 'conv_dpid_seq and camp_dpid_seq_ are deleted' do
|
||||
account = create(:account)
|
||||
|
||||
Reference in New Issue
Block a user