Compare commits

..
Author SHA1 Message Date
Muhsin Keloth 935b394fb5 Update integrations.json 2024-05-23 12:48:53 +05:30
Muhsin Keloth a5c8c790e9 chore: add conversation id 2024-05-23 12:46:50 +05:30
Muhsin Keloth a95ea1a5a4 feat: add title for linear issue links 2024-05-23 09:45:32 +05:30
Muhsin Keloth 4a692e7e3a Merge branch 'feature/cw-3271' of https://github.com/chatwoot/chatwoot into feature/cw-3271 2024-05-23 09:10:50 +05:30
Muhsin Keloth 74dc1ef2ae fix: project id 2024-05-23 09:08:40 +05:30
Muhsin KelothandGitHub 723763b54d Merge branch 'develop' into feature/cw-3271 2024-05-23 09:08:23 +05:30
Muhsin Keloth a4ec09a744 chore: review fixes 2024-05-22 16:01:21 +05:30
Muhsin Keloth e847c34380 chore: review fixes 2024-05-22 15:03:10 +05:30
Sivin VargheseandGitHub 023d3e0821 Merge branch 'develop' into feature/cw-3271 2024-05-22 14:07:32 +05:30
Muhsin KelothandGitHub 63e407b1c5 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-22 13:01:00 +05:30
Muhsin Keloth 717e744b08 Update linear.js 2024-05-22 13:00:50 +05:30
Muhsin Keloth aefe3a1b58 chore: rename linked_issues route 2024-05-22 12:57:53 +05:30
Muhsin KelothandGitHub eab8d8d0fb Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-22 12:13:41 +05:30
Muhsin Keloth dd78655181 chore: review fixes 2024-05-22 11:59:40 +05:30
Muhsin KelothandGitHub 831ac02d82 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-22 08:56:18 +05:30
Muhsin KelothandGitHub a0d85557e4 Merge branch 'develop' into feat/linear-backend 2024-05-22 08:54:18 +05:30
Muhsin KelothandGitHub fb22659ec8 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-21 19:51:12 +05:30
Muhsin Keloth 23ac2d9e4b chore: remove unused code 2024-05-21 16:53:20 +05:30
Muhsin Keloth 497471632b Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-21 13:50:58 +05:30
Muhsin Keloth 903ec124f6 chore: add identifier in search query 2024-05-21 13:50:48 +05:30
Muhsin Keloth 902f3aa926 Merge branch 'feature/cw-3271' of https://github.com/chatwoot/chatwoot into feature/cw-3271 2024-05-21 13:50:40 +05:30
Muhsin Keloth 4b0345a577 chore: remove unwanted changes 2024-05-21 13:50:31 +05:30
Muhsin Keloth a2dab6a3a8 chore: remove unwanted changes 2024-05-21 12:42:07 +05:30
Muhsin KelothandGitHub d278107133 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-21 12:37:31 +05:30
Muhsin Keloth efb18c1b64 chore: add identifier in search query 2024-05-21 12:37:21 +05:30
Muhsin KelothandGitHub 4c86d56200 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-21 12:36:44 +05:30
Muhsin Keloth 70e1bf8d31 chore: cleanup 2024-05-21 12:36:19 +05:30
Muhsin Keloth e8f5e35dc9 chore: add title in link issue 2024-05-21 12:34:05 +05:30
Muhsin Keloth df0a7a348b chore: change link dropdown component 2024-05-20 17:05:07 +05:30
Muhsin KelothandGitHub 6550287f97 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-20 16:25:30 +05:30
Muhsin KelothandGitHub cfb1ae0dd1 Merge branch 'develop' into feat/linear-backend 2024-05-20 16:25:23 +05:30
Muhsin Keloth 95d2a57a81 chore: add create comment api 2024-05-20 15:34:38 +05:30
Muhsin Keloth 6b86b6f87d chore: refactor link issue related components 2024-05-20 10:55:00 +05:30
Muhsin Keloth b5d655171e chore: refactor issue component 2024-05-20 10:31:37 +05:30
Muhsin Keloth 4263ea7db1 chore: more cleanups 2024-05-20 10:07:55 +05:30
Muhsin Keloth 3552eede47 chore: more cleanups 2024-05-20 09:57:28 +05:30
Muhsin Keloth 24825a070a feat: change search component 2024-05-18 22:46:32 +05:30
Muhsin Keloth 7767c83206 Update IssueItem.vue 2024-05-18 12:34:30 +05:30
Muhsin Keloth 64324d239e chore: style changes 2024-05-18 12:08:22 +05:30
Muhsin KelothandGitHub bd55bb1f08 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-17 20:26:28 +05:30
Muhsin Keloth ca8dc3760f chore: update linear config 2024-05-17 20:26:09 +05:30
Muhsin Keloth 6471db4ba7 chore: code cleanup 2024-05-17 20:23:55 +05:30
Muhsin Keloth d7e7e4d8c9 Update IssueItem.vue 2024-05-17 16:49:22 +05:30
Muhsin KelothandGitHub d169d97987 Merge branch 'develop' into feat/linear-backend 2024-05-17 16:45:03 +05:30
Muhsin Keloth 480888e115 chore: unused image 2024-05-17 16:20:03 +05:30
Muhsin Keloth 1fb84c6402 chore: change everything to vue3 syntax 2024-05-17 16:09:08 +05:30
Muhsin Keloth 2c2e000264 Merge branch 'feature/cw-3271' of https://github.com/chatwoot/chatwoot into feature/cw-3271 2024-05-17 16:08:43 +05:30
Muhsin Keloth 3376183b03 chore: migrate linear base to vue3 syntax 2024-05-17 15:14:38 +05:30
Muhsin KelothandGitHub 8159aad772 Merge branch 'feat/linear-backend' into feature/cw-3271 2024-05-17 15:01:26 +05:30
Muhsin Keloth c26e0cc7e0 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-17 15:01:09 +05:30
Muhsin Keloth 2378efd853 Update linear_queries.rb 2024-05-17 15:00:28 +05:30
Muhsin KelothandGitHub b40c3110aa Merge branch 'develop' into feat/linear-backend 2024-05-17 15:00:04 +05:30
Muhsin Keloth 3d1bafa3c0 feat: complete basic front end 2024-05-17 14:04:48 +05:30
Muhsin Keloth 08c487d1c3 chore: update issue query 2024-05-17 12:08:32 +05:30
Muhsin Keloth 0e11dac471 feat: create basic front end 2024-05-17 12:06:19 +05:30
Muhsin KelothandGitHub e992c23d03 Merge branch 'feat/linear-backend' into feat/linear-frontend 2024-05-16 17:30:58 +05:30
Muhsin Keloth 669a138114 chore: team validation 2024-05-16 16:20:24 +05:30
Muhsin Keloth 31732a1a7b chore: replace graphlient with HTTParty 2024-05-16 16:18:47 +05:30
Muhsin KelothandGitHub b663f3201c Merge branch 'develop' into feat/linear-backend 2024-05-16 15:25:33 +05:30
Muhsin Keloth 01ddb61c9e chore: add url in issue 2024-05-16 15:25:10 +05:30
Muhsin Keloth 87e8e342c9 Update index.vue 2024-05-14 12:27:52 +05:30
Muhsin Keloth 27ec845faa chore: add url in issue 2024-05-14 08:28:24 +05:30
iamsivin 91cefe2072 chore: Fix input styles 2024-05-14 00:55:54 +05:30
Muhsin KelothandGitHub d356067300 Merge branch 'develop' into feat/linear-backend 2024-05-13 22:06:57 +05:30
Muhsin Keloth aa3f2b56bc chore: minor fix 2024-05-13 21:08:59 +05:30
Muhsin Keloth bc5449f208 chore: translation updates 2024-05-13 19:13:03 +05:30
Muhsin Keloth e1bfd9a17a chore: translation updates 2024-05-13 19:04:06 +05:30
Muhsin Keloth 4825ab1a29 chore: code cleanup 2024-05-13 18:34:40 +05:30
iamsivin 7efc2b9325 chore: Style fix 2024-05-13 14:33:01 +05:30
Muhsin Keloth 4a491d8a91 feat: add or link issue 2024-05-13 13:59:38 +05:30
Muhsin Keloth f2db469c75 feat: add basic create issue feature 2024-05-10 16:59:41 +05:30
Muhsin Keloth 5d48c2b1e5 chore: add more fields in get issue querey 2024-05-10 16:53:01 +05:30
Muhsin Keloth ecce6e021b chore: update link issue API 2024-05-10 15:50:13 +05:30
Muhsin Keloth 4d47fc1c25 chore: rename from team_entites to team_entities 2024-05-10 15:16:56 +05:30
Muhsin KelothandGitHub b25e395bf7 Merge branch 'develop' into feat/linear-backend 2024-05-10 11:40:56 +05:30
Muhsin KelothandGitHub 12b3323364 Merge branch 'develop' into feat/linear-backend 2024-05-10 09:44:59 +05:30
Muhsin KelothandGitHub 793420c341 Merge branch 'develop' into feat/linear-backend 2024-05-09 23:41:57 +05:30
Muhsin KelothandGitHub 775e8251a6 Merge branch 'develop' into feat/linear-backend 2024-05-09 22:45:43 +05:30
Muhsin Keloth 4645e57e9a feat: add linked issue api 2024-05-09 22:40:42 +05:30
Muhsin Keloth 5e1dbfb64d feat: search issue API 2024-05-09 22:28:47 +05:30
Muhsin Keloth df29b43540 feat: unlink issue api 2024-05-09 21:44:42 +05:30
Muhsin Keloth 3e4f43d7da chore: add specs for create and link apis 2024-05-09 16:24:31 +05:30
Muhsin Keloth 2b7c972cbf feat: add specs for linear processor 2024-05-09 15:16:14 +05:30
Muhsin Keloth 429cfd0166 feat: add specs for create and link issue 2024-05-09 15:12:20 +05:30
Muhsin Keloth fc41669178 feat: add link issue API 2024-05-09 15:05:36 +05:30
Muhsin Keloth e567be8fb2 chore: issue input validation 2024-05-09 14:25:00 +05:30
Muhsin Keloth 1ddc05dadb feat: add create issue API 2024-05-09 12:23:14 +05:30
Muhsin KelothandGitHub 0268d544d1 Merge branch 'develop' into feat/linear-backend 2024-05-09 11:33:07 +05:30
Muhsin KelothandGitHub 2960bcdf7d Merge branch 'develop' into feat/linear-backend 2024-05-09 10:37:54 +05:30
Muhsin KelothandGitHub 3c4a7bb9f7 Merge branch 'develop' into feat/linear-backend 2024-05-09 07:32:51 +05:30
Muhsin Keloth 792bc5cf9d Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 13:46:43 +05:30
Muhsin Keloth bf40242497 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 13:46:35 +05:30
Muhsin Keloth 2130a2ebe1 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 12:36:54 +05:30
Muhsin KelothandGitHub 2bad7f435a Merge branch 'develop' into feat/linear-backend 2024-05-08 12:36:43 +05:30
Muhsin Keloth c0d985a62b chore: make applications component in vue 3 syntax 2024-05-08 12:36:33 +05:30
Muhsin Keloth 5b7af73abe Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-08 12:15:44 +05:30
Muhsin Keloth 2ae79bd9a1 chore: add specs 2024-05-08 12:15:38 +05:30
Muhsin Keloth 17b61ad2b4 chore: add specs 2024-05-08 10:51:10 +05:30
Muhsin KelothandGitHub e098e463ad Merge branch 'develop' into feat/linear-backend 2024-05-08 09:09:13 +05:30
Muhsin Keloth b11f4d5bd5 Update linear_controller.rb 2024-05-07 15:22:59 +05:30
Muhsin Keloth 0a45b7e156 chore: add feature flag linear_integration 2024-05-07 15:13:33 +05:30
Muhsin Keloth ee184d9792 chore: cleanup code 2024-05-07 14:19:34 +05:30
Muhsin KelothandGitHub 3595903fb5 Merge branch 'develop' into feat/linear-backend 2024-05-07 14:06:03 +05:30
Muhsin KelothandGitHub 68a7fce45c Merge branch 'develop' into feat/linear-backend 2024-05-06 22:35:00 +05:30
Muhsin KelothandGitHub 8e5cabd14d Merge branch 'develop' into feat/linear-backend 2024-05-06 16:31:59 +05:30
Muhsin Keloth dd2938dc03 Merge branch 'feat/linear-backend' of https://github.com/chatwoot/chatwoot into feat/linear-backend 2024-05-06 15:58:56 +05:30
Muhsin Keloth 0bf4fc9e21 chore: add ProcessorService spec 2024-05-06 15:58:53 +05:30
Muhsin KelothandGitHub 8f7162d6c1 Merge branch 'develop' into feat/linear-backend 2024-05-06 14:27:21 +05:30
Muhsin Keloth 243834c401 feat: add team entities API 2024-05-06 14:26:52 +05:30
Muhsin KelothandGitHub 1bba61a758 Merge branch 'develop' into feat/linear-backend 2024-05-03 14:29:47 +05:30
Muhsin Keloth e58f1ad98d fix: specs 2024-05-03 14:25:10 +05:30
Shivam Mishra 7e61397186 fix: linear spec 2024-05-03 13:36:24 +05:30
Muhsin Keloth c5e34669f6 chore: add linear schema 2024-05-03 12:59:34 +05:30
Muhsin Keloth 97f96db60e Create linear_spec.rb 2024-05-03 12:48:14 +05:30
Muhsin Keloth 0fafe37646 chore: code cleanup 2024-05-02 16:45:57 +05:30
Muhsin Keloth 68cb6b19b0 feat: add teams end point 2024-05-02 16:11:21 +05:30
Muhsin Keloth ead7cc0c36 feat: add liner integration 2024-05-02 13:39:57 +05:30
71 changed files with 251 additions and 1007 deletions
+6 -5
View File
@@ -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
+2 -34
View File
@@ -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
View File
@@ -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)
-5
View File
@@ -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();
}
},
},
@@ -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"
@@ -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;
}
@@ -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'));
}
};
+1 -2
View File
@@ -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: {
@@ -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>
@@ -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) {
@@ -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: {
@@ -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;
},
-8
View File
@@ -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
+1 -4
View File
@@ -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
+34 -2
View File
@@ -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 %>
-5
View File
@@ -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 ----#
-1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
-17
View File
@@ -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)
+4 -4
View File
@@ -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"