Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
935b394fb5 | ||
|
|
a5c8c790e9 | ||
|
|
a95ea1a5a4 | ||
|
|
4a692e7e3a | ||
|
|
74dc1ef2ae | ||
|
|
723763b54d | ||
|
|
a4ec09a744 | ||
|
|
e847c34380 | ||
|
|
023d3e0821 | ||
|
|
63e407b1c5 | ||
|
|
717e744b08 | ||
|
|
aefe3a1b58 | ||
|
|
eab8d8d0fb | ||
|
|
dd78655181 | ||
|
|
831ac02d82 | ||
|
|
a0d85557e4 | ||
|
|
fb22659ec8 | ||
|
|
23ac2d9e4b | ||
|
|
497471632b | ||
|
|
903ec124f6 | ||
|
|
902f3aa926 | ||
|
|
4b0345a577 | ||
|
|
a2dab6a3a8 | ||
|
|
d278107133 | ||
|
|
efb18c1b64 | ||
|
|
4c86d56200 | ||
|
|
70e1bf8d31 | ||
|
|
e8f5e35dc9 | ||
|
|
df0a7a348b | ||
|
|
6550287f97 | ||
|
|
cfb1ae0dd1 | ||
|
|
95d2a57a81 | ||
|
|
6b86b6f87d | ||
|
|
b5d655171e | ||
|
|
4263ea7db1 | ||
|
|
3552eede47 | ||
|
|
24825a070a | ||
|
|
7767c83206 | ||
|
|
64324d239e | ||
|
|
bd55bb1f08 | ||
|
|
ca8dc3760f | ||
|
|
6471db4ba7 | ||
|
|
d7e7e4d8c9 | ||
|
|
d169d97987 | ||
|
|
480888e115 | ||
|
|
1fb84c6402 | ||
|
|
2c2e000264 | ||
|
|
3376183b03 | ||
|
|
8159aad772 | ||
|
|
c26e0cc7e0 | ||
|
|
2378efd853 | ||
|
|
b40c3110aa | ||
|
|
3d1bafa3c0 | ||
|
|
08c487d1c3 | ||
|
|
0e11dac471 | ||
|
|
e992c23d03 | ||
|
|
669a138114 | ||
|
|
31732a1a7b | ||
|
|
b663f3201c | ||
|
|
01ddb61c9e | ||
|
|
87e8e342c9 | ||
|
|
27ec845faa | ||
|
|
91cefe2072 | ||
|
|
d356067300 | ||
|
|
aa3f2b56bc | ||
|
|
bc5449f208 | ||
|
|
e1bfd9a17a | ||
|
|
4825ab1a29 | ||
|
|
7efc2b9325 | ||
|
|
4a491d8a91 | ||
|
|
f2db469c75 | ||
|
|
5d48c2b1e5 | ||
|
|
ecce6e021b | ||
|
|
4d47fc1c25 | ||
|
|
b25e395bf7 | ||
|
|
12b3323364 | ||
|
|
793420c341 | ||
|
|
775e8251a6 | ||
|
|
4645e57e9a | ||
|
|
5e1dbfb64d | ||
|
|
df29b43540 | ||
|
|
3e4f43d7da | ||
|
|
2b7c972cbf | ||
|
|
429cfd0166 | ||
|
|
fc41669178 | ||
|
|
e567be8fb2 | ||
|
|
1ddc05dadb | ||
|
|
0268d544d1 | ||
|
|
2960bcdf7d | ||
|
|
3c4a7bb9f7 | ||
|
|
792bc5cf9d | ||
|
|
bf40242497 | ||
|
|
2130a2ebe1 | ||
|
|
2bad7f435a | ||
|
|
c0d985a62b | ||
|
|
5b7af73abe | ||
|
|
2ae79bd9a1 | ||
|
|
17b61ad2b4 | ||
|
|
e098e463ad | ||
|
|
b11f4d5bd5 | ||
|
|
0a45b7e156 | ||
|
|
ee184d9792 | ||
|
|
3595903fb5 | ||
|
|
68a7fce45c | ||
|
|
8e5cabd14d | ||
|
|
dd2938dc03 | ||
|
|
0bf4fc9e21 | ||
|
|
8f7162d6c1 | ||
|
|
243834c401 | ||
|
|
1bba61a758 | ||
|
|
e58f1ad98d | ||
|
|
7e61397186 | ||
|
|
c5e34669f6 | ||
|
|
97f96db60e | ||
|
|
0fafe37646 | ||
|
|
68cb6b19b0 | ||
|
|
ead7cc0c36 |
+6
-5
@@ -1,9 +1,4 @@
|
||||
# Learn about the various environment variables at
|
||||
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
|
||||
|
||||
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||
# Use `rake secret` to generate this variable
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
# Replace with the URL you are planning to use for your app
|
||||
@@ -189,6 +184,12 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
# SENTRY_DSN=
|
||||
|
||||
|
||||
# MICROSOFT CLARITY
|
||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||
|
||||
# GOOGLE_TAG_MANAGER
|
||||
# GOOGLE_TAG = GTM-XXXXXXX
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
|
||||
@@ -16,34 +16,15 @@ on:
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
# - name: Setup tmate session
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# if: always()
|
||||
# ## temporary fix for gh actions since node 20 installed via script is not taking effect till 30june2024
|
||||
# # ## https://github.blog/changelog/2024-05-17-updated-dates-for-actions-runner-using-node20-instead-of-node16-by-default
|
||||
# - name: Setup Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: '20'
|
||||
- name: check node version
|
||||
run: |
|
||||
node --version
|
||||
which node
|
||||
- name: check yarn version
|
||||
run: |
|
||||
yarn --version
|
||||
which yarn
|
||||
|
||||
- name: get installer
|
||||
run: |
|
||||
wget https://get.chatwoot.app/linux/install.sh
|
||||
chmod +x install.sh
|
||||
#fix for postgtres not starting automatically in gh action env
|
||||
sed -i '/function configure_db() {/a sudo service postgresql start' install.sh
|
||||
#sed -i '/function setup_chatwoot() {/a export PATH="/usr/bin/:$PATH"' install.sh
|
||||
#sed -i '/git checkout "\$BRANCH"/a export PATH="/usr/bin/:$PATH"' install.sh
|
||||
# sed -i '/function setup_chatwoot() {/a sudo chown -R chatwoot:chatwoot /home/runner/.config' install.sh
|
||||
|
||||
- name: create input file
|
||||
run: |
|
||||
@@ -63,19 +44,6 @@ jobs:
|
||||
# sudo systemctl restart chatwoot.target
|
||||
# curl http://localhost:3000/api
|
||||
|
||||
- name: check node/yarn inside chatwoot
|
||||
run: |
|
||||
sudo -i -u chatwoot << EOF
|
||||
ls
|
||||
cd chatwoot
|
||||
node --version
|
||||
yarn --versio
|
||||
yarn
|
||||
EOF
|
||||
if: always()
|
||||
- name: Setup tmate session
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
if: always()
|
||||
- name: Upload chatwoot setup log file as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
|
||||
+1
-1
@@ -555,7 +555,7 @@ GEM
|
||||
rack (2.2.9)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
rack-contrib (2.4.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.0)
|
||||
rack (>= 2.0.0)
|
||||
|
||||
@@ -3,7 +3,6 @@ class DashboardController < ActionController::Base
|
||||
|
||||
before_action :set_application_pack
|
||||
before_action :set_global_config
|
||||
before_action :set_dashboard_scripts
|
||||
around_action :switch_locale
|
||||
before_action :ensure_installation_onboarding, only: [:index]
|
||||
before_action :render_hc_if_custom_domain, only: [:index]
|
||||
@@ -36,10 +35,6 @@ class DashboardController < ActionController::Base
|
||||
).merge(app_config)
|
||||
end
|
||||
|
||||
def set_dashboard_scripts
|
||||
@dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS')
|
||||
end
|
||||
|
||||
def ensure_installation_onboarding
|
||||
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
end
|
||||
|
||||
@@ -7,14 +7,9 @@ class Microsoft::CallbacksController < ApplicationController
|
||||
redirect_uri: "#{base_url}/microsoft/callback"
|
||||
)
|
||||
|
||||
inbox, already_exists = find_or_create_inbox
|
||||
inbox = find_or_create_inbox
|
||||
::Redis::Alfred.delete(users_data['email'].downcase)
|
||||
|
||||
if already_exists
|
||||
redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
else
|
||||
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
end
|
||||
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
@@ -45,17 +40,9 @@ class Microsoft::CallbacksController < ApplicationController
|
||||
|
||||
def find_or_create_inbox
|
||||
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
# we need this value to know where to redirect on sucessful processing of the callback
|
||||
channel_exists = channel_email.present?
|
||||
|
||||
channel_email ||= create_microsoft_channel_with_inbox
|
||||
update_microsoft_channel(channel_email)
|
||||
|
||||
# reauthorize channel, this code path only triggers when microsoft auth is successful
|
||||
# reauthorized will also update cache keys for the associated inbox
|
||||
channel_email.reauthorized!
|
||||
|
||||
[channel_email.inbox, channel_exists]
|
||||
channel_email.inbox
|
||||
end
|
||||
|
||||
# Fallback name, for when name field is missing from users_data
|
||||
|
||||
@@ -15,7 +15,6 @@ class ConversationApi extends ApiClient {
|
||||
teamId,
|
||||
conversationType,
|
||||
sortBy,
|
||||
updatedWithin,
|
||||
}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
@@ -27,7 +26,6 @@ class ConversationApi extends ApiClient {
|
||||
labels,
|
||||
conversation_type: conversationType,
|
||||
sort_by: sortBy,
|
||||
updated_within: updatedWithin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ describe('#ConversationAPI', () => {
|
||||
page: 1,
|
||||
labels: [],
|
||||
teamId: 1,
|
||||
updatedWithin: 20,
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
|
||||
params: {
|
||||
@@ -56,7 +55,6 @@ describe('#ConversationAPI', () => {
|
||||
assignee_type: 'me',
|
||||
page: 1,
|
||||
labels: [],
|
||||
updated_within: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import LinearAPIClient from '../../integrations/linear';
|
||||
import ApiClient from '../../ApiClient';
|
||||
|
||||
describe('#linearAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(LinearAPIClient).toBeInstanceOf(ApiClient);
|
||||
expect(LinearAPIClient).toHaveProperty('getTeams');
|
||||
expect(LinearAPIClient).toHaveProperty('getTeamEntities');
|
||||
expect(LinearAPIClient).toHaveProperty('createIssue');
|
||||
expect(LinearAPIClient).toHaveProperty('link_issue');
|
||||
expect(LinearAPIClient).toHaveProperty('getLinkedIssue');
|
||||
expect(LinearAPIClient).toHaveProperty('unlinkIssue');
|
||||
expect(LinearAPIClient).toHaveProperty('searchIssues');
|
||||
});
|
||||
|
||||
describe('getTeams', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.getTeams();
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/teams'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamEntities', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.getTeamEntities(1);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/team_entities?team_id=1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIssue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
const issueData = {
|
||||
title: 'New Issue',
|
||||
description: 'Issue description',
|
||||
};
|
||||
LinearAPIClient.createIssue(issueData);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/create_issue',
|
||||
issueData
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link_issue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.link_issue(1, 2);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/link_issue',
|
||||
{
|
||||
issue_id: 2,
|
||||
conversation_id: 1,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLinkedIssue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.getLinkedIssue(1);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/linked_issues?conversation_id=1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unlinkIssue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.unlinkIssue(1);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/unlink_issue',
|
||||
{
|
||||
link_id: 1,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchIssues', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.searchIssues('query');
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/search_issue?q=query'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -268,7 +268,6 @@ export default {
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
chatListFilters: 'getChatListFilters',
|
||||
unAssignedChatsList: 'getUnAssignedChats',
|
||||
chatListLoading: 'getChatListLoadingStatus',
|
||||
currentUserID: 'getCurrentUserID',
|
||||
@@ -294,6 +293,13 @@ export default {
|
||||
hasAppliedFiltersOrActiveFolders() {
|
||||
return this.hasAppliedFilters || this.hasActiveFolders;
|
||||
},
|
||||
savedFoldersValue() {
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
showEndOfListMessage() {
|
||||
return (
|
||||
this.conversationList.length &&
|
||||
@@ -369,6 +375,7 @@ export default {
|
||||
labels: this.label ? [this.label] : undefined,
|
||||
teamId: this.teamId || undefined,
|
||||
conversationType: this.conversationType || undefined,
|
||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||
};
|
||||
},
|
||||
conversationListPagination() {
|
||||
@@ -481,13 +488,7 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
this.updateVirtualListProps('conversationType', this.conversationType);
|
||||
},
|
||||
activeFolder(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch(
|
||||
'customViews/setActiveConversationFolder',
|
||||
newVal || null
|
||||
);
|
||||
}
|
||||
activeFolder() {
|
||||
this.resetAndFetchData();
|
||||
this.updateVirtualListProps('foldersId', this.foldersId);
|
||||
},
|
||||
@@ -497,14 +498,8 @@ export default {
|
||||
showAssigneeInConversationCard(newVal) {
|
||||
this.updateVirtualListProps('showAssignee', newVal);
|
||||
},
|
||||
conversationFilters(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch('updateChatListFilters', newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setChatListFilters', this.conversationFilters);
|
||||
this.setFiltersFromUISettings();
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||
@@ -700,9 +695,8 @@ export default {
|
||||
this.fetchConversations();
|
||||
},
|
||||
fetchConversations() {
|
||||
this.$store.dispatch('updateChatListFilters', this.conversationFilters);
|
||||
this.$store
|
||||
.dispatch('fetchAllConversations')
|
||||
.dispatch('fetchAllConversations', this.conversationFilters)
|
||||
.then(this.emitConversationLoaded);
|
||||
},
|
||||
loadMoreConversations() {
|
||||
@@ -993,7 +987,7 @@ export default {
|
||||
allSelectedConversationsStatus(status) {
|
||||
if (!this.selectedConversations.length) return false;
|
||||
return this.selectedConversations.every(item => {
|
||||
return this.$store.getters.getConversationById(item)?.status === status;
|
||||
return this.$store.getters.getConversationById(item).status === status;
|
||||
});
|
||||
},
|
||||
onContextMenuToggle(state) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
/>
|
||||
</span>
|
||||
<woot-button
|
||||
v-if="showActions && value"
|
||||
v-if="showCopyAndDeleteButton"
|
||||
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||
variant="link"
|
||||
size="medium"
|
||||
@@ -90,7 +90,7 @@
|
||||
</p>
|
||||
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
|
||||
<woot-button
|
||||
v-if="showActions && value"
|
||||
v-if="showCopyAndDeleteButton"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||
variant="link"
|
||||
size="small"
|
||||
@@ -100,7 +100,7 @@
|
||||
@click="onCopy"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-if="showEditButton"
|
||||
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||
variant="link"
|
||||
size="small"
|
||||
@@ -174,6 +174,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showCopyAndDeleteButton() {
|
||||
return this.value && this.showActions;
|
||||
},
|
||||
showEditButton() {
|
||||
return !this.value && this.showActions;
|
||||
},
|
||||
displayValue() {
|
||||
if (this.isAttributeTypeDate) {
|
||||
return this.value
|
||||
|
||||
@@ -99,9 +99,7 @@ export default {
|
||||
onMouseUp() {
|
||||
if (this.mousedDownOnBackdrop) {
|
||||
this.mousedDownOnBackdrop = false;
|
||||
if (this.closeOnBackdropClick) {
|
||||
this.onClose();
|
||||
}
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
+3
-3
@@ -65,11 +65,11 @@
|
||||
</div>
|
||||
<span
|
||||
v-if="warningIcon"
|
||||
class="inline-flex mr-1 bg-red-50 dark:bg-red-900 p-0.5 rounded-sm"
|
||||
class="inline-flex rounded-sm mr-1 bg-slate-100"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip.top-end="$t('SIDEBAR.REAUTHORIZE')"
|
||||
class="text-xxs text-red-500 dark:text-red-300"
|
||||
v-tooltip.top-end="$t('SIDEBAR.FACEBOOK_REAUTHORIZE')"
|
||||
class="text-xxs"
|
||||
:icon="warningIcon"
|
||||
size="12"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import ListItemButton from './DropdownListItemButton.vue';
|
||||
import DropdownSearch from './DropdownSearch.vue';
|
||||
import DropdownEmptyState from './DropdownEmptyState.vue';
|
||||
import DropdownLoadingState from './DropdownLoadingState.vue';
|
||||
|
||||
const props = defineProps({
|
||||
listItems: {
|
||||
@@ -28,24 +26,16 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['on-search']);
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const onSearch = debounce(value => {
|
||||
const onSearch = value => {
|
||||
searchTerm.value = value;
|
||||
emits('on-search', value);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const filteredListItems = computed(() => {
|
||||
if (!searchTerm.value) return props.listItems;
|
||||
@@ -60,16 +50,6 @@ const isFilterActive = id => {
|
||||
if (!props.activeFilterId) return false;
|
||||
return id === props.activeFilterId;
|
||||
};
|
||||
|
||||
const shouldShowLoadingState = computed(() => {
|
||||
return (
|
||||
props.isLoading && isDropdownListEmpty.value && props.loadingPlaceholder
|
||||
);
|
||||
});
|
||||
|
||||
const shouldShowEmptyState = computed(() => {
|
||||
return !props.isLoading && isDropdownListEmpty.value;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
@@ -87,12 +67,8 @@ const shouldShowEmptyState = computed(() => {
|
||||
/>
|
||||
</slot>
|
||||
<slot name="listItem">
|
||||
<dropdown-loading-state
|
||||
v-if="shouldShowLoadingState"
|
||||
:message="loadingPlaceholder"
|
||||
/>
|
||||
<dropdown-empty-state
|
||||
v-else-if="shouldShowEmptyState"
|
||||
v-if="isDropdownListEmpty"
|
||||
:message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')"
|
||||
/>
|
||||
<list-item-button
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,11 +18,11 @@ defineProps({
|
||||
<div
|
||||
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<div class="flex items-center w-full gap-2" @keyup.space.prevent>
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
size="16"
|
||||
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
|
||||
size="18"
|
||||
class="text-slate-400 dark:text-slate-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ const toggleShowAllNRT = () => {
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute flex flex-col items-start bg-white dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
|
||||
class="absolute flex flex-col items-start bg-[#fdfdfd] dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
|
||||
>
|
||||
<span class="text-sm font-medium text-slate-900 dark:text-slate-25">
|
||||
{{ $t('SLA.EVENTS.TITLE') }}
|
||||
|
||||
@@ -107,7 +107,6 @@ import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import validations from './validations';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
const props = defineProps({
|
||||
accountId: {
|
||||
@@ -141,14 +140,6 @@ const priorities = [
|
||||
{ id: 4, name: 'Low' },
|
||||
];
|
||||
|
||||
const statusDesiredOrder = [
|
||||
'Backlog',
|
||||
'Todo',
|
||||
'In Progress',
|
||||
'Done',
|
||||
'Canceled',
|
||||
];
|
||||
|
||||
const isCreating = ref(false);
|
||||
const inputStyles = { borderRadius: '12px', fontSize: '14px' };
|
||||
|
||||
@@ -186,11 +177,7 @@ const getTeams = async () => {
|
||||
const response = await LinearAPI.getTeams();
|
||||
teams.value = response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -199,16 +186,12 @@ const getTeamEntities = async () => {
|
||||
const response = await LinearAPI.getTeamEntities(formState.teamId);
|
||||
assignees.value = response.data.users;
|
||||
labels.value = response.data.labels;
|
||||
statuses.value = response.data.states;
|
||||
projects.value = response.data.projects;
|
||||
statuses.value = statusDesiredOrder
|
||||
.map(name => response.data.states.find(status => status.name === name))
|
||||
.filter(Boolean);
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
useAlert(
|
||||
t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.LOADING_TEAM_ENTITIES_ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,11 +226,7 @@ const createIssue = async () => {
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS'));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_ERROR'));
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
|
||||
+8
-1
@@ -42,6 +42,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { fromUnixTime, formatDistanceToNow } from 'date-fns';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { computed, ref } from 'vue';
|
||||
import LinkIssue from './LinkIssue.vue';
|
||||
@@ -62,11 +63,17 @@ const { t } = useI18n();
|
||||
|
||||
const selectedTabIndex = ref(0);
|
||||
|
||||
const timeAgo = time => {
|
||||
const unixTime = fromUnixTime(time);
|
||||
return formatDistanceToNow(unixTime, { addSuffix: true });
|
||||
};
|
||||
const title = computed(() => {
|
||||
const { meta: { sender: { name = null } = {} } = {} } = props.conversation;
|
||||
const { meta: { sender: { name = null } = {} } = {}, created_at: createdAt } =
|
||||
props.conversation;
|
||||
return t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_TITLE', {
|
||||
conversationId: props.conversation.id,
|
||||
name,
|
||||
timeAgo: timeAgo(createdAt),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const unlinkIssue = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute flex flex-col items-start bg-white dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
|
||||
class="absolute flex flex-col items-start bg-ash-50 dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
|
||||
>
|
||||
<div class="flex flex-col w-full">
|
||||
<issue-header
|
||||
|
||||
@@ -16,16 +16,9 @@
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="h-[24px]"
|
||||
:is-loading="isUnlinking"
|
||||
@click="unlinkIssue"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="!isUnlinking"
|
||||
icon="unlink"
|
||||
size="12"
|
||||
type="outline"
|
||||
icon-lib="lucide"
|
||||
/>
|
||||
<fluent-icon icon="unlink" size="12" type="outline" icon-lib="lucide" />
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@@ -40,7 +33,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
const props = defineProps({
|
||||
identifier: {
|
||||
type: String,
|
||||
@@ -52,8 +44,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const isUnlinking = inject('isUnlinking');
|
||||
|
||||
const emit = defineEmits(['unlink-issue']);
|
||||
|
||||
const unlinkIssue = () => {
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
:show-clear-filter="false"
|
||||
:list-items="issues"
|
||||
:active-filter-id="selectedOption.id"
|
||||
:is-loading="isFetching"
|
||||
:input-placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.SEARCH')"
|
||||
:loading-placeholder="$t('INTEGRATION_SETTINGS.LINEAR.LINK.LOADING')"
|
||||
enable-search
|
||||
class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10"
|
||||
class="left-0 flex flex-col w-full overflow-y-auto h-fit max-h-[160px] md:left-auto md:right-0 top-10"
|
||||
@on-search="onSearch"
|
||||
@click="onSelectIssue"
|
||||
/>
|
||||
@@ -52,7 +50,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -100,7 +97,6 @@ const onClose = () => {
|
||||
};
|
||||
|
||||
const onSearch = async value => {
|
||||
issues.value = [];
|
||||
if (!value) return;
|
||||
searchQuery.value = value;
|
||||
try {
|
||||
@@ -111,11 +107,7 @@ const onSearch = async value => {
|
||||
name: `${issue.identifier} ${issue.title}`,
|
||||
}));
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.LINK.ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.ERROR'));
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
@@ -131,11 +123,7 @@ const linkIssue = async () => {
|
||||
issues.value = [];
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.LINK.LINK_ERROR'));
|
||||
} finally {
|
||||
isLinking.value = false;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
<woot-modal
|
||||
:show.sync="shouldShowPopup"
|
||||
:on-close="closePopup"
|
||||
:close-on-backdrop-click="false"
|
||||
class="!items-start [&>div]:!top-12 [&>div]:sticky"
|
||||
class="!items-start [&>div]:!top-12"
|
||||
>
|
||||
<create-or-link-issue
|
||||
:conversation="conversation"
|
||||
@@ -40,18 +39,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch, defineComponent, provide } from 'vue';
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
|
||||
import Issue from './Issue.vue';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
defineComponent({
|
||||
name: 'Linear',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -66,9 +60,6 @@ const { t } = useI18n();
|
||||
const linkedIssue = ref(null);
|
||||
const shouldShow = ref(false);
|
||||
const shouldShowPopup = ref(false);
|
||||
const isUnlinking = ref(false);
|
||||
|
||||
provide('isUnlinking', isUnlinking);
|
||||
|
||||
const currentAccountId = getters.getCurrentAccountId;
|
||||
|
||||
@@ -89,28 +80,17 @@ const loadLinkedIssue = async () => {
|
||||
const issues = response.data;
|
||||
linkedIssue.value = issues && issues.length ? issues[0] : null;
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
useAlert(error?.message || t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const unlinkIssue = async linkId => {
|
||||
try {
|
||||
isUnlinking.value = true;
|
||||
await LinearAPI.unlinkIssue(linkId);
|
||||
linkedIssue.value = null;
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR')
|
||||
);
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
isUnlinking.value = false;
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||
};
|
||||
|
||||
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
||||
const allowedInboxTypes = [INBOX_TYPES.FB, INBOX_TYPES.EMAIL];
|
||||
if (allowedInboxTypes.includes(type) && reauthorizationRequired) {
|
||||
if (type === INBOX_TYPES.FB && reauthorizationRequired) {
|
||||
return 'warning';
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"NONE": "None",
|
||||
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
|
||||
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assigned successfully.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign team. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "Inboxes",
|
||||
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected, you will not receive any new messages. <a class=\"underline\" href=\"%{actionUrl}\">Click here</a> to reconnect.",
|
||||
"LIST": {
|
||||
"404": "There are no inboxes attached to this account."
|
||||
},
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
"ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"LINK_SUCCESS": "Issue linked successfully",
|
||||
"LINK_ERROR": "There was an error linking the issue, please try again",
|
||||
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
|
||||
"LINK_TITLE": "#%{conversationId}: %{name} - Created %{timeAgo}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Create/link linear issue",
|
||||
@@ -235,34 +235,22 @@
|
||||
},
|
||||
"TEAM": {
|
||||
"LABEL": "Team",
|
||||
"PLACEHOLDER": "Select team",
|
||||
"SEARCH": "Search team",
|
||||
"REQUIRED_ERROR": "Team is required"
|
||||
},
|
||||
"ASSIGNEE": {
|
||||
"LABEL": "Assignee",
|
||||
"PLACEHOLDER": "Select assignee",
|
||||
"SEARCH": "Search assignee"
|
||||
"LABEL": "Assignee"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"LABEL": "Priority",
|
||||
"PLACEHOLDER": "Select priority",
|
||||
"SEARCH": "Search priority"
|
||||
"LABEL": "Priority"
|
||||
},
|
||||
"LABEL": {
|
||||
"LABEL": "Label",
|
||||
"PLACEHOLDER": "Select label",
|
||||
"SEARCH": "Search label"
|
||||
"LABEL": "Label"
|
||||
},
|
||||
"STATUS": {
|
||||
"LABEL": "Status",
|
||||
"PLACEHOLDER": "Select status",
|
||||
"SEARCH": "Search status"
|
||||
"LABEL": "Status"
|
||||
},
|
||||
"PROJECT": {
|
||||
"LABEL": "Project",
|
||||
"PLACEHOLDER": "Select project",
|
||||
"SEARCH": "Search project"
|
||||
"LABEL": "Project"
|
||||
}
|
||||
},
|
||||
"CREATE": "Create",
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
"SLA": "SLA",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||
"HELP_CENTER": {
|
||||
"TITLE": "Help Center",
|
||||
"ALL_ARTICLES": "All Articles",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = [
|
||||
];
|
||||
|
||||
const slugifyChannel = name =>
|
||||
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
|
||||
name.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
|
||||
|
||||
export const isEditorHotKeyEnabled = (uiSettings, key) => {
|
||||
const {
|
||||
@@ -70,8 +70,6 @@ export default {
|
||||
this.updateUISettings({ [key]: !this.isContactSidebarItemOpen(key) });
|
||||
},
|
||||
setSignatureFlagForInbox(channelType, value) {
|
||||
if (!channelType) return;
|
||||
|
||||
channelType = slugifyChannel(channelType);
|
||||
this.updateUISettings({
|
||||
[`${channelType}_signature_enabled`]: value,
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import InboxCard from './components/InboxCard.vue';
|
||||
import InboxListHeader from './components/InboxListHeader.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
@@ -104,13 +103,6 @@ export default {
|
||||
return !this.uiFlags.isFetching && !this.notifications.length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inboxFilters(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch('notifications/updateNotificationFilters', newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSavedFilter();
|
||||
this.fetchNotifications();
|
||||
@@ -198,10 +190,6 @@ export default {
|
||||
this.status = status;
|
||||
this.type = type;
|
||||
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||
this.$store.dispatch(
|
||||
'notifications/setNotificationFilters',
|
||||
this.inboxFilters
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -200,7 +200,7 @@ export default {
|
||||
},
|
||||
|
||||
featureInboundEmailEnabled() {
|
||||
return !!this.features?.inbound_emails;
|
||||
return !!this.features.inbound_emails;
|
||||
},
|
||||
|
||||
featureCustomReplyDomainEnabled() {
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
</woot-tabs>
|
||||
</setting-intro-banner>
|
||||
|
||||
<microsoft-reauthorize v-if="microsoftUnauthorized" :inbox="inbox" />
|
||||
<facebook-reauthorize v-if="facebookUnauthorized" :inbox="inbox" />
|
||||
<inbox-reconnection-required
|
||||
v-if="isReconnectionRequired"
|
||||
class="mx-8 mt-5"
|
||||
/>
|
||||
|
||||
<div v-if="selectedTabKey === 'inbox_settings'" class="mx-8">
|
||||
<settings-section
|
||||
@@ -400,6 +402,7 @@
|
||||
@click="updateInbox"
|
||||
/>
|
||||
</settings-section>
|
||||
<facebook-reauthorize v-if="isAFacebookInbox" :inbox-id="inbox.id" />
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTabKey === 'collaborators'" class="mx-8">
|
||||
@@ -437,7 +440,7 @@ import WeeklyAvailability from './components/WeeklyAvailability.vue';
|
||||
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
|
||||
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
|
||||
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
|
||||
import MicrosoftReauthorize from './channels/microsoft/Reauthorize.vue';
|
||||
import InboxReconnectionRequired from './components/InboxReconnectionRequired';
|
||||
import WidgetBuilder from './WidgetBuilder.vue';
|
||||
import BotConfiguration from './components/BotConfiguration.vue';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
@@ -456,7 +459,7 @@ export default {
|
||||
WeeklyAvailability,
|
||||
WidgetBuilder,
|
||||
SenderNameExamplePreview,
|
||||
MicrosoftReauthorize,
|
||||
InboxReconnectionRequired,
|
||||
},
|
||||
mixins: [alertMixin, configMixin, inboxMixin],
|
||||
data() {
|
||||
@@ -618,11 +621,8 @@ export default {
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
microsoftUnauthorized() {
|
||||
return this.isAMicrosoftInbox && this.inbox.reauthorization_required;
|
||||
},
|
||||
facebookUnauthorized() {
|
||||
return this.isAFacebookInbox && this.inbox.reauthorization_required;
|
||||
isReconnectionRequired() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
+61
-42
@@ -1,44 +1,63 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired';
|
||||
import microsoftClient from 'dashboard/api/channel/microsoftClient';
|
||||
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const isRequestingAuthorization = ref(false);
|
||||
|
||||
async function requestAuthorization() {
|
||||
try {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await microsoftClient.generateAuthorization({
|
||||
email: props.inbox.email,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isRequestingAuthorization.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<inbox-reconnection-required
|
||||
class="mx-8 mt-5"
|
||||
@reauthorize="requestAuthorization"
|
||||
/>
|
||||
<div class="mx-8">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.MICROSOFT.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.MICROSOFT.SUBTITLE')"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<form @submit.prevent="requestAuthorization">
|
||||
<woot-submit-button
|
||||
icon="brand-twitter"
|
||||
button-text="Sign in with Microsoft"
|
||||
type="submit"
|
||||
:loading="isRequestingAuthorization"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</settings-section>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import microsoftClient from '../../../../../../api/channel/microsoftClient';
|
||||
import SettingsSection from '../../../../../../components/SettingsSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { isRequestingAuthorization: false };
|
||||
},
|
||||
methods: {
|
||||
async requestAuthorization() {
|
||||
try {
|
||||
this.isRequestingAuthorization = true;
|
||||
const response = await microsoftClient.generateAuthorization({
|
||||
email: this.inbox.email,
|
||||
});
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
this.isRequestingAuthorization = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.smtp-details-wrap {
|
||||
margin-bottom: var(--space-medium);
|
||||
}
|
||||
</style>
|
||||
|
||||
+14
-9
@@ -1,14 +1,19 @@
|
||||
<script setup>
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
defineProps({
|
||||
actionUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<banner
|
||||
color-scheme="alert"
|
||||
class="justify-start rounded-md"
|
||||
:banner-message="$t('INBOX_MGMT.RECONNECTION_REQUIRED')"
|
||||
:action-button-label="$t('INBOX_MGMT.CLICK_TO_RECONNECT')"
|
||||
has-action-button
|
||||
@click="$emit('reauthorize')"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-2 px-4 py-3 text-sm text-white bg-red-500 rounded-md dark:bg-red-800/30 dark:text-red-50 min-h-10"
|
||||
>
|
||||
<fluent-icon icon="error-circle" class="text-white dark:text-red-50" />
|
||||
<slot>
|
||||
<span v-html="$t('INBOX_MGMT.RECONNECTION_REQUIRED', { actionUrl })" />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<template>
|
||||
<inbox-reconnection-required class="mx-8 mt-5" @reauthorize="tryFBlogin" />
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.FACEBOOK_REAUTHORIZE.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.FACEBOOK_REAUTHORIZE.SUBTITLE')"
|
||||
>
|
||||
<a class="fb--login" href="#" @click="tryFBlogin">
|
||||
<img
|
||||
src="~dashboard/assets/images/channels/facebook_login.png"
|
||||
alt="Facebook-logo"
|
||||
/>
|
||||
</a>
|
||||
</settings-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global FB */
|
||||
import InboxReconnectionRequired from '../components/InboxReconnectionRequired';
|
||||
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxReconnectionRequired,
|
||||
SettingsSection,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
inboxId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inboxId() {
|
||||
return this.inbox.id;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initFB();
|
||||
this.loadFBsdk();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initFB() {
|
||||
if (window.fbSDKLoaded === undefined) {
|
||||
|
||||
+7
-1
@@ -104,6 +104,10 @@
|
||||
</div>
|
||||
<imap-settings :inbox="inbox" />
|
||||
<smtp-settings v-if="inbox.imap_enabled" :inbox="inbox" />
|
||||
<microsoft-reauthorize
|
||||
v-if="inbox.microsoft_reauthorization"
|
||||
:inbox="inbox"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
|
||||
<div v-if="inbox.provider_config" class="mx-8">
|
||||
@@ -126,7 +130,7 @@
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||
class="whatsapp-settings--content items-center flex flex-1 justify-between mt-2"
|
||||
>
|
||||
<woot-input
|
||||
v-model.trim="whatsAppInboxAPIKey"
|
||||
@@ -156,6 +160,7 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||
import ImapSettings from '../ImapSettings.vue';
|
||||
import SmtpSettings from '../SmtpSettings.vue';
|
||||
import MicrosoftReauthorize from '../channels/microsoft/Reauthorize.vue';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
@@ -163,6 +168,7 @@ export default {
|
||||
SettingsSection,
|
||||
ImapSettings,
|
||||
SmtpSettings,
|
||||
MicrosoftReauthorize,
|
||||
},
|
||||
mixins: [inboxMixin, alertMixin],
|
||||
props: {
|
||||
|
||||
+12
-14
@@ -64,19 +64,17 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
tableData() {
|
||||
return this.agentMetrics
|
||||
.filter(agentMetric => this.getAgentInformation(agentMetric.id))
|
||||
.map(agent => {
|
||||
const agentInformation = this.getAgentInformation(agent.id);
|
||||
return {
|
||||
agent: agentInformation.name || agentInformation.available_name,
|
||||
email: agentInformation.email,
|
||||
thumbnail: agentInformation.thumbnail,
|
||||
open: agent.metric.open || 0,
|
||||
unattended: agent.metric.unattended || 0,
|
||||
status: agentInformation.availability_status,
|
||||
};
|
||||
});
|
||||
return this.agentMetrics.map(agent => {
|
||||
const agentInformation = this.getAgentInformation(agent.id);
|
||||
return {
|
||||
agent: agentInformation.name,
|
||||
email: agentInformation.email,
|
||||
thumbnail: agentInformation.thumbnail,
|
||||
open: agent.metric.open || 0,
|
||||
unattended: agent.metric.unattended || 0,
|
||||
status: agentInformation.availability_status,
|
||||
};
|
||||
});
|
||||
},
|
||||
columns() {
|
||||
return [
|
||||
@@ -132,7 +130,7 @@ export default {
|
||||
this.$emit('page-change', pageIndex);
|
||||
},
|
||||
getAgentInformation(id) {
|
||||
return this.agents?.find(agent => agent.id === Number(id));
|
||||
return this.agents.find(agent => agent.id === Number(id));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,10 +37,9 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
fetchAllConversations: async ({ commit, state, dispatch }) => {
|
||||
fetchAllConversations: async ({ commit, dispatch }, params) => {
|
||||
commit(types.SET_LIST_LOADING_STATUS);
|
||||
try {
|
||||
const params = state.conversationFilters;
|
||||
const {
|
||||
data: { data },
|
||||
} = await ConversationApi.get(params);
|
||||
@@ -447,14 +446,6 @@ const actions = {
|
||||
commit(types.CLEAR_CONVERSATION_FILTERS);
|
||||
},
|
||||
|
||||
setChatListFilters({ commit }, data) {
|
||||
commit(types.SET_CHAT_LIST_FILTERS, data);
|
||||
},
|
||||
|
||||
updateChatListFilters({ commit }, data) {
|
||||
commit(types.UPDATE_CHAT_LIST_FILTERS, data);
|
||||
},
|
||||
|
||||
assignPriority: async ({ dispatch }, { conversationId, priority }) => {
|
||||
try {
|
||||
await ConversationApi.togglePriority({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { applyPageFilters, sortComparator } from './helpers';
|
||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||
|
||||
export const getSelectedChatConversation = ({
|
||||
allConversations,
|
||||
@@ -22,7 +21,6 @@ const getters = {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
return selectedChat.attachments || [];
|
||||
},
|
||||
getChatListFilters: ({ conversationFilters }) => conversationFilters,
|
||||
getLastEmailInSelectedChat: (stage, _getters) => {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
const { messages = [] } = selectedChat;
|
||||
@@ -58,10 +56,6 @@ const getters = {
|
||||
getAppliedConversationFilters: _state => {
|
||||
return _state.appliedFilters;
|
||||
},
|
||||
getAppliedConversationFiltersQuery: _state => {
|
||||
const hasAppliedFilters = _state.appliedFilters.length !== 0;
|
||||
return hasAppliedFilters ? filterQueryGenerator(_state.appliedFilters) : [];
|
||||
},
|
||||
getUnAssignedChats: _state => activeFilters => {
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const isUnAssigned = !conversation.meta.assignee;
|
||||
|
||||
@@ -19,7 +19,6 @@ const state = {
|
||||
conversationParticipants: [],
|
||||
conversationLastSeen: null,
|
||||
syncConversationsMessages: {},
|
||||
conversationFilters: {},
|
||||
};
|
||||
|
||||
// mutations
|
||||
@@ -32,20 +31,6 @@ export const mutations = {
|
||||
);
|
||||
if (indexInCurrentList < 0) {
|
||||
newAllConversations.push(conversation);
|
||||
} else if (conversation.id !== _state.selectedChatId) {
|
||||
// If the conversation is already in the list, replace it
|
||||
// Added this to fix the issue of the conversation not being updated
|
||||
// When reconnecting to the websocket. If the selectedChatId is not the same as
|
||||
// the conversation.id in the store, replace the existing conversation with the new one
|
||||
newAllConversations[indexInCurrentList] = conversation;
|
||||
} else {
|
||||
// If the conversation is already in the list and selectedChatId is the same,
|
||||
// replace all data except the messages array
|
||||
const existingConversation = newAllConversations[indexInCurrentList];
|
||||
newAllConversations[indexInCurrentList] = {
|
||||
...conversation,
|
||||
messages: existingConversation.messages,
|
||||
};
|
||||
}
|
||||
});
|
||||
_state.allConversations = newAllConversations;
|
||||
@@ -301,13 +286,6 @@ export const mutations = {
|
||||
[types.SET_CONTEXT_MENU_CHAT_ID](_state, chatId) {
|
||||
_state.contextMenuChatId = chatId;
|
||||
},
|
||||
|
||||
[types.SET_CHAT_LIST_FILTERS](_state, data) {
|
||||
_state.conversationFilters = data;
|
||||
},
|
||||
[types.UPDATE_CHAT_LIST_FILTERS](_state, data) {
|
||||
_state.conversationFilters = { ..._state.conversationFilters, ...data };
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -9,7 +9,6 @@ export const state = {
|
||||
isCreating: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
activeConversationFolder: null,
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
@@ -22,9 +21,6 @@ export const getters = {
|
||||
getCustomViewsByFilterType: _state => filterType => {
|
||||
return _state.records.filter(record => record.filter_type === filterType);
|
||||
},
|
||||
getActiveConversationFolder(_state) {
|
||||
return _state.activeConversationFolder;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
@@ -75,9 +71,6 @@ export const actions = {
|
||||
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
setActiveConversationFolder({ commit }, data) {
|
||||
commit(types.SET_ACTIVE_CONVERSATION_FOLDER, data);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -92,10 +85,6 @@ export const mutations = {
|
||||
[types.SET_CUSTOM_VIEW]: MutationHelpers.set,
|
||||
[types.UPDATE_CUSTOM_VIEW]: MutationHelpers.update,
|
||||
[types.DELETE_CUSTOM_VIEW]: MutationHelpers.destroy,
|
||||
|
||||
[types.SET_ACTIVE_CONVERSATION_FOLDER](_state, folder) {
|
||||
_state.activeConversationFolder = folder;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -159,11 +159,4 @@ export const actions = {
|
||||
clear({ commit }) {
|
||||
commit(types.CLEAR_NOTIFICATIONS);
|
||||
},
|
||||
|
||||
setNotificationFilters: ({ commit }, filters) => {
|
||||
commit(types.SET_NOTIFICATION_FILTERS, filters);
|
||||
},
|
||||
updateNotificationFilters: ({ commit }, filters) => {
|
||||
commit(types.UPDATE_NOTIFICATION_FILTERS, filters);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,4 @@ export const getters = {
|
||||
getMeta: $state => {
|
||||
return $state.meta;
|
||||
},
|
||||
getNotificationFilters($state) {
|
||||
return $state.notificationFilters;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ const state = {
|
||||
isUpdatingUnreadCount: false,
|
||||
isAllNotificationsLoaded: false,
|
||||
},
|
||||
notificationFilters: {},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -28,16 +28,6 @@ export const mutations = {
|
||||
},
|
||||
[types.SET_NOTIFICATIONS]: ($state, data) => {
|
||||
data.forEach(notification => {
|
||||
// Find existing notification with same primary_actor_id (primary_actor_id is unique)
|
||||
const existingNotification = Object.values($state.records).find(
|
||||
record => record.primary_actor_id === notification.primary_actor_id
|
||||
);
|
||||
// This is to handle the case where the same notification is received multiple times
|
||||
// On reconnect, if there is existing notification with same primary_actor_id,
|
||||
// it will be deleted and the new one will be added. So it will solve with duplicate notification
|
||||
if (existingNotification) {
|
||||
Vue.delete($state.records, existingNotification.id);
|
||||
}
|
||||
Vue.set($state.records, notification.id, {
|
||||
...($state.records[notification.id] || {}),
|
||||
...notification,
|
||||
@@ -95,14 +85,4 @@ export const mutations = {
|
||||
[types.SNOOZE_NOTIFICATION]: ($state, { id, snoozed_until }) => {
|
||||
Vue.set($state.records[id], 'snoozed_until', snoozed_until);
|
||||
},
|
||||
|
||||
[types.SET_NOTIFICATION_FILTERS]: ($state, filters) => {
|
||||
Vue.set($state, 'notificationFilters', filters);
|
||||
},
|
||||
[types.UPDATE_NOTIFICATION_FILTERS]: ($state, filters) => {
|
||||
Vue.set($state, 'notificationFilters', {
|
||||
...$state.notificationFilters,
|
||||
...filters,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -659,32 +659,4 @@ describe('#addMentions', () => {
|
||||
expect(commit.mock.calls).toEqual([[types.SET_CONTEXT_MENU_CHAT_ID, 1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setChatListFilters', () => {
|
||||
it('set chat list filters', () => {
|
||||
const filters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
actions.setChatListFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CHAT_LIST_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateChatListFilters', () => {
|
||||
it('update chat list filters', () => {
|
||||
actions.updateChatListFilters({ commit }, { updatedWithin: 20 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.UPDATE_CHAT_LIST_FILTERS, { updatedWithin: 20 }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -279,45 +279,4 @@ describe('#getters', () => {
|
||||
expect(getters.getContextMenuChatId(state)).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getChatListFilters', () => {
|
||||
it('get chat list filters', () => {
|
||||
const conversationFilters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
const state = { conversationFilters: conversationFilters };
|
||||
expect(getters.getChatListFilters(state)).toEqual(conversationFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAppliedConversationFiltersQuery', () => {
|
||||
it('get applied conversation filters query', () => {
|
||||
const filtersList = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
const state = { appliedFilters: filtersList };
|
||||
expect(getters.getAppliedConversationFiltersQuery(state)).toEqual({
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
query_operator: undefined,
|
||||
values: ['snoozed'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -279,66 +279,6 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_CONVERSATION', () => {
|
||||
it('set all conversation', () => {
|
||||
const state = { allConversations: [{ id: 1 }] };
|
||||
const data = [{ id: 1, name: 'test' }];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is the same', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, status: 'open' }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
const data = [{ id: 1, name: 'test', status: 'resolved' }];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is the same then do not update messages', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 1, content: 'test' }] }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
messages: [{ id: 1, content: 'updated message' }],
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{ id: 1, name: 'test', messages: [{ id: 1, content: 'test' }] },
|
||||
];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(expected);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is not the same', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, status: 'open' }],
|
||||
selectedChatId: 2,
|
||||
};
|
||||
const data = [{ id: 1, name: 'test', status: 'resolved' }];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is not the same then update messages', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 1, content: 'test' }] }],
|
||||
selectedChatId: 2,
|
||||
};
|
||||
const data = [
|
||||
{ id: 1, name: 'test', messages: [{ id: 1, content: 'tested' }] },
|
||||
];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_ATTACHMENTS', () => {
|
||||
it('set all attachments', () => {
|
||||
const state = {
|
||||
@@ -471,54 +411,4 @@ describe('#mutations', () => {
|
||||
expect(state.contextMenuChatId).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CHAT_LIST_FILTERS', () => {
|
||||
it('set chat list filters', () => {
|
||||
const conversationFilters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
const state = { conversationFilters: conversationFilters };
|
||||
mutations[types.SET_CHAT_LIST_FILTERS](state, conversationFilters);
|
||||
expect(state.conversationFilters).toEqual(conversationFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CHAT_LIST_FILTERS', () => {
|
||||
it('update chat list filters', () => {
|
||||
const conversationFilters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
const state = { conversationFilters: conversationFilters };
|
||||
mutations[types.UPDATE_CHAT_LIST_FILTERS](state, {
|
||||
inboxId: 2,
|
||||
updatedWithin: 20,
|
||||
assigneeType: 'all',
|
||||
});
|
||||
expect(state.conversationFilters).toEqual({
|
||||
inboxId: 2,
|
||||
assigneeType: 'all',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
updatedWithin: 20,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,13 +91,4 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setActiveConversationFolder', () => {
|
||||
it('set active conversation folder', async () => {
|
||||
await actions.setActiveConversationFolder({ commit }, customViewList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACTIVE_CONVERSATION_FOLDER, customViewList[0]],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,11 +76,4 @@ describe('#getters', () => {
|
||||
isDeleting: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('getActiveConversationFolder', () => {
|
||||
const state = { activeConversationFolder: customViewList[0] };
|
||||
expect(getters.getActiveConversationFolder(state)).toEqual(
|
||||
customViewList[0]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,12 +34,4 @@ describe('#mutations', () => {
|
||||
expect(state.records).toEqual(updateCustomViewList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ACTIVE_CONVERSATION_FOLDER', () => {
|
||||
it('set active conversation folder', () => {
|
||||
const state = { activeConversationFolder: customViewList[0] };
|
||||
mutations[types.SET_ACTIVE_CONVERSATION_FOLDER](state, customViewList[0]);
|
||||
expect(state.activeConversationFolder).toEqual(customViewList[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,34 +283,4 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setNotificationFilters', () => {
|
||||
it('set notification filters', async () => {
|
||||
const filters = {
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
await actions.setNotificationFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATION_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateNotificationFilters', () => {
|
||||
it('update notification filters', async () => {
|
||||
const filters = {
|
||||
page: 1,
|
||||
status: 'unread',
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
await actions.updateNotificationFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.UPDATE_NOTIFICATION_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,18 +81,4 @@ describe('#getters', () => {
|
||||
};
|
||||
expect(getters.getMeta(state)).toEqual({ unreadCount: 1 });
|
||||
});
|
||||
|
||||
it('getNotificationFilters', () => {
|
||||
const state = {
|
||||
notificationFilters: {
|
||||
page: 1,
|
||||
status: 'unread',
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
};
|
||||
expect(getters.getNotificationFilters(state)).toEqual(
|
||||
state.notificationFilters
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,19 +52,19 @@ describe('#mutations', () => {
|
||||
});
|
||||
|
||||
describe('#SET_NOTIFICATIONS', () => {
|
||||
it('set notifications', () => {
|
||||
it('set notifications ', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.SET_NOTIFICATIONS](state, [
|
||||
{ id: 1, primary_actor_id: 1 },
|
||||
{ id: 2, primary_actor_id: 2 },
|
||||
{ id: 3, primary_actor_id: 3 },
|
||||
{ id: 4, primary_actor_id: 4 },
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
]);
|
||||
expect(state.records).toEqual({
|
||||
1: { id: 1, primary_actor_id: 1 },
|
||||
2: { id: 2, primary_actor_id: 2 },
|
||||
3: { id: 3, primary_actor_id: 3 },
|
||||
4: { id: 4, primary_actor_id: 4 },
|
||||
1: { id: 1 },
|
||||
2: { id: 2 },
|
||||
3: { id: 3 },
|
||||
4: { id: 4 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,9 +60,6 @@ export default {
|
||||
|
||||
SET_CONTEXT_MENU_CHAT_ID: 'SET_CONTEXT_MENU_CHAT_ID',
|
||||
|
||||
SET_CHAT_LIST_FILTERS: 'SET_CHAT_LIST_FILTERS',
|
||||
UPDATE_CHAT_LIST_FILTERS: 'UPDATE_CHAT_LIST_FILTERS',
|
||||
|
||||
// Inboxes
|
||||
SET_INBOXES_UI_FLAG: 'SET_INBOXES_UI_FLAG',
|
||||
SET_INBOXES: 'SET_INBOXES',
|
||||
@@ -150,8 +147,6 @@ export default {
|
||||
DELETE_READ_NOTIFICATIONS: 'DELETE_READ_NOTIFICATIONS',
|
||||
DELETE_ALL_NOTIFICATIONS: 'DELETE_ALL_NOTIFICATIONS',
|
||||
SNOOZE_NOTIFICATION: 'SNOOZE_NOTIFICATION',
|
||||
SET_NOTIFICATION_FILTERS: 'SET_NOTIFICATION_FILTERS',
|
||||
UPDATE_NOTIFICATION_FILTERS: 'UPDATE_NOTIFICATION_FILTERS',
|
||||
|
||||
// Contact Conversation
|
||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
@@ -244,7 +239,6 @@ export default {
|
||||
ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
|
||||
UPDATE_CUSTOM_VIEW: 'UPDATE_CUSTOM_VIEW',
|
||||
DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
|
||||
SET_ACTIVE_CONVERSATION_FOLDER: 'SET_ACTIVE_CONVERSATION_FOLDER',
|
||||
|
||||
// Bulk Actions
|
||||
SET_BULK_ACTIONS_FLAG: 'SET_BULK_ACTIONS_FLAG',
|
||||
|
||||
@@ -96,9 +96,3 @@ export const throwErrorMessage = error => {
|
||||
const errorMessage = parseAPIErrorResponse(error);
|
||||
throw new Error(errorMessage);
|
||||
};
|
||||
|
||||
export const parseLinearAPIErrorResponse = (error, defaultMessage) => {
|
||||
const errorData = error.response.data;
|
||||
const errorMessage = errorData?.error?.errors?.[0]?.message || defaultMessage;
|
||||
return errorMessage;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
parseAPIErrorResponse,
|
||||
setLoadingStatus,
|
||||
throwErrorMessage,
|
||||
parseLinearAPIErrorResponse,
|
||||
} from '../api';
|
||||
|
||||
describe('#getLoadingStatus', () => {
|
||||
@@ -50,26 +49,3 @@ describe('#throwErrorMessage', () => {
|
||||
expect(errorFn).toThrow('Error Message [message]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#parseLinearAPIErrorResponse', () => {
|
||||
it('returns correct values', () => {
|
||||
expect(
|
||||
parseLinearAPIErrorResponse(
|
||||
{
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
errors: [
|
||||
{
|
||||
message: 'Error Message [message]',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'Default Message'
|
||||
)
|
||||
).toBe('Error Message [message]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,9 +44,6 @@ export default {
|
||||
whatsAppAPIProvider() {
|
||||
return this.inbox.provider || '';
|
||||
},
|
||||
isAMicrosoftInbox() {
|
||||
return this.isAnEmailChannel && this.inbox.provider === 'microsoft';
|
||||
},
|
||||
isAPIInbox() {
|
||||
return this.channelType === INBOX_TYPES.API;
|
||||
},
|
||||
|
||||
@@ -54,8 +54,6 @@ module Reauthorizable
|
||||
update!(active: false)
|
||||
mailer.automation_rule_disabled(self).deliver_later
|
||||
end
|
||||
|
||||
invalidate_inbox_cache unless instance_of?(::AutomationRule)
|
||||
end
|
||||
|
||||
def process_integration_hook_reauthorization_emails(mailer)
|
||||
@@ -70,16 +68,10 @@ module Reauthorizable
|
||||
def reauthorized!
|
||||
::Redis::Alfred.delete(authorization_error_count_key)
|
||||
::Redis::Alfred.delete(reauthorization_required_key)
|
||||
|
||||
invalidate_inbox_cache unless instance_of?(::AutomationRule)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invalidate_inbox_cache
|
||||
inbox.update_account_cache if inbox.present?
|
||||
end
|
||||
|
||||
def authorization_error_count_key
|
||||
format(::Redis::Alfred::AUTHORIZATION_ERROR_COUNT, obj_type: self.class.table_name.singularize, obj_id: id)
|
||||
end
|
||||
|
||||
@@ -71,11 +71,8 @@ if resource.email?
|
||||
json.imap_address resource.channel.try(:imap_address)
|
||||
json.imap_port resource.channel.try(:imap_port)
|
||||
json.imap_enabled resource.channel.try(:imap_enabled)
|
||||
json.microsoft_reauthorization resource.channel.try(:microsoft?) && resource.channel.try(:provider_config).empty?
|
||||
json.imap_enable_ssl resource.channel.try(:imap_enable_ssl)
|
||||
|
||||
if resource.channel.try(:microsoft?)
|
||||
json.reauthorization_required resource.channel.try(:provider_config).empty? || resource.channel.try(:reauthorization_required?)
|
||||
end
|
||||
end
|
||||
|
||||
## SMTP
|
||||
|
||||
@@ -70,8 +70,40 @@
|
||||
<div id="app"></div>
|
||||
<noscript id="noscript">This app works best with JavaScript enabled.</noscript>
|
||||
<%= yield %>
|
||||
<% if @dashboard_scripts.present? %>
|
||||
<%= @dashboard_scripts.html_safe %>
|
||||
<% if @global_config['CHATWOOT_INBOX_TOKEN'].present? %>
|
||||
<script>
|
||||
window.chatwootSettings = { hideMessageBubble: true, position: 'left' };
|
||||
(function(d,t) {
|
||||
var BASE_URL='<%= ENV.fetch('FRONTEND_URL', 'https://app.chatwoot.com') %>';
|
||||
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||
g.src=BASE_URL+"/packs/js/sdk.js"; g.defer=true; g.async=true;
|
||||
s.parentNode.insertBefore(g,s);
|
||||
g.onload=function(){
|
||||
window.chatwootSDK.run({
|
||||
websiteToken: '<%= @global_config['CHATWOOT_INBOX_TOKEN'] %>',
|
||||
baseUrl: BASE_URL,
|
||||
});
|
||||
}
|
||||
})(document,"script");
|
||||
</script>
|
||||
<% end %>
|
||||
<% if ENV.fetch('MS_CLARITY_TOKEN', nil).present? %>
|
||||
<script type="text/javascript">
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src='https://www.clarity.ms/tag/'+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, 'clarity', 'script', '<%= ENV.fetch('MS_CLARITY_TOKEN', '') %>');
|
||||
</script>
|
||||
<% end %>
|
||||
<% if ENV.fetch('GOOGLE_TAG', nil).present? %>
|
||||
<script>
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer', '<%= ENV.fetch('GOOGLE_TAG', '') %>');
|
||||
</script>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,25 +13,18 @@
|
||||
<div class="field-unit__label">
|
||||
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
|
||||
</div>
|
||||
<div class="-mt-2 field-unit__field ">
|
||||
<div class="field-unit__field -mt-2 ">
|
||||
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
||||
<%= form.select "app_config[#{key}]",
|
||||
[["True", true], ["False", false]],
|
||||
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||
%>
|
||||
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
|
||||
<%= form.text_area "app_config[#{key}]",
|
||||
value: @app_config[key],
|
||||
rows: 12,
|
||||
wrap: 'off',
|
||||
class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll"
|
||||
%>
|
||||
<% else %>
|
||||
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
|
||||
<% end %>
|
||||
<%if @installation_configs[key]&.dig('description').present? %>
|
||||
<p class="pt-2 text-xs italic text-slate-400">
|
||||
<p class="text-slate-400 text-xs italic pt-2">
|
||||
<%= @installation_configs[key]&.dig('description') %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -163,11 +163,6 @@
|
||||
value:
|
||||
display_title: 'Clearbit API Key'
|
||||
description: 'This API key is used for onboarding the users, to pre-fill account data.'
|
||||
- name: DASHBOARD_SCRIPTS
|
||||
value:
|
||||
display_title: 'Dashboard Scripts'
|
||||
description: 'Scripts are loaded as the last item in the <body> tag'
|
||||
type: code
|
||||
# ------- End of Chatwoot Internal Config for Cloud ----#
|
||||
|
||||
# ------- Chatwoot Internal Config for Self Hosted ----#
|
||||
|
||||
@@ -20,7 +20,6 @@ Rails.application.routes.draw do
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/microsoft', to: 'dashboard#index', as: 'app_new_microsoft_inbox'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
|
||||
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_microsoft_inbox_agents'
|
||||
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_microsoft_inbox_settings'
|
||||
|
||||
resource :widget, only: [:show]
|
||||
namespace :survey do
|
||||
|
||||
@@ -30,6 +30,6 @@ module Enterprise::SuperAdmin::AppConfigsController
|
||||
end
|
||||
|
||||
def internal_config_options
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY ANALYTICS_TOKEN CLEARBIT_API_KEY DASHBOARD_SCRIPTS]
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY ANALYTICS_TOKEN CLEARBIT_API_KEY]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ class ChatwootMarkdownRenderer
|
||||
|
||||
def render_article
|
||||
markdown_renderer = CustomMarkdownRenderer.new
|
||||
doc = CommonMarker.render_doc(@content, :DEFAULT, [:table])
|
||||
doc = CommonMarker.render_doc(@content, :DEFAULT)
|
||||
html = markdown_renderer.render(doc)
|
||||
|
||||
render_as_html_safe(html)
|
||||
|
||||
@@ -3,7 +3,7 @@ module Linear::Mutations
|
||||
case value
|
||||
when String
|
||||
# Strings must be enclosed in double quotes
|
||||
"\"#{value.gsub("\n", '\\n')}\""
|
||||
"\"#{value}\""
|
||||
when Array
|
||||
# Arrays need to be recursively converted
|
||||
"[#{value.map { |v| graphql_value(v) }.join(', ')}]"
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/prosemirror-schema": "1.0.9",
|
||||
"@chatwoot/prosemirror-schema": "1.0.5",
|
||||
"@chatwoot/utils": "^0.0.25",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^2.0.0",
|
||||
|
||||
@@ -49,7 +49,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(response).to redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
|
||||
@@ -16,10 +16,6 @@ RSpec.describe ChatwootMarkdownRenderer do
|
||||
end
|
||||
|
||||
describe '#render_article' do
|
||||
before do
|
||||
allow(CommonMarker).to receive(:render_doc).with(markdown_content, :DEFAULT, [:table]).and_return(doc)
|
||||
end
|
||||
|
||||
let(:rendered_content) { renderer.render_article }
|
||||
|
||||
it 'renders the markdown content to html' do
|
||||
@@ -29,38 +25,6 @@ RSpec.describe ChatwootMarkdownRenderer do
|
||||
it 'returns an html safe string' do
|
||||
expect(rendered_content).to be_html_safe
|
||||
end
|
||||
|
||||
context 'when tables in markdown' do
|
||||
let(:markdown_content) do
|
||||
<<~MARKDOWN
|
||||
This is a **bold** text and *italic* text.
|
||||
|
||||
| Header1 | Header2 |
|
||||
| ------------ | ------------ |
|
||||
| **Bold Cell**| *Italic Cell*|
|
||||
| Cell3 | Cell4 |
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
let(:html_content) do
|
||||
<<~HTML
|
||||
<p>This is a <strong>bold</strong> text and <em>italic</em> text.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Header1</th><th>Header2</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><strong>Bold Cell</strong></td><td><em>Italic Cell</em></td></tr>
|
||||
<tr><td>Cell3</td><td>Cell4</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'renders tables in html' do
|
||||
expect(rendered_content.to_s).to eq(html_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#render_message' do
|
||||
|
||||
@@ -148,23 +148,6 @@ describe Linear do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the description is markdown' do
|
||||
let(:description) { 'Cmd/Ctrl` `K` **is our most powerful feature.** \n\nUse it to search for or take any action in the app' }
|
||||
|
||||
before do
|
||||
stub_request(:post, url)
|
||||
.to_return(status: 200, body: { success: true,
|
||||
data: { issueCreate: { id: 'issue1', title: 'Title',
|
||||
description: description } } }.to_json, headers: headers)
|
||||
end
|
||||
|
||||
it 'creates an issue' do
|
||||
response = linear_client.create_issue(params)
|
||||
expect(response).to eq({ 'issueCreate' => { 'id' => 'issue1', 'title' => 'Title',
|
||||
'description' => description } })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API response is an error' do
|
||||
before do
|
||||
stub_request(:post, url)
|
||||
|
||||
@@ -3165,10 +3165,10 @@
|
||||
hotkeys-js "3.8.7"
|
||||
lit "2.2.6"
|
||||
|
||||
"@chatwoot/prosemirror-schema@1.0.9":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.9.tgz#447408fc219a312c5bb851c043ac6ea2e069906f"
|
||||
integrity sha512-jR+EgBWXhbKeU5krQRNkX0uuButu5K55notuJgxQUeWvgg3EJkz8LhDizmBoq/5NgV4FoDzqOD2eN6rUsXFa1w==
|
||||
"@chatwoot/prosemirror-schema@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.5.tgz#d6053692beae59d466ac0b04128fa157f59eb176"
|
||||
integrity sha512-dOzkZ2K53PPbE9AQB0RHlVs+GIEyHHdXeeW44dNSEuULwH99PmTpzA2r45QX3uaVa2j7Mip76AQbJZGKbM2fxg==
|
||||
dependencies:
|
||||
markdown-it-sup "^1.0.0"
|
||||
prosemirror-commands "^1.1.4"
|
||||
|
||||
Reference in New Issue
Block a user