From dc335e88c9dee4f6cfd60f41bb99f39bcf37c3a6 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 28 May 2025 15:15:05 +0530 Subject: [PATCH 1/7] fix: External links in widget not opening in new tab (#11608) --- app/javascript/portal/portalHelpers.js | 11 +-- app/javascript/portal/specs/portal.spec.js | 98 +++++++++++++++++++++- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index 5cced0fa4..9cb28e63a 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -38,16 +38,9 @@ export const openExternalLinksInNewTab = () => { document.addEventListener('click', event => { if (!isOnArticlePage) return; - // Some of the links come wrapped in strong tag through prosemirror - - const isTagAnchor = event.target.tagName === 'A'; - const isParentTagAnchor = - event.target.tagName === 'STRONG' && - event.target.parentNode.tagName === 'A'; - - if (isTagAnchor || isParentTagAnchor) { - const link = isTagAnchor ? event.target : event.target.parentNode; + const link = event.target.closest('a'); + if (link) { const isInternalLink = link.hostname === window.location.hostname || link.href.includes(customDomain) || diff --git a/app/javascript/portal/specs/portal.spec.js b/app/javascript/portal/specs/portal.spec.js index 13edd3718..861950a57 100644 --- a/app/javascript/portal/specs/portal.spec.js +++ b/app/javascript/portal/specs/portal.spec.js @@ -1,6 +1,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { JSDOM } from 'jsdom'; -import { InitializationHelpers } from '../portalHelpers'; +import { + InitializationHelpers, + openExternalLinksInNewTab, +} from '../portalHelpers'; describe('InitializationHelpers.navigateToLocalePage', () => { let dom; @@ -44,3 +47,96 @@ describe('InitializationHelpers.navigateToLocalePage', () => { ); }); }); + +describe('openExternalLinksInNewTab', () => { + let dom; + let document; + let window; + + beforeEach(() => { + dom = new JSDOM( + ` + + +
+ External + Internal + Custom + CodeBold + +
+ + `, + { url: 'https://app.chatwoot.com/hc/article' } + ); + + document = dom.window.document; + window = dom.window; + + window.portalConfig = { + customDomain: 'custom.domain.com', + hostURL: 'app.chatwoot.com', + }; + + global.document = document; + global.window = window; + }); + + afterEach(() => { + dom = null; + document = null; + window = null; + delete global.document; + delete global.window; + }); + + const simulateClick = selector => { + const element = document.querySelector(selector); + const event = new window.MouseEvent('click', { bubbles: true }); + element.dispatchEvent(event); + return element.closest('a') || element; + }; + + it('opens external links in new tab', () => { + openExternalLinksInNewTab(); + + const link = simulateClick('#external'); + + expect(link.target).toBe('_blank'); + expect(link.rel).toBe('noopener noreferrer'); + }); + + it('preserves internal links', () => { + openExternalLinksInNewTab(); + + const internal = simulateClick('#internal'); + const custom = simulateClick('#custom'); + + expect(internal.target).not.toBe('_blank'); + expect(custom.target).not.toBe('_blank'); + }); + + it('handles clicks on nested elements', () => { + openExternalLinksInNewTab(); + + simulateClick('#nested code'); + simulateClick('#nested strong'); + + const link = document.getElementById('nested'); + expect(link.target).toBe('_blank'); + expect(link.rel).toBe('noopener noreferrer'); + }); + + it('handles links inside list items with strong tags', () => { + openExternalLinksInNewTab(); + + // Click on the strong element inside the link in the list + simulateClick('#list-link strong'); + + const link = document.getElementById('list-link'); + expect(link.target).toBe('_blank'); + expect(link.rel).toBe('noopener noreferrer'); + }); +}); From f916fb2924e525b879ac060526bd3ccfb80ef47e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 28 May 2025 17:56:32 +0530 Subject: [PATCH 2/7] fix: handle empty customDomain when checking for `isInternalLink` (#11609) This PR improves the portal's internal link detection logic to be more robust when handling empty or undefined configuration values. Previously, the code could fail when `customDomain` was empty, causing external links to incorrectly behave as internal links. The fix introduces a new `isSameOrigin` helper function that safely compares URLs using proper URL parsing and origin comparison, gracefully handling edge cases like missing domains, relative paths, and malformed URLs. This ensures external links consistently open in new tabs regardless of portal configuration completeness. --- app/javascript/portal/portalHelpers.js | 63 ++++++++++++++++++---- app/javascript/portal/specs/portal.spec.js | 45 +++++++++++++++- 2 files changed, 98 insertions(+), 10 deletions(-) diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index 9cb28e63a..68e2cb6a8 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -25,15 +25,56 @@ export const getHeadingsfromTheArticle = () => { return rows; }; +/** + * Converts various input formats to URL objects. + * Handles URL objects, domain strings, relative paths, and full URLs. + * @param {string|URL} input - Input to convert to URL object + * @returns {URL|null} URL object or null if input is invalid + */ +const toURL = input => { + if (!input) return null; + if (input instanceof URL) return input; + + if ( + typeof input === 'string' && + !input.includes('://') && + !input.startsWith('/') + ) { + return new URL(`https://${input}`); + } + + if (typeof input === 'string' && input.startsWith('/')) { + return new URL(input, window.location.origin); + } + + return new URL(input); +}; + +/** + * Determines if two URLs belong to the same host by comparing their normalized URL objects. + * Handles various input formats including URL objects, domain strings, relative paths, and full URLs. + * Returns false if either URL cannot be parsed or normalized. + * @param {string|URL} url1 - First URL to compare + * @param {string|URL} url2 - Second URL to compare + * @returns {boolean} True if both URLs have the same host, false otherwise + */ +const isSameHost = (url1, url2) => { + try { + const urlObj1 = toURL(url1); + const urlObj2 = toURL(url2); + + if (!urlObj1 || !urlObj2) return false; + + return urlObj1.hostname === urlObj2.hostname; + } catch (error) { + return false; + } +}; + export const openExternalLinksInNewTab = () => { const { customDomain, hostURL } = window.portalConfig; - const isSameHost = - window.location.href.includes(customDomain) || - window.location.href.includes(hostURL); - - // Modify external links only on articles page const isOnArticlePage = - isSameHost && document.querySelector('#cw-article-content') !== null; + document.querySelector('#cw-article-content') !== null; document.addEventListener('click', event => { if (!isOnArticlePage) return; @@ -41,10 +82,14 @@ export const openExternalLinksInNewTab = () => { const link = event.target.closest('a'); if (link) { + const currentLocation = window.location.href; + const linkHref = link.href; + + // Check against current location and custom domains const isInternalLink = - link.hostname === window.location.hostname || - link.href.includes(customDomain) || - link.href.includes(hostURL); + isSameHost(linkHref, currentLocation) || + (customDomain && isSameHost(linkHref, customDomain)) || + (hostURL && isSameHost(linkHref, hostURL)); if (!isInternalLink) { link.target = '_blank'; diff --git a/app/javascript/portal/specs/portal.spec.js b/app/javascript/portal/specs/portal.spec.js index 861950a57..5205c5d45 100644 --- a/app/javascript/portal/specs/portal.spec.js +++ b/app/javascript/portal/specs/portal.spec.js @@ -103,7 +103,6 @@ describe('openExternalLinksInNewTab', () => { openExternalLinksInNewTab(); const link = simulateClick('#external'); - expect(link.target).toBe('_blank'); expect(link.rel).toBe('noopener noreferrer'); }); @@ -139,4 +138,48 @@ describe('openExternalLinksInNewTab', () => { expect(link.target).toBe('_blank'); expect(link.rel).toBe('noopener noreferrer'); }); + + it('opens external links in a new tab even if customDomain is empty', () => { + window = dom.window; + window.portalConfig = { + hostURL: 'app.chatwoot.com', + }; + + global.window = window; + + openExternalLinksInNewTab(); + + const link = simulateClick('#external'); + const internal = simulateClick('#internal'); + const custom = simulateClick('#custom'); + + expect(link.target).toBe('_blank'); + expect(link.rel).toBe('noopener noreferrer'); + + expect(internal.target).not.toBe('_blank'); + // this will be blank since the configs customDomain is empty + // which is a fair expectation + expect(custom.target).toBe('_blank'); + }); + + it('opens external links in a new tab even if hostURL is empty', () => { + window = dom.window; + window.portalConfig = { + customDomain: 'custom.domain.com', + }; + + global.window = window; + + openExternalLinksInNewTab(); + + const link = simulateClick('#external'); + const internal = simulateClick('#internal'); + const custom = simulateClick('#custom'); + + expect(link.target).toBe('_blank'); + expect(link.rel).toBe('noopener noreferrer'); + + expect(internal.target).not.toBe('_blank'); + expect(custom.target).not.toBe('_blank'); + }); }); From b5ebc4763723e15ad3a04ab0b0ecb1e01087a2b1 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 28 May 2025 19:34:11 +0530 Subject: [PATCH 3/7] fix: Send CSAT survey only when agent can reply in conversation (#11584) Fixes https://github.com/chatwoot/chatwoot/issues/11569 ## Problem On platforms like WhatsApp and Facebook Messenger, customers cannot reply to messages after 24 hours (or other channel-specific messaging windows). Despite this limitation, the system continued sending CSAT surveys to customers outside their messaging window, making it impossible for them to respond. ## Solution Added a check for `conversation.can_reply?` in the `should_send_csat_survey?` method. This leverages the existing `MessageWindowService` which already handles all channel-specific messaging window logic. --- .../concerns/activity_message_handler.rb | 1 + .../concerns/csat_activity_message_handler.rb | 8 +++++ .../hook_execution_service.rb | 23 +++++++++++---- config/locales/en.yml | 2 ++ spec/models/conversation_spec.rb | 14 +++++++++ .../hook_execution_service_spec.rb | 29 ++++++++++++++++++- 6 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 app/models/concerns/csat_activity_message_handler.rb diff --git a/app/models/concerns/activity_message_handler.rb b/app/models/concerns/activity_message_handler.rb index 54e58b4d9..0d6741c7a 100644 --- a/app/models/concerns/activity_message_handler.rb +++ b/app/models/concerns/activity_message_handler.rb @@ -5,6 +5,7 @@ module ActivityMessageHandler include LabelActivityMessageHandler include SlaActivityMessageHandler include TeamActivityMessageHandler + include CsatActivityMessageHandler private diff --git a/app/models/concerns/csat_activity_message_handler.rb b/app/models/concerns/csat_activity_message_handler.rb new file mode 100644 index 000000000..7a2488c4d --- /dev/null +++ b/app/models/concerns/csat_activity_message_handler.rb @@ -0,0 +1,8 @@ +module CsatActivityMessageHandler + extend ActiveSupport::Concern + + def create_csat_not_sent_activity_message + content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window') + ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + end +end diff --git a/app/services/message_templates/hook_execution_service.rb b/app/services/message_templates/hook_execution_service.rb index a8a4c4318..8291c8a9c 100644 --- a/app/services/message_templates/hook_execution_service.rb +++ b/app/services/message_templates/hook_execution_service.rb @@ -17,7 +17,7 @@ class MessageTemplates::HookExecutionService ::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message? ::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting? ::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if inbox.enable_email_collect && should_send_email_collect? - ::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform if should_send_csat_survey? + handle_csat_survey end def should_send_out_of_office_message? @@ -65,13 +65,26 @@ class MessageTemplates::HookExecutionService true end - def should_send_csat_survey? + def handle_csat_survey return unless csat_enabled_conversation? - # only send CSAT once in a conversation - return if conversation.messages.where(content_type: :input_csat).present? + return if csat_already_sent? - true + # Only send CSAT if agent can still reply by checking the messaging window restriction + # https://www.chatwoot.com/docs/self-hosted/supported-features#outgoing-message-restriction + if within_messaging_window? + ::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform + else + conversation.create_csat_not_sent_activity_message + end + end + + def csat_already_sent? + conversation.messages.where(content_type: :input_csat).present? + end + + def within_messaging_window? + conversation.can_reply? end end MessageTemplates::HookExecutionService.prepend_mod_with('MessageTemplates::HookExecutionService') diff --git a/config/locales/en.yml b/config/locales/en.yml index 9a35197ad..b309e718c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -185,6 +185,8 @@ en: removed: '%{user_name} removed %{labels}' sla: added: '%{user_name} added SLA policy %{sla_name}' + csat: + not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions' removed: '%{user_name} removed SLA policy %{sla_name}' muted: '%{user_name} has muted the conversation' unmuted: '%{user_name} has unmuted the conversation' diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index aef91603d..ad3446a13 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -435,6 +435,20 @@ RSpec.describe Conversation do end end + describe '#create_csat_not_sent_activity_message' do + subject(:create_csat_not_sent_activity_message) { conversation.create_csat_not_sent_activity_message } + + let(:conversation) { create(:conversation) } + + it 'creates CSAT not sent activity message' do + create_csat_not_sent_activity_message + expect(Conversations::ActivityMessageJob) + .to(have_been_enqueued.at_least(:once).with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, + message_type: :activity, + content: 'CSAT survey not sent due to outgoing message restrictions' })) + end + end + describe 'unread_messages' do subject(:unread_messages) { conversation.unread_messages } diff --git a/spec/services/message_templates/hook_execution_service_spec.rb b/spec/services/message_templates/hook_execution_service_spec.rb index 24e40ea8d..6e97aac42 100644 --- a/spec/services/message_templates/hook_execution_service_spec.rb +++ b/spec/services/message_templates/hook_execution_service_spec.rb @@ -121,8 +121,9 @@ describe MessageTemplates::HookExecutionService do create(:message, conversation: conversation, message_type: 'incoming') end - it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled' do + it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled and can reply' do conversation.inbox.update(csat_survey_enabled: true) + allow(conversation).to receive(:can_reply?).and_return(true) conversation.resolved! Conversations::ActivityMessageJob.perform_now(conversation, @@ -172,6 +173,32 @@ describe MessageTemplates::HookExecutionService do expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation) expect(csat_survey).not_to have_received(:perform) end + + it 'will not call ::MessageTemplates::Template::CsatSurvey if cannot reply' do + conversation.inbox.update(csat_survey_enabled: true) + allow(conversation).to receive(:can_reply?).and_return(false) + + conversation.resolved! + Conversations::ActivityMessageJob.perform_now(conversation, + { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, + content: 'Conversation marked resolved!!' }) + + expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation) + expect(csat_survey).not_to have_received(:perform) + end + + it 'creates activity message when CSAT not sent due to messaging window restriction' do + conversation.inbox.update(csat_survey_enabled: true) + allow(conversation).to receive(:can_reply?).and_return(false) + allow(conversation).to receive(:create_csat_not_sent_activity_message) + + conversation.resolved! + Conversations::ActivityMessageJob.perform_now(conversation, + { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity, + content: 'Conversation marked resolved!!' }) + + expect(conversation).to have_received(:create_csat_not_sent_activity_message) + end end context 'when it is after working hours' do From c3d98fc06416eabe61dc151f9f92fb0bfb3595e9 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 29 May 2025 10:19:56 +0530 Subject: [PATCH 4/7] chore: Move URL comparison logic to utils (#11617) --- app/javascript/portal/portalHelpers.js | 47 +------------------------- package.json | 2 +- pnpm-lock.yaml | 10 +++--- 3 files changed, 7 insertions(+), 52 deletions(-) diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index 68e2cb6a8..bed50387e 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -2,6 +2,7 @@ import { createApp } from 'vue'; import VueDOMPurifyHTML from 'vue-dompurify-html'; import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer'; import { directive as onClickaway } from 'vue3-click-away'; +import { isSameHost } from '@chatwoot/utils'; import slugifyWithCounter from '@sindresorhus/slugify'; import PublicArticleSearch from './components/PublicArticleSearch.vue'; @@ -25,52 +26,6 @@ export const getHeadingsfromTheArticle = () => { return rows; }; -/** - * Converts various input formats to URL objects. - * Handles URL objects, domain strings, relative paths, and full URLs. - * @param {string|URL} input - Input to convert to URL object - * @returns {URL|null} URL object or null if input is invalid - */ -const toURL = input => { - if (!input) return null; - if (input instanceof URL) return input; - - if ( - typeof input === 'string' && - !input.includes('://') && - !input.startsWith('/') - ) { - return new URL(`https://${input}`); - } - - if (typeof input === 'string' && input.startsWith('/')) { - return new URL(input, window.location.origin); - } - - return new URL(input); -}; - -/** - * Determines if two URLs belong to the same host by comparing their normalized URL objects. - * Handles various input formats including URL objects, domain strings, relative paths, and full URLs. - * Returns false if either URL cannot be parsed or normalized. - * @param {string|URL} url1 - First URL to compare - * @param {string|URL} url2 - Second URL to compare - * @returns {boolean} True if both URLs have the same host, false otherwise - */ -const isSameHost = (url1, url2) => { - try { - const urlObj1 = toURL(url1); - const urlObj2 = toURL(url2); - - if (!urlObj1 || !urlObj2) return false; - - return urlObj1.hostname === urlObj2.hostname; - } catch (error) { - return false; - } -}; - export const openExternalLinksInNewTab = () => { const { customDomain, hostURL } = window.portalConfig; const isOnArticlePage = diff --git a/package.json b/package.json index 90b7708f6..6280ec7fd 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", "@chatwoot/prosemirror-schema": "1.1.1-next", - "@chatwoot/utils": "^0.0.43", + "@chatwoot/utils": "^0.0.45", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", "@hcaptcha/vue3-hcaptcha": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af7703fc6..606ebbb63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: 1.1.1-next version: 1.1.1-next '@chatwoot/utils': - specifier: ^0.0.43 - version: 0.0.43 + specifier: ^0.0.45 + version: 0.0.45 '@formkit/core': specifier: ^1.6.7 version: 1.6.7 @@ -406,8 +406,8 @@ packages: '@chatwoot/prosemirror-schema@1.1.1-next': resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==} - '@chatwoot/utils@0.0.43': - resolution: {integrity: sha512-kMIXAGebCak9qOi68QnGer+rQLLo/z2N9cR+7tvGdZCW0ThDiVCF7JbHYHVDlYsdDFIx0FLlyIdCfEbooVT2Dw==} + '@chatwoot/utils@0.0.45': + resolution: {integrity: sha512-zqmuri6MrEFAY1tLv7Z3HBy4Ig60LhSrLkEiHegVsOVSxPv4Bedq+xmAW7LphvcLNgbkkvu17MU91gvMVlpEHw==} engines: {node: '>=10'} '@codemirror/commands@6.7.0': @@ -5255,7 +5255,7 @@ snapshots: prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3) prosemirror-view: 1.34.1 - '@chatwoot/utils@0.0.43': + '@chatwoot/utils@0.0.45': dependencies: date-fns: 2.30.0 From f6510e0d4367f5a7a9e9a142032483063be34358 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 29 May 2025 11:23:27 +0530 Subject: [PATCH 5/7] docs: add swagger spec for accounts API (#11620) --- swagger/definitions/index.yml | 7 + .../request/account/update_payload.yml | 49 +++ .../definitions/resource/account_detail.yml | 84 +++++ .../resource/account_show_response.yml | 13 + swagger/paths/application/accounts/show.yml | 28 ++ swagger/paths/application/accounts/update.yml | 43 +++ swagger/paths/index.yml | 9 + swagger/swagger.json | 331 ++++++++++++++++++ swagger/tag_groups/application_swagger.json | 199 +++++++++++ swagger/tag_groups/client_swagger.json | 199 +++++++++++ swagger/tag_groups/other_swagger.json | 199 +++++++++++ swagger/tag_groups/platform_swagger.json | 199 +++++++++++ 12 files changed, 1360 insertions(+) create mode 100644 swagger/definitions/request/account/update_payload.yml create mode 100644 swagger/definitions/resource/account_detail.yml create mode 100644 swagger/definitions/resource/account_show_response.yml create mode 100644 swagger/paths/application/accounts/show.yml create mode 100644 swagger/paths/application/accounts/update.yml diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index 57e10e2dd..faf947217 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -60,6 +60,10 @@ webhook: $ref: ./resource/webhook.yml account: $ref: ./resource/account.yml +account_detail: + $ref: ./resource/account_detail.yml +account_show_response: + $ref: ./resource/account_show_response.yml account_user: $ref: ./resource/account_user.yml platform_account: @@ -87,6 +91,9 @@ public_inbox: account_create_update_payload: $ref: ./request/account/create_update_payload.yml +account_update_payload: + $ref: ./request/account/update_payload.yml + account_user_create_update_payload: $ref: ./request/account_user/create_update_payload.yml diff --git a/swagger/definitions/request/account/update_payload.yml b/swagger/definitions/request/account/update_payload.yml new file mode 100644 index 000000000..b2e47cbc2 --- /dev/null +++ b/swagger/definitions/request/account/update_payload.yml @@ -0,0 +1,49 @@ +type: object +properties: + name: + type: string + description: Name of the account + example: 'My Account' + locale: + type: string + description: The locale of the account + example: 'en' + domain: + type: string + description: The domain of the account + example: 'example.com' + support_email: + type: string + description: The support email of the account + example: 'support@example.com' + # Settings parameters (stored in settings JSONB column) + auto_resolve_after: + type: integer + minimum: 10 + maximum: 1439856 + nullable: true + description: Auto resolve conversations after specified minutes + example: 1440 + auto_resolve_message: + type: string + nullable: true + description: Message to send when auto resolving + example: "This conversation has been automatically resolved due to inactivity" + auto_resolve_ignore_waiting: + type: boolean + nullable: true + description: Whether to ignore waiting conversations for auto resolve + example: false + # Custom attributes parameters (stored in custom_attributes JSONB column) + industry: + type: string + description: Industry type + example: "Technology" + company_size: + type: string + description: Company size + example: "50-100" + timezone: + type: string + description: Account timezone + example: "UTC" \ No newline at end of file diff --git a/swagger/definitions/resource/account_detail.yml b/swagger/definitions/resource/account_detail.yml new file mode 100644 index 000000000..0ff463bae --- /dev/null +++ b/swagger/definitions/resource/account_detail.yml @@ -0,0 +1,84 @@ +type: object +properties: + id: + type: number + description: Account ID + name: + type: string + description: Name of the account + locale: + type: string + description: The locale of the account + domain: + type: string + description: The domain of the account + support_email: + type: string + description: The support email of the account + status: + type: string + description: The status of the account + created_at: + type: string + format: date-time + description: The creation date of the account + cache_keys: + type: object + description: Cache keys for the account + features: + type: array + items: + type: string + description: Enabled features for the account + settings: + type: object + description: Account settings + properties: + auto_resolve_after: + type: number + description: Auto resolve conversations after specified minutes + auto_resolve_message: + type: string + description: Message to send when auto resolving + auto_resolve_ignore_waiting: + type: boolean + description: Whether to ignore waiting conversations for auto resolve + custom_attributes: + type: object + description: Custom attributes of the account + properties: + plan_name: + type: string + description: Subscription plan name + subscribed_quantity: + type: number + description: Subscribed quantity + subscription_status: + type: string + description: Subscription status + subscription_ends_on: + type: string + format: date + description: Subscription end date + industry: + type: string + description: Industry type + company_size: + type: string + description: Company size + timezone: + type: string + description: Account timezone + logo: + type: string + description: Account logo URL + onboarding_step: + type: string + description: Current onboarding step + marked_for_deletion_at: + type: string + format: date-time + description: When account was marked for deletion + marked_for_deletion_reason: + type: string + description: Reason for account deletion \ No newline at end of file diff --git a/swagger/definitions/resource/account_show_response.yml b/swagger/definitions/resource/account_show_response.yml new file mode 100644 index 000000000..208b27218 --- /dev/null +++ b/swagger/definitions/resource/account_show_response.yml @@ -0,0 +1,13 @@ +allOf: + - $ref: '#/components/schemas/account_detail' + - type: object + properties: + latest_chatwoot_version: + type: string + description: Latest version of Chatwoot available + example: "3.0.0" + subscribed_features: + type: array + items: + type: string + description: List of subscribed enterprise features (if enterprise edition is enabled) \ No newline at end of file diff --git a/swagger/paths/application/accounts/show.yml b/swagger/paths/application/accounts/show.yml new file mode 100644 index 000000000..56e0c8052 --- /dev/null +++ b/swagger/paths/application/accounts/show.yml @@ -0,0 +1,28 @@ +tags: + - Account +operationId: get-account-details +summary: Get account details +description: Get the details of the current account +security: + - userApiKey: [] +parameters: + - $ref: '#/components/parameters/account_id' +responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/account_show_response' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' \ No newline at end of file diff --git a/swagger/paths/application/accounts/update.yml b/swagger/paths/application/accounts/update.yml new file mode 100644 index 000000000..23c9c40f8 --- /dev/null +++ b/swagger/paths/application/accounts/update.yml @@ -0,0 +1,43 @@ +tags: + - Account +operationId: update-account +summary: Update account +description: Update account details, settings, and custom attributes +security: + - userApiKey: [] +parameters: + - $ref: '#/components/parameters/account_id' +requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/account_update_payload' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/account_update_payload' +responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/account_detail' + '401': + description: Unauthorized (requires administrator role) + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' \ No newline at end of file diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index 379b2eef0..a70800b89 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -166,6 +166,15 @@ # ------------ Application API routes ------------# +# Accounts +/api/v1/accounts/{id}: + parameters: + - $ref: '#/components/parameters/account_id' + get: + $ref: ./application/accounts/show.yml + patch: + $ref: ./application/accounts/update.yml + # AgentBots /api/v1/accounts/{account_id}/agent_bots: parameters: diff --git a/swagger/swagger.json b/swagger/swagger.json index 943b7a3e8..b9ecbcf27 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1476,6 +1476,138 @@ } } }, + "/api/v1/accounts/{id}": { + "parameters": [ + { + "$ref": "#/components/parameters/account_id" + } + ], + "get": { + "tags": [ + "Account" + ], + "operationId": "get-account-details", + "summary": "Get account details", + "description": "Get the details of the current account", + "security": [ + { + "userApiKey": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/account_id" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/account_show_response" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + }, + "404": { + "description": "Account not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Account" + ], + "operationId": "update-account", + "summary": "Update account", + "description": "Update account details, settings, and custom attributes", + "security": [ + { + "userApiKey": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/account_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/account_update_payload" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/account_update_payload" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/account_detail" + } + } + } + }, + "401": { + "description": "Unauthorized (requires administrator role)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + }, + "404": { + "description": "Account not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + } + } + } + }, "/api/v1/accounts/{account_id}/agent_bots": { "parameters": [ { @@ -8774,6 +8906,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -9113,6 +9384,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/application_swagger.json b/swagger/tag_groups/application_swagger.json index 33e75183f..2a8162358 100644 --- a/swagger/tag_groups/application_swagger.json +++ b/swagger/tag_groups/application_swagger.json @@ -7135,6 +7135,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -7474,6 +7613,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/client_swagger.json b/swagger/tag_groups/client_swagger.json index 6d471da43..cefc39324 100644 --- a/swagger/tag_groups/client_swagger.json +++ b/swagger/tag_groups/client_swagger.json @@ -1978,6 +1978,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -2317,6 +2456,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/other_swagger.json b/swagger/tag_groups/other_swagger.json index fc61e8721..2cc045747 100644 --- a/swagger/tag_groups/other_swagger.json +++ b/swagger/tag_groups/other_swagger.json @@ -1393,6 +1393,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -1732,6 +1871,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/platform_swagger.json b/swagger/tag_groups/platform_swagger.json index 18ec796a0..085e969ab 100644 --- a/swagger/tag_groups/platform_swagger.json +++ b/swagger/tag_groups/platform_swagger.json @@ -2154,6 +2154,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -2493,6 +2632,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ From 23a804512aed3849f4500ecf76961516a6d4f8dc Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 29 May 2025 01:05:10 -0600 Subject: [PATCH 6/7] feat: Update the UI to support the change for Copilot as a universal copilot (#11618) Co-authored-by: Shivam Mishra Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../dashboard/assets/scss/widgets/_tabs.scss | 2 +- .../Conversation/SidepanelSwitch.vue | 87 +++++++++++++++++++ ...ory.vue => SidebarActionsHeader.story.vue} | 16 +++- .../components-next/SidebarActionsHeader.vue | 47 ++++++++++ .../components-next/copilot/Copilot.vue | 43 ++++++--- .../components-next/copilot/CopilotHeader.vue | 32 ------- .../components-next/sidebar/Sidebar.vue | 2 +- .../dashboard/components/ChatList.vue | 2 +- .../dashboard/components/ChatListHeader.vue | 8 +- .../components/widgets/ChatTypeTabs.vue | 3 +- .../widgets/conversation/ConversationBox.vue | 21 +---- .../widgets/conversation/ConversationCard.vue | 4 +- .../conversation/ConversationHeader.vue | 38 ++------ .../conversation/ConversationSidebar.vue | 60 +++++-------- .../widgets/conversation/MessagesView.vue | 36 -------- .../dashboard/i18n/locale/en/general.json | 3 +- .../dashboard/conversation/ContactPanel.vue | 25 +++--- .../conversation/ConversationView.vue | 32 ++++--- 18 files changed, 248 insertions(+), 213 deletions(-) create mode 100644 app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue rename app/javascript/dashboard/components-next/{copilot/CopilotHeader.story.vue => SidebarActionsHeader.story.vue} (50%) create mode 100644 app/javascript/dashboard/components-next/SidebarActionsHeader.vue delete mode 100644 app/javascript/dashboard/components-next/copilot/CopilotHeader.vue diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index 72a2e6be8..72773de7f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -3,7 +3,7 @@ } .tabs--container--with-border { - @apply border-b border-n-weak; + @apply border-b border-b-n-weak; } .tabs--container--compact.tab--chat-type { diff --git a/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue b/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue new file mode 100644 index 000000000..ec3a8d03a --- /dev/null +++ b/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue @@ -0,0 +1,87 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue b/app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue similarity index 50% rename from app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue rename to app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue index 78a345093..13d528240 100644 --- a/app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue +++ b/app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue @@ -1,21 +1,29 @@ diff --git a/app/javascript/dashboard/components-next/SidebarActionsHeader.vue b/app/javascript/dashboard/components-next/SidebarActionsHeader.vue new file mode 100644 index 000000000..210ddfa0e --- /dev/null +++ b/app/javascript/dashboard/components-next/SidebarActionsHeader.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue index 5feb474a6..6fb45c278 100644 --- a/app/javascript/dashboard/components-next/copilot/Copilot.vue +++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue @@ -3,13 +3,15 @@ import { nextTick, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useTrack } from 'dashboard/composables'; import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; +import { useUISettings } from 'dashboard/composables/useUISettings'; import CopilotInput from './CopilotInput.vue'; import CopilotLoader from './CopilotLoader.vue'; import CopilotAgentMessage from './CopilotAgentMessage.vue'; import CopilotAssistantMessage from './CopilotAssistantMessage.vue'; import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue'; -import Icon from '../icon/Icon.vue'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; +import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue'; const props = defineProps({ supportAgent: { @@ -54,10 +56,6 @@ const useSuggestion = opt => { useTrack(COPILOT_EVENTS.SEND_SUGGESTED); }; -const handleReset = () => { - emit('reset'); -}; - const chatContainer = ref(null); const scrollToBottom = async () => { @@ -82,6 +80,21 @@ const promptOptions = [ }, ]; +const { updateUISettings } = useUISettings(); + +const closeCopilotPanel = () => { + updateUISettings({ + is_copilot_panel_open: false, + is_contact_sidebar_open: false, + }); +}; + +const handleSidebarAction = action => { + if (action === 'reset') { + emit('reset'); + } +}; + watch( [() => props.messages, () => props.isCaptainTyping], () => { @@ -93,6 +106,18 @@ watch(