Compare commits

...
Author SHA1 Message Date
PranavandGitHub d18be84b45 Merge branch 'develop' into feature/service-worker-caching 2026-03-11 23:48:51 -07:00
Sojan JoseandGitHub 199dcd382e fix: Skip redundant contact saves in ContactIdentifyAction (#13778)
When the SDK sends identify calls with identical payloads (common on
every page load), `save!` fires even though no attributes changed. While
Rails skips the actual UPDATE SQL, it still opens a transaction, runs
all callbacks (including validation queries like `Contact Exists?`), and
triggers `after_commit` hooks — all for a no-op.

This adds a `changed?` guard before `save!` to skip it entirely when no
attributes have actually changed.

**How to test**

- Trigger an identify call via the SDK with a contact's existing
attributes (same name, email, custom_attributes, etc.)
- The contact should not fire a save (no transaction, no callbacks)
- Trigger an identify call with a changed attribute — save should work
normally

**What changed**

- `ContactIdentifyAction#update_contact`: guard `save!` with `changed?`
check
- Added specs to verify `save!` is skipped for unchanged params and
avatar job still enqueues independently
2026-03-11 21:40:38 -07:00
PranavandGitHub c6f82783ba chore: Remove message touch:true, use combined update query (#13770)
Removes touch: true from the belongs_to :conversation association on
Message and consolidates the conversation timestamp update into the
existing set_conversation_activity callback.

Previously, every message save triggered two separate UPDATE queries on
the conversation — one from Rails' touch (updating updated_at) and
another from set_conversation_activity (updating last_activity_at). This
combines both into a single update_columns call, reducing write load on
the conversations table on every message creation.

### What changed
- Removed touch: true from belongs_to :conversation in Message
- Added updated_at: created_at to the existing update_columns call in
set_conversation_activity
2026-03-11 07:31:46 -07:00
Tanmay Deep SharmaandGitHub 9b3f0029a4 fix: override minimatch to patch ReDoS vulnerability (#13769)
## Description

Remediates high severity ReDoS vulnerability in minimatch
(CVE-2026-27903) flagged by Vanta/Dependabot.
minimatch is a transitive dev-only dependency (via eslint and
tailwindcss build tooling) — not shipped to production. Added pnpm
overrides to force patched versions:
- minimatch@<4 → 3.1.5
- minimatch@>=9.0.0 <9.0.7 → 9.0.9

Closes:
https://linear.app/chatwoot/issue/CW-6595/vanta-remediate-high-vulnerabilities-identified-in-packages-are

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- No production impact — minimatch is only used in dev tooling, not at
runtime
- pnpm install completes successfully

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-03-11 16:48:48 +05:30
6e46be36c8 fix: Add fix to only allow confirmed agents to used in Agent Assingments at Macros/Automations (#13225)
# Pull Request Template

## Description

Unconfirmed agents (pending email verification) were incorrectly
appearing in the "assign agent" dropdown for macros and automations.
This fix filters out unconfirmed agents from these dropdowns and adds
backend validation to prevent assignment of unconfirmed agents.

Fixes #13223

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Backend tests:**
```bash
docker compose run --rm rails bundle exec rspec spec/services/action_service_spec.rb
```
- Added tests for confirmed agent assignment (should succeed)
- Added tests for unconfirmed agent assignment (should be skipped)

**Frontend tests:**
```bash
docker compose run --rm rails pnpm test app/javascript/dashboard/composables/spec/useMacros.spec.js
```
- Updated mocks to use `getVerifiedAgents` getter

**Manual testing:**
1. Create an unconfirmed agent via platform
2. Navigate to Settings → Macros → New Macro → Add "Assign Agent" action
3. Verify unconfirmed agent does NOT appear in dropdown
4. Navigate to Settings → Automations → New Automation → Add "Assign
Agent" action
5. Verify unconfirmed agent does NOT appear in dropdown

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-11 02:01:53 -07:00
Aakash BakhleandGitHub 87f5af4caa fix: playground captain v2 scenarios (#13747)
# Pull Request Template

## Description

Playground now uses v2. It was only wired to use v1. Traces get `source:
playground` on langfuse when playground has been used.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

locally and specs
<img width="1806" height="1276" alt="image"
src="https://github.com/user-attachments/assets/41ef4eb3-52b1-4b8e-9a4f-e8510c90cb39"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-03-11 14:05:16 +05:30
dbe35252bc fix: Use handoff_key for scenarios (#13755)
# Pull Request Template

## Description

Ensure agent function names stay within OpenAI's 64-char limit
(ai-agents prepends "handoff_to_").

Add HANDOFF_TITLE_SLUG_MAX_LENGTH and
handoff_key generation: persisted records use `scenario_{id}_agent`; new
records use a truncated title slug.
Assistant scenario keys and agent_name now reference the generated
handoff key.

fixes :

`Invalid 'messages[9].tool_calls[0].function.name': string too long.
Expected a string with maximum length 64, but got a string with length
95 instead.`

## Type of change


- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

Tested locally
<img width="1806" height="1044" alt="image"
src="https://github.com/user-attachments/assets/40cd7a3d-3d97-43a8-bd56-d3f5d63abbda"
/>

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-03-11 14:01:25 +05:30
Tanmay Deep SharmaandGitHub de8aa48b83 feat: make assignment_v2 feature available to all accounts (#13764)
## Description

Makes the assignment_v2 feature flag available to all installations by
removing the chatwoot_internal restriction. Previously this feature was
hidden from self-hosted installations; this change surfaces it in the
feature flags UI so any Chatwoot instance can enable it.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-03-11 13:46:53 +05:30
Shivam MishraandGitHub 43977a1927 chore: upgrade packages to resolve dependency advisories (#13762)
This PR generally upgrades the available packages clearing existing
transient package vulnerabilities

### minimatch

This is mostly a dev-dependency, it was in the main dependency for
`@formkit/vue`, this PR clears it.
It still remains a dependency for tailwind and tailwind typograpghy, but
it's tolerable
_Fixes: CW-6594 CW-6592 CW-6598_


### Axios

Upgraded the dependency directly. Fixed it
_Fixes: CW-6591_

### dompurify

Upgraded the dependency, it's in the safe range now
_Fixes: CW-6611 CW-6610_

### ajv

Dev dependency, can be safely ignored
_Fixes: CW-6606 CW-6604_

### @tootallnate/once

Dev dependency, comes from Histoire, can be safely ignored
_Fixes: CW-6603_
2026-03-11 13:20:17 +05:30
Sivin VargheseandGitHub a9cabad529 chore: Hide reply-to when copilot is active (#13749) 2026-03-11 11:30:30 +05:30
Sojan JoseandGitHub fdc326094a docs(swagger): document account label endpoints (#13760)
Documents the missing account-level label CRUD endpoints in Chatwoot's
Swagger output so label management is discoverable alongside the
existing contact and conversation label APIs.

Fixes: none
Closes: none

Why
The account-level label API already exists in Chatwoot, but it was
missing from the published Swagger spec. That made label management
harder to discover even though contact and conversation label assignment
endpoints were already documented.

What this change does
- adds a `Labels` tag to the application Swagger docs
- adds the label resource and create/update payload schemas
- documents `GET/POST /api/v1/accounts/{account_id}/labels`
- documents `GET/PATCH/DELETE /api/v1/accounts/{account_id}/labels/{id}`
- regenerates the compiled Swagger JSON artifacts

Validation
- rebuilt the Swagger JSON from the source YAML
- verified the new label endpoints appear in `swagger/swagger.json`
- verified the new label endpoints appear in
`swagger/tag_groups/application_swagger.json`
- started the local Rails server and confirmed `/swagger` and
`/swagger/swagger.json` return `200 OK`
2026-03-10 22:24:16 -07:00
9a9398b386 feat: validate OpenAPI spec using Skooma (#13623)
Adds Skooma-based OpenAPI validation so SDK-facing request specs can
assert that documented request and response contracts match real Rails
behavior. This also upgrades the spec to OpenAPI 3.1 and fixes contract
drift uncovered while validating core application and platform
resources.

Closes
None

Why
We want CI to catch OpenAPI drift before it reaches SDK consumers. While
wiring validation in, this PR surfaced several mismatches between the
documented contract and what the Rails endpoints actually accept or
return.

What this change does
- Adds Skooma-backed OpenAPI validation to the request spec flow and a
dedicated OpenAPI validation spec.
- Migrates nullable schema definitions to OpenAPI 3.1-compatible unions.
- Updates core SDK-facing schemas and payloads across accounts,
contacts, conversations, inboxes, messages, teams, reporting events, and
platform account resources.
- Documents concrete runtime cases that were previously missing or
inaccurate, including nested `profile` update payloads, multipart avatar
uploads, required profile update bodies, nullable inbox feature flags,
and message sender types that include both `Captain::Assistant` and
senderless activity-style messages.
- Regenerates the committed Swagger JSON and tag-group artifacts used by
CI sync checks.

Validation
- `bundle exec rake swagger:build`
- `bundle exec rspec spec/swagger/openapi_spec.rb`

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-10 18:33:55 -07:00
dc0e5eb465 fix: optimize message query with account_id filter (#13759)
## Description

This PR optimizes message queries by explicitly filtering with
`account_id` so the database can use the existing indexes more
efficiently.

Changes:
- Add `account_id` to message query filters to improve index
utilization.
- Update `last_incoming_message` query to include `account_id`.
- Avoid unnecessary preloading of `contact_inboxes` where it is not
required.
- Update specs to ensure `account_id` is set correctly in
message-related tests.

These changes reduce query cost and improve performance for message
lookups, especially on large accounts.

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2026-03-10 16:46:20 -07:00
PranavandGitHub b023487c3e Merge branch 'develop' into feature/service-worker-caching 2026-03-09 05:09:16 -07:00
PranavandGitHub 40d944b7d7 Merge branch 'develop' into feature/service-worker-caching 2026-01-29 19:57:12 -08:00
Muhsin KelothandGitHub b7a3bb892d Merge branch 'develop' into feature/service-worker-caching 2026-01-12 14:00:43 +04:00
Vinay KeerthiandGitHub 08c415ea0e Merge branch 'develop' into feature/service-worker-caching 2026-01-12 14:13:27 +05:30
Sivin VargheseandGitHub 6d662cf339 Merge branch 'develop' into feature/service-worker-caching 2026-01-12 09:45:24 +05:30
Pranav 9d32e27c98 Add a check with http 2026-01-11 11:31:32 -08:00
Pranav 52625c8138 remove unnecessary comments 2026-01-11 11:28:28 -08:00
Pranav 1b9f496832 remove unnecessary comment 2026-01-11 11:26:40 -08:00
Pranav 3527c07e94 Cleanup the code to make more readable 2026-01-11 11:18:17 -08:00
PranavandClaude Opus 4.5 b345826566 refactor: Simplify prefetch by moving to activate event
Remove message-based prefetch in favor of prefetching on activate:
- Remove PREFETCH_ASSETS message listener from service worker
- Remove prefetchAssets() call and idle callback from pushHelper.js
- Call prefetchAssets() directly in activate event

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:14:35 -08:00
PranavandClaude Opus 4.5 6d3d76982a refactor: Remove HTML shell caching for navigation
Remove networkFirstWithShellCache - offline shell caching has limited
value for a real-time chat app that requires network connectivity.

The service worker now only caches static assets (JS, CSS, fonts, images).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:58:13 -08:00
PranavandClaude Opus 4.5 59427c4de3 fix: Include assets (fonts, images) from manifest
Add entry.assets array to precache list for fonts and images.
Increases precached assets from 69 to 86.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:54:05 -08:00
PranavandClaude Opus 4.5 7787ec1265 fix: Include CSS files from manifest css arrays
The Vite manifest has CSS files in two places:
- entry.file (main CSS files)
- entry.css (CSS imported by JS modules)

The previous code only read entry.file, missing CSS from entry.css.
This increases precached assets from 57 to 69.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:51:27 -08:00
PranavandClaude Opus 4.5 8daac4cdd1 fix: Construct CDN asset URLs correctly with protocol and path
- Add https:// protocol to ASSET_CDN_HOST if missing
- Include asset path (/vite/) when constructing CDN URLs
- Fix URL: https://cdn.example.com/vite/assets/file.css

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:13:39 -08:00
PranavandClaude Opus 4.5 adbee23e12 refactor: Colocate service worker build script with source files
Move build script from scripts/build-service-worker.js to
app/javascript/service-worker/build.js to keep all service worker
related code in one place.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 19:30:15 -08:00
PranavandClaude Opus 4.5 38232a73dc refactor: Use stable cache name with incremental asset updates
Instead of invalidating the entire cache on every deployment (via git hash
in cache name), use a stable cache name and leverage Vite's content hashes
in filenames.

Changes:
- Use fixed cache name 'chatwoot-assets-v1' instead of 'chatwoot-{git-hash}'
- Add cleanupStaleAssets() to remove old assets not in current manifest
- Unchanged files (same content hash) stay cached across deployments
- Only new/changed files are fetched
- Remove unused getCacheVersion() and CACHE_VERSION references

This significantly reduces bandwidth on deployments since most assets
don't change between releases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 16:28:42 -08:00
PranavandClaude Opus 4.5 0d21a711e7 fix: Build service worker after assets to include manifest
Two issues were causing the service worker to not cache assets in production:

1. The prefetch baseUrl was hardcoded to /vite-dev/ which only exists in
   development. In production, assets are served from /packs/. Added
   ASSET_PATH build-time constant to use the correct path per environment.

2. The service worker was built before assets:precompile, so the Vite
   manifest didn't exist yet, resulting in 0 precached assets. Moved
   the SW build to after_assets_precompile so it can read the manifest.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 16:18:00 -08:00
PranavandClaude Opus 4.5 f5d538ba48 fix: Use bare identifiers for Vite define replacement in service worker
Vite's define option replaces identifiers, not string literals.
Changed from '__CACHE_VERSION__' (string) to __CACHE_VERSION__ (identifier)
so Vite can properly inject the values at build time.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:38:12 -08:00
PranavandClaude Opus 4.5 f713678328 refactor: Simplify service worker implementation
Remove over-engineering from asset caching service worker:
- Remove retry logic, stats tracking, unused message handlers
- Inline helper functions used only once
- Reduce sw-runtime.js from 314 to 170 lines (46% reduction)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:02:34 -08:00
PranavandClaude Opus 4.5 d34df7c314 feat: Add service worker for asset caching
- Add Workbox-based service worker to cache JS, CSS, and fonts
- JS/CSS cached for 30 days, fonts for 1 year
- Support for CDN assets via ASSET_CDN_HOST
- Version-based cache invalidation on deployments
- Automatic cache cleanup for old versions
- Build service worker during asset precompilation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 14:48:23 -08:00
96 changed files with 4956 additions and 1534 deletions
+2 -2
View File
@@ -93,8 +93,8 @@ jobs:
exit 1
fi
mkdir -p ~/tmp
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.19.0/openapi-generator-cli-7.19.0.jar > ~/tmp/openapi-generator-cli-7.19.0.jar
java -jar ~/tmp/openapi-generator-cli-7.19.0.jar validate -i swagger/swagger.json
# Bundle audit
- run:
+1
View File
@@ -103,4 +103,5 @@ CLAUDE.local.md
.histoire
.pnpm-store/*
local/
sw.js
Procfile.worktree
+1
View File
@@ -268,6 +268,7 @@ group :development, :test do
gem 'seed_dump'
gem 'shoulda-matchers'
gem 'simplecov', '>= 0.21', require: false
gem 'skooma'
gem 'spring'
gem 'spring-watcher-listen'
end
+11
View File
@@ -473,6 +473,12 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
json_skooma (0.2.5)
bigdecimal
hana (~> 1.3)
regexp_parser (~> 2.0)
uri-idna (~> 0.2)
zeitwerk (~> 2.6)
judoscale-rails (1.8.2)
judoscale-ruby (= 1.8.2)
railties
@@ -910,6 +916,9 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
skooma (0.3.7)
json_skooma (~> 0.2.5)
zeitwerk (~> 2.6)
slack-ruby-client (2.7.0)
faraday (>= 2.0.1)
faraday-mashify
@@ -970,6 +979,7 @@ GEM
unicode-emoji (4.0.4)
uniform_notifier (1.17.0)
uri (1.1.1)
uri-idna (0.3.1)
uri_template (0.7.0)
valid_email2 (5.2.6)
activemodel (>= 3.2)
@@ -1143,6 +1153,7 @@ DEPENDENCIES
sidekiq_alive
simplecov (>= 0.21)
simplecov_json_formatter
skooma
slack-ruby-client (~> 2.7.0)
spring
spring-watcher-listen
+1 -1
View File
@@ -104,7 +104,7 @@ class ContactIdentifyAction
# blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs
@contact.save!
@contact.save! if @contact.changed?
enqueue_avatar_job
end
@@ -201,7 +201,9 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def fetch_contact
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
contact_scope = Current.account.contacts
contact_scope = contact_scope.includes(contact_inboxes: [:inbox]) if @include_contact_inboxes
@contact = contact_scope.find(params[:id])
end
def process_avatar_from_url
@@ -18,10 +18,18 @@ const newMessage = ref('');
const isLoading = ref(false);
const formatMessagesForApi = () => {
return messages.value.map(message => ({
role: message.sender,
content: message.content,
}));
return messages.value.map(message => {
const payload = {
role: message.sender,
content: message.content,
};
if (message.sender === 'assistant' && message.agentName) {
payload.agent_name = message.agentName;
}
return payload;
});
};
const resetConversation = () => {
@@ -62,6 +70,7 @@ const sendMessage = async () => {
messages.value.push({
content: data.response,
sender: 'assistant',
agentName: data.agent_name,
timestamp: new Date().toISOString(),
});
} catch (error) {
@@ -78,7 +78,7 @@ const onSend = () => {
<div
v-else-if="isGeneratingContent"
key="loading-state"
class="bg-n-iris-5 rounded min-h-16 w-full mb-4 p-4 flex items-start"
class="bg-n-iris-5 rounded min-h-[4.75rem] w-full mb-4 p-4 flex items-start"
>
<div class="flex items-center gap-2">
<CaptainLoader class="text-n-iris-10 size-4" />
@@ -162,7 +162,8 @@ export default {
this.inReplyTo?.id &&
!this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
!this.is360DialogWhatsAppChannel
!this.is360DialogWhatsAppChannel &&
!this.copilot.isActive.value
);
},
showWhatsappTemplates() {
@@ -1464,7 +1465,7 @@ export default {
}
.reply-box__top {
@apply relative py-0 px-4 -mt-px;
@apply relative py-0 px-3 -mt-px;
}
.emoji-dialog {
@@ -14,7 +14,7 @@ const emit = defineEmits(['dismiss']);
<template>
<div
class="reply-editor bg-n-slate-9/10 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
class="reply-editor bg-n-slate-9/10 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5"
>
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
<div class="flex-grow gap-1 mt-px text-xs truncate">
@@ -38,7 +38,7 @@ describe('useAutomation', () => {
});
useMapGetter.mockImplementation(getter => {
const getterMap = {
'agents/getAgents': agents,
'agents/getVerifiedAgents': agents,
'campaigns/getAllCampaigns': campaigns,
'contacts/getContacts': contacts,
'inboxes/getInboxes': inboxes,
@@ -111,7 +111,7 @@ describe('useMacros', () => {
useStoreGetters.mockReturnValue({
'labels/getLabels': { value: mockLabels },
'teams/getTeams': { value: mockTeams },
'agents/getAgents': { value: mockAgents },
'agents/getVerifiedAgents': { value: mockAgents },
});
});
@@ -167,7 +167,7 @@ describe('useMacros', () => {
useStoreGetters.mockReturnValue({
'labels/getLabels': { value: [] },
'teams/getTeams': { value: [] },
'agents/getAgents': { value: [] },
'agents/getVerifiedAgents': { value: [] },
});
const { getMacroDropdownValues } = useMacros();
@@ -20,7 +20,7 @@ import {
export default function useAutomationValues() {
const getters = useStoreGetters();
const { t } = useI18n();
const agents = useMapGetter('agents/getAgents');
const agents = useMapGetter('agents/getVerifiedAgents');
const campaigns = useMapGetter('campaigns/getAllCampaigns');
const contacts = useMapGetter('contacts/getContacts');
const inboxes = useMapGetter('inboxes/getInboxes');
@@ -13,7 +13,7 @@ export const useMacros = () => {
const labels = computed(() => getters['labels/getLabels'].value);
const teams = computed(() => getters['teams/getTeams'].value);
const agents = computed(() => getters['agents/getAgents'].value);
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
/**
* Get dropdown values based on the specified type
+20 -2
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import NotificationSubscriptions from '../api/notificationSubscription';
import auth from '../api/auth';
import { useAlert } from 'dashboard/composables';
@@ -16,7 +15,26 @@ export const verifyServiceWorkerExistence = (callback = () => {}) => {
navigator.serviceWorker
.register('/sw.js')
.then(registration => callback(registration))
.then(registration => {
// Check for updates on load
registration.update();
// Listen for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
// eslint-disable-next-line no-console
console.log('New service worker available');
}
});
});
callback(registration);
})
.catch(registrationError => {
// eslint-disable-next-line
console.log('SW registration failed: ', registrationError);
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const { build } = require('vite');
const ROOT_DIR = process.cwd();
const PATHS = {
manifest: path.join(ROOT_DIR, 'public/vite/.vite/manifest.json'),
swRuntime: path.join(__dirname, 'sw-runtime.js'),
swRuntimeBuilt: path.join(ROOT_DIR, 'tmp/sw-build/sw-runtime.js'),
pushHandlers: path.join(__dirname, 'push-handlers.js'),
buildDir: path.join(ROOT_DIR, 'tmp/sw-build'),
output: path.join(ROOT_DIR, 'public/sw.js'),
};
const buildSWContent = (runtimeBundle, pushHandlers, assetCount) => `
/* Service Worker for Chatwoot */
/* Generated: ${new Date().toISOString()} */
/* Precached assets: ${assetCount} */
${runtimeBundle}
${pushHandlers}
`;
function generateAssetManifest() {
const manifestPath = fs.existsSync(PATHS.manifest) ? PATHS.manifest : null;
if (!manifestPath) {
console.warn(
'⚠️ No Vite manifest found, skipping asset manifest generation'
);
return [];
}
console.log(`📋 Reading manifest from: ${manifestPath}`);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const assets = [];
const seen = new Set();
Object.entries(manifest).forEach(([, entry]) => {
const file = entry.file;
if (file && !seen.has(file)) {
seen.add(file);
if (file.endsWith('.js') || file.endsWith('.css')) {
assets.push({ url: file, revision: null });
}
}
if (entry.css) {
entry.css.forEach(cssFile => {
if (!seen.has(cssFile)) {
seen.add(cssFile);
assets.push({ url: cssFile, revision: null });
}
});
}
if (entry.assets) {
entry.assets.forEach(assetFile => {
if (!seen.has(assetFile)) {
seen.add(assetFile);
assets.push({ url: assetFile, revision: null });
}
});
}
});
console.log(`📦 Found ${assets.length} assets to precache`);
return assets;
}
async function buildServiceWorker() {
console.log('🔨 Building service worker...');
let assetOrigin = process.env.ASSET_CDN_HOST || '';
if (assetOrigin && !assetOrigin.startsWith('http')) {
assetOrigin = `https://${assetOrigin}`;
}
const isProduction = process.env.NODE_ENV === 'production';
const assetPath = '/vite/';
console.log(`📁 Root dir: ${ROOT_DIR}`);
console.log(`🌐 Asset origin: ${assetOrigin || '(local)'}`);
console.log(`🏭 Environment: ${isProduction ? 'production' : 'development'}`);
if (!isProduction) {
console.log(
'⚠️ Development mode: Using push notifications only (no caching)'
);
const devServiceWorker = fs.readFileSync(PATHS.pushHandlers, 'utf8');
fs.writeFileSync(PATHS.output, devServiceWorker);
console.log('✅ Development service worker created at public/sw.js');
return;
}
const assetManifest = generateAssetManifest();
try {
console.log('📦 Building service worker runtime...');
await build({
configFile: false,
css: {
postcss: {
plugins: [],
},
},
build: {
lib: {
entry: PATHS.swRuntime,
formats: ['iife'],
name: 'ServiceWorkerRuntime',
fileName: () => 'sw-runtime.js',
},
outDir: PATHS.buildDir,
emptyOutDir: true,
minify: true,
rollupOptions: {
output: {
inlineDynamicImports: true,
},
},
},
define: {
__ASSET_ORIGIN__: JSON.stringify(assetOrigin),
__ASSET_PATH__: JSON.stringify(assetPath),
__ASSET_MANIFEST__: JSON.stringify(assetManifest),
'process.env.NODE_ENV': JSON.stringify('production'),
},
logLevel: 'warn',
});
console.log('✅ Service worker runtime built');
const runtimeBundle = fs.readFileSync(PATHS.swRuntimeBuilt, 'utf8');
const pushHandlers = fs.readFileSync(PATHS.pushHandlers, 'utf8');
const finalServiceWorker = buildSWContent(
runtimeBundle,
pushHandlers,
assetManifest.length
);
fs.writeFileSync(PATHS.output, finalServiceWorker);
console.log('✅ Service worker built successfully at public/sw.js');
console.log(` - ${assetManifest.length} assets in precache manifest`);
} catch (error) {
console.error('❌ Failed to build service worker:', error);
process.exit(1);
}
}
buildServiceWorker().catch(err => {
console.error('❌ Unhandled error:', err);
process.exit(1);
});
@@ -1,5 +1,6 @@
/* eslint-disable no-restricted-globals, no-console */
/* eslint-disable no-restricted-globals */
/* globals clients */
self.addEventListener('push', event => {
let notification = event.data && event.data.json();
+142
View File
@@ -0,0 +1,142 @@
/* eslint-disable no-restricted-globals, no-use-before-define, no-undef */
// Build-time injected values (replaced by Vite define at build time)
const ASSET_ORIGIN = __ASSET_ORIGIN__;
const ASSET_PATH = __ASSET_PATH__;
const ASSET_MANIFEST = __ASSET_MANIFEST__;
const CACHE_NAME = 'chatwoot-assets-v1';
const EXCLUDED_PATHS = [
'/api/',
'/auth/',
'/rails/',
'/cable',
'/sidekiq',
'/super_admin',
'/swagger',
'/webhooks/',
'/widget',
'/survey/',
'/__vite',
'/sw.js',
];
const BATCH_SIZE = 5;
const buildAssetListChunks = (arr, size) => {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
};
const prefetchBatch = async (cache, baseUrl, assets) => {
const fetchAndCacheAsset = async asset => {
const url = `${baseUrl}${asset.url}`;
if (await cache.match(url)) {
return;
}
try {
const response = await fetch(url);
if (response.ok) {
cache.put(url, response);
}
} catch {
// Ignore prefetch failures
}
};
await Promise.all(assets.map(fetchAndCacheAsset));
};
const cacheFirst = async request => {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (err) {
const stale = await cache.match(request);
if (stale) return stale;
throw err;
}
};
const prefetchAssets = async () => {
if (!ASSET_MANIFEST?.length) return;
const cache = await caches.open(CACHE_NAME);
const baseUrl = ASSET_ORIGIN
? `${ASSET_ORIGIN}${ASSET_PATH}`
: `${self.location.origin}${ASSET_PATH}`;
const batches = buildAssetListChunks(ASSET_MANIFEST, BATCH_SIZE);
await batches.reduce(
(promise, batch) =>
promise.then(() => prefetchBatch(cache, baseUrl, batch)),
Promise.resolve()
);
};
const cleanupStaleAssets = async () => {
if (!ASSET_MANIFEST?.length) return;
const cache = await caches.open(CACHE_NAME);
const cachedRequests = await cache.keys();
const baseUrl = ASSET_ORIGIN
? `${ASSET_ORIGIN}${ASSET_PATH}`
: `${self.location.origin}${ASSET_PATH}`;
const validUrls = new Set(
ASSET_MANIFEST.map(asset => `${baseUrl}${asset.url}`)
);
const deletions = cachedRequests
.filter(request => {
const url = new URL(request.url);
const isAssetPath = url.pathname.startsWith('/vite/assets/');
return isAssetPath && !validUrls.has(request.url);
})
.map(request => cache.delete(request));
await Promise.all(deletions);
};
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', event => {
event.waitUntil(
Promise.all([cleanupStaleAssets(), prefetchAssets(), self.clients.claim()])
);
});
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
if (EXCLUDED_PATHS.some(path => url.pathname.startsWith(path))) return;
if (request.method !== 'GET') return;
const isAChatwootRequest =
url.origin === self.location.origin ||
(ASSET_ORIGIN && url.origin === ASSET_ORIGIN);
if (!isAChatwootRequest) return;
const isAnAsset =
url.pathname.startsWith('/vite/') ||
url.origin === ASSET_ORIGIN ||
/\.(js|css|woff2?|ttf|otf|eot|svg|png)$/i.test(url.pathname);
if (isAnAsset) {
event.respondWith(cacheFirst(request));
}
});
+1 -1
View File
@@ -143,7 +143,7 @@ class Conversation < ApplicationRecord
end
def last_incoming_message
messages&.incoming&.last
messages.where(account_id: account_id)&.incoming&.last
end
def toggle_status
+2 -2
View File
@@ -125,7 +125,7 @@ class Message < ApplicationRecord
belongs_to :account
belongs_to :inbox
belongs_to :conversation, touch: true
belongs_to :conversation
belongs_to :sender, polymorphic: true, optional: true
has_many :attachments, dependent: :destroy, autosave: true, before_add: :validate_attachments_limit
@@ -429,7 +429,7 @@ class Message < ApplicationRecord
def set_conversation_activity
# rubocop:disable Rails/SkipsModelValidations
conversation.update_columns(last_activity_at: created_at)
conversation.update_columns(last_activity_at: created_at, updated_at: Time.current)
# rubocop:enable Rails/SkipsModelValidations
end
+3 -1
View File
@@ -47,7 +47,9 @@ class ActionService
@agent = @account.users.find_by(id: agent_ids)
@conversation.update!(assignee_id: @agent.id) if @agent.present?
return unless @agent.present? && @agent.confirmed?
@conversation.update!(assignee_id: @agent.id)
end
def remove_label(labels)
-1
View File
@@ -191,7 +191,6 @@
- name: assignment_v2
display_name: Assignment V2
enabled: false
chatwoot_internal: true
- name: twilio_content_templates
display_name: Twilio Content Templates
enabled: false
@@ -24,10 +24,16 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
end
def playground
response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response(
additional_message: params[:message_content],
message_history: message_history
)
response = if captain_v2_enabled?
Captain::Assistant::AgentRunnerService.new(assistant: @assistant, source: 'playground').generate_response(
message_history: playground_message_history
)
else
Captain::Llm::AssistantChatService.new(assistant: @assistant, source: 'playground').generate_response(
additional_message: playground_params[:message_content],
message_history: message_history
)
end
render json: response
end
@@ -64,10 +70,31 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
end
def playground_params
params.require(:assistant).permit(:message_content, message_history: [:role, :content])
params.require(:assistant).permit(:message_content, message_history: [:role, :content, :agent_name])
end
def message_history
(playground_params[:message_history] || []).map { |message| { role: message[:role], content: message[:content] } }
(playground_params[:message_history] || []).map do |message|
{
role: message[:role],
content: message[:content],
agent_name: message[:agent_name]
}.compact
end
end
def playground_message_history
history = message_history
current_message = playground_params[:message_content]
return history if current_message.blank?
current_user_message = { role: 'user', content: current_message }
return history if history.last == current_user_message
history + [current_user_message]
end
def captain_v2_enabled?
@assistant.account.feature_enabled?('captain_integration_v2')
end
end
@@ -5,7 +5,6 @@ module Captain::ChatHelper
def request_chat_completion
log_chat_completion_request
chat = build_chat
add_messages_to_chat(chat)
@@ -86,7 +85,8 @@ module Captain::ChatHelper
temperature: temperature,
metadata: {
assistant_id: @assistant&.id,
channel_type: resolved_channel_type
channel_type: resolved_channel_type,
source: @source
}.compact
}
end
@@ -130,7 +130,6 @@ module Captain::ChatHelper
end
def log_chat_completion_request
Rails.logger.info("#{self.class.name} Assistant: #{@assistant.id}, Requesting chat completion " \
"for messages #{@messages} with #{@tools&.length || 0} tools")
Rails.logger.info("#{self.class.name} Assistant: #{@assistant.id}, requesting completion for #{@messages} with #{@tools&.length || 0} tools")
end
end
+1 -1
View File
@@ -106,7 +106,7 @@ class Captain::Assistant < ApplicationRecord
scenarios: scenarios.enabled.map do |scenario|
{
title: scenario.title,
key: scenario.title.parameterize.underscore,
key: scenario.handoff_key,
description: scenario.description
}
end,
+39 -1
View File
@@ -24,6 +24,19 @@ class Captain::Scenario < ApplicationRecord
include Concerns::CaptainToolsHelpers
include Concerns::Agentable
# OpenAI enforces a 64-char limit on function names. The ai-agents gem
# prepends "handoff_to_" (11 chars), so we keep a safety margin and cap
# the full tool name to MAX_HANDOFF_TOOL_NAME_LENGTH (60 chars).
# Format: "scenario_{id}_{slug}_agent" for persisted records (stable + readable),
# and "scenario_draft_{slug}_agent" for unsaved records, with slug truncated
# based on the available length budget.
HANDOFF_TOOL_PREFIX = 'handoff_to_'.freeze
HANDOFF_KEY_PREFIX = 'scenario'.freeze
HANDOFF_KEY_SUFFIX = 'agent'.freeze
MAX_HANDOFF_TOOL_NAME_LENGTH = 60
MAX_AGENT_NAME_LENGTH = MAX_HANDOFF_TOOL_NAME_LENGTH - HANDOFF_TOOL_PREFIX.length
MAX_HANDOFF_SLUG_LENGTH = 24
self.table_name = 'captain_scenarios'
belongs_to :assistant, class_name: 'Captain::Assistant'
@@ -42,6 +55,10 @@ class Captain::Scenario < ApplicationRecord
before_save :resolve_tool_references
def handoff_key
[handoff_id_key, compact_handoff_slug, HANDOFF_KEY_SUFFIX].compact.join('_')
end
def prompt_context
{
title: title,
@@ -56,7 +73,28 @@ class Captain::Scenario < ApplicationRecord
private
def agent_name
"#{title} Agent".parameterize(separator: '_')
handoff_key
end
def handoff_id_key
return "#{HANDOFF_KEY_PREFIX}_#{id}" if id.present?
"#{HANDOFF_KEY_PREFIX}_draft"
end
def compact_handoff_slug
slug = title.to_s.parameterize(separator: '_').presence
return nil if slug.blank?
max_slug_length = [MAX_HANDOFF_SLUG_LENGTH, dynamic_slug_max_length].min
return nil if max_slug_length <= 0
slug.first(max_slug_length).sub(/_+\z/, '').presence
end
def dynamic_slug_max_length
# handoff_to_#{scenario_<id>_<slug>_agent}
MAX_AGENT_NAME_LENGTH - handoff_id_key.length - HANDOFF_KEY_SUFFIX.length - 2
end
def agent_tools
@@ -19,11 +19,11 @@ class Captain::Assistant::AgentRunnerService
CONTACT_INBOX_STATE_ATTRIBUTES = %i[id hmac_verified].freeze
CAMPAIGN_STATE_ATTRIBUTES = %i[id title message campaign_type description].freeze
def initialize(assistant:, conversation: nil, callbacks: {})
def initialize(assistant:, conversation: nil, callbacks: {}, source: nil)
@assistant = assistant
@conversation = conversation
@callbacks = callbacks
@source = source
end
def generate_response(message_history: [])
@@ -32,8 +32,7 @@ class Captain::Assistant::AgentRunnerService
process_agent_result(result)
rescue StandardError => e
# when running the agent runner service in a rake task, the conversation might not have an account associated
# for regular production usage, it will run just fine
# In rake/local runs, conversation may not be present, so account is optional here.
ChatwootExceptionTracker.new(e, account: @conversation&.account).capture_exception
Rails.logger.error "[Captain V2] AgentRunnerService error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
@@ -128,6 +127,7 @@ class Captain::Assistant::AgentRunnerService
assistant_id: @assistant.id,
assistant_config: @assistant.config
}
state[:source] = @source if @source.present?
build_conversation_state(state) if @conversation
state
@@ -140,8 +140,7 @@ class Captain::Assistant::AgentRunnerService
state[:campaign] = @conversation.campaign.attributes.symbolize_keys.slice(*CAMPAIGN_STATE_ATTRIBUTES) if @conversation.campaign
return unless @conversation.contact_inbox
state[:contact_inbox] =
@conversation.contact_inbox.attributes.symbolize_keys.slice(*CONTACT_INBOX_STATE_ATTRIBUTES)
state[:contact_inbox] = @conversation.contact_inbox.attributes.symbolize_keys.slice(*CONTACT_INBOX_STATE_ATTRIBUTES)
end
def build_and_wire_agents
@@ -180,6 +179,7 @@ class Captain::Assistant::AgentRunnerService
format(ATTR_LANGFUSE_METADATA, 'conversation_id') => conversation[:id],
format(ATTR_LANGFUSE_METADATA, 'conversation_display_id') => conversation[:display_id],
format(ATTR_LANGFUSE_METADATA, 'channel_type') => state[:channel_type],
format(ATTR_LANGFUSE_METADATA, 'source') => state[:source],
ATTR_LANGFUSE_TRACE_INPUT => trace_input,
ATTR_LANGFUSE_OBSERVATION_INPUT => trace_input
}.compact.transform_values(&:to_s)
@@ -1,11 +1,12 @@
class Captain::Llm::AssistantChatService < Llm::BaseAiService
include Captain::ChatHelper
def initialize(assistant: nil, conversation_id: nil)
def initialize(assistant: nil, conversation_id: nil, source: nil)
super()
@assistant = assistant
@conversation_id = conversation_id
@source = source
@messages = [system_message]
@response = ''
+9 -3
View File
@@ -8,6 +8,12 @@ task before_assets_precompile: :environment do
system('echo "-------------- Bulding App for Production --------------"')
end
# every time you execute 'rake assets:precompile'
# run 'before_assets_precompile' first
Rake::Task['assets:precompile'].enhance %w[before_assets_precompile]
task after_assets_precompile: :environment do
# Build service worker after Vite has generated the asset manifest
system('echo "-------------- Building Service Worker --------------"')
system('NODE_ENV=production pnpm run build:sw')
end
Rake::Task['assets:precompile'].enhance %w[before_assets_precompile] do
Rake::Task['after_assets_precompile'].invoke
end
+12 -9
View File
@@ -13,6 +13,7 @@
"dev": "overmind start -f ./Procfile.dev",
"ruby:prettier": "bundle exec rubocop -a",
"build:sdk": "BUILD_MODE=library vite build",
"build:sw": "node app/javascript/service-worker/build.js",
"prepare": "husky install",
"size": "size-limit",
"story:dev": "histoire dev",
@@ -36,8 +37,8 @@
"@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.3.7",
"@chatwoot/utils": "^0.0.52",
"@formkit/core": "^1.6.7",
"@formkit/vue": "^1.6.7",
"@formkit/core": "^1.7.2",
"@formkit/vue": "^1.7.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@highlightjs/vue-plugin": "^2.1.0",
"@iconify-json/fluent": "^1.2.32",
@@ -49,7 +50,7 @@
"@scmmishra/pico-search": "0.6.0",
"@sentry/vue": "^8.55.0",
"@sindresorhus/slugify": "2.2.1",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/vue-table": "^8.20.5",
"@twilio/voice-sdk": "^2.12.4",
"@vitejs/plugin-vue": "^5.1.4",
@@ -59,7 +60,7 @@
"@vueuse/components": "^12.0.0",
"@vueuse/core": "^12.0.0",
"activestorage": "^5.2.6",
"axios": "^1.13.2",
"axios": "^1.13.6",
"camelcase-keys": "^9.1.3",
"chart.js": "~4.4.4",
"color2k": "^2.0.2",
@@ -68,7 +69,7 @@
"countries-and-timezones": "^3.6.0",
"date-fns": "2.21.1",
"date-fns-tz": "^1.3.3",
"dompurify": "3.2.4",
"dompurify": "3.3.2",
"flag-icons": "^7.2.3",
"floating-vue": "^5.2.2",
"highlight.js": "^11.10.0",
@@ -99,7 +100,7 @@
"vue": "^3.5.12",
"vue-chartjs": "5.3.1",
"vue-datepicker-next": "^1.0.3",
"vue-dompurify-html": "^5.1.0",
"vue-dompurify-html": "^5.3.0",
"vue-i18n": "9.14.5",
"vue-letter": "^0.2.1",
"vue-router": "~4.4.5",
@@ -111,7 +112,7 @@
"wavesurfer.js": "7.8.6"
},
"devDependencies": {
"@egoist/tailwindcss-icons": "^1.9.0",
"@egoist/tailwindcss-icons": "^1.9.2",
"@histoire/plugin-vue": "0.17.15",
"@iconify-json/logos": "^1.2.10",
"@iconify-json/lucide": "^1.2.82",
@@ -142,7 +143,7 @@
"prettier": "^3.3.3",
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.13",
"tailwindcss": "^3.4.19",
"vite": "^5.4.21",
"vite-plugin-ruby": "^5.0.0",
"vitest": "3.0.5"
@@ -160,7 +161,9 @@
"overrides": {
"vite-node": "2.0.1",
"vite": "5.4.21",
"vitest": "3.0.5"
"vitest": "3.0.5",
"minimatch@<4": "3.1.5",
"minimatch@>=9.0.0 <9.0.7": "9.0.9"
}
},
"lint-staged": {
+152 -240
View File
@@ -8,6 +8,8 @@ overrides:
vite-node: 2.0.1
vite: 5.4.21
vitest: 3.0.5
minimatch@<4: 3.1.5
minimatch@>=9.0.0 <9.0.7: 9.0.9
importers:
@@ -29,11 +31,11 @@ importers:
specifier: ^0.0.52
version: 0.0.52
'@formkit/core':
specifier: ^1.6.7
version: 1.6.7
specifier: ^1.7.2
version: 1.7.2
'@formkit/vue':
specifier: ^1.6.7
version: 1.6.7(tailwindcss@3.4.13)(vue@3.5.12(typescript@5.6.2))
specifier: ^1.7.2
version: 1.7.2(vue@3.5.12(typescript@5.6.2))
'@hcaptcha/vue3-hcaptcha':
specifier: ^1.3.0
version: 1.3.0(vue@3.5.12(typescript@5.6.2))
@@ -68,8 +70,8 @@ importers:
specifier: 2.2.1
version: 2.2.1
'@tailwindcss/typography':
specifier: ^0.5.15
version: 0.5.15(tailwindcss@3.4.13)
specifier: ^0.5.19
version: 0.5.19(tailwindcss@3.4.19)
'@tanstack/vue-table':
specifier: ^8.20.5
version: 8.20.5(vue@3.5.12(typescript@5.6.2))
@@ -98,8 +100,8 @@ importers:
specifier: ^5.2.6
version: 5.2.8
axios:
specifier: ^1.13.2
version: 1.13.2
specifier: ^1.13.6
version: 1.13.6
camelcase-keys:
specifier: ^9.1.3
version: 9.1.3
@@ -125,8 +127,8 @@ importers:
specifier: ^1.3.3
version: 1.3.8(date-fns@2.21.1)
dompurify:
specifier: 3.2.4
version: 3.2.4
specifier: 3.3.2
version: 3.3.2
flag-icons:
specifier: ^7.2.3
version: 7.2.3
@@ -218,8 +220,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3(vue@3.5.12(typescript@5.6.2))
vue-dompurify-html:
specifier: ^5.1.0
version: 5.1.0(vue@3.5.12(typescript@5.6.2))
specifier: ^5.3.0
version: 5.3.0(vue@3.5.12(typescript@5.6.2))
vue-i18n:
specifier: 9.14.5
version: 9.14.5(vue@3.5.12(typescript@5.6.2))
@@ -249,8 +251,8 @@ importers:
version: 7.8.6
devDependencies:
'@egoist/tailwindcss-icons':
specifier: ^1.9.0
version: 1.9.0(tailwindcss@3.4.13)
specifier: ^1.9.2
version: 1.9.2(tailwindcss@3.4.19)
'@histoire/plugin-vue':
specifier: 0.17.15
version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
@@ -342,8 +344,8 @@ importers:
specifier: ^8.2.4
version: 8.2.6
tailwindcss:
specifier: ^3.4.13
version: 3.4.13
specifier: ^3.4.19
version: 3.4.19
vite:
specifier: 5.4.21
version: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
@@ -402,9 +404,6 @@ packages:
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
'@asamuzakjp/css-color@4.1.0':
resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==}
@@ -700,8 +699,8 @@ packages:
peerDependencies:
postcss-selector-parser: ^6.0.10
'@egoist/tailwindcss-icons@1.9.0':
resolution: {integrity: sha512-xWA9cUy6hzlK7Y6TaoRIcwmilSXiTJ8rbXcEdf9uht7yzDgw/yIgF4rThIQMrpD2Y2v4od51+r2y6Z7GStanDQ==}
'@egoist/tailwindcss-icons@1.9.2':
resolution: {integrity: sha512-I6XsSykmhu2cASg5Hp/ICLsJ/K/1aXPaSKjgbWaNp2xYnb4We/arWMmkhhV+9CglOFCUbqx0A3mM2kWV32ZIhw==}
peerDependencies:
tailwindcss: '*'
@@ -874,46 +873,35 @@ packages:
'@floating-ui/utils@0.2.7':
resolution: {integrity: sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==}
'@formkit/core@1.6.7':
resolution: {integrity: sha512-wEoWK7crcCPRV5KJfEGLjjIS+qwbuD8I5Ur0zTtKRQrdO4oRL6kVoubxQOpgnq1l8sWfcRY8Wpf22Wna2LD20Q==}
'@formkit/core@1.7.2':
resolution: {integrity: sha512-XDRVqkDtOziU3z44hdvgWEqGpiv6nTNeCmP8cnESKkr24a4keQuEwmfDVvibYV0ywyTMg6ylp2lyklCYeRHykQ==}
'@formkit/dev@1.6.7':
resolution: {integrity: sha512-mMtkfvfkl1P1v0haizUE4DadalbG9/3m0ZymmMKKb0F3pojQJFtdfohy67ZQKtmzy4bamowHyEUr+XzLbKY2EA==}
'@formkit/dev@1.7.2':
resolution: {integrity: sha512-W38xbbFS4h4LTV22kUC6ZbPHBmlM2lVbAz5PSYU3SPaNc4FeXp4GTe4GzLEmcS82B+5L1zbSOgGB43kWZts7wA==}
'@formkit/i18n@1.6.7':
resolution: {integrity: sha512-i9Mnc2XHCm2c10fppEIxdGv+jqOaixO22iFXX3xF+AkJnxtOzV5hP4f3/TeG+BNahGUq8vj+e1y+VMnjS6duxA==}
'@formkit/i18n@1.7.2':
resolution: {integrity: sha512-Zs6f+rtP2j2Nnt1HkNrL85WU6rtS1l1Q0BAQjCMGQPsbrppiL7/TZ5bNIrVm0DTntLgG3firDS7bNWAxPSwerQ==}
'@formkit/inputs@1.6.7':
resolution: {integrity: sha512-VLxoAJn5VGOEXkGI499lju5Irnu12cu+spI1HL//46nlVkqnb1XcV0k0MK9K+hogz4fGmyZUvHf0lxU6dh7ZZQ==}
'@formkit/inputs@1.7.2':
resolution: {integrity: sha512-4xs7RJN8EFGctTCNRXBTvor2O8RvuEEK3q2Hl/RMU5lhhn5tmck90fkkeAr9o+rWRoSNiV8XgbLQArQ/B1IYPw==}
'@formkit/observer@1.6.7':
resolution: {integrity: sha512-ei5z5ernNMKKiBuoRcFgEthhP1i+KKb02hsPsikLA3XehuoJdWIejn9AAq6jOEGbUMZ5XAAgFJcxzO5tnKuPnw==}
'@formkit/observer@1.7.2':
resolution: {integrity: sha512-KUr5mcu2SAeHOOK1FaMyFigtA8hkHLsUkaPFWpTC81j79o1AUjJppmfPaX6PbtlHLeMs2Zq8Qhp6VAhi7D2WJg==}
'@formkit/rules@1.6.7':
resolution: {integrity: sha512-adzOuTvf6ghZbV0g0ZH9+MU9jfoF4DojBztAbqzFP/fT4d+WxhSHHlkWq6PU66fHPy3OH4DkWdx9trL1wGHuzQ==}
'@formkit/rules@1.7.2':
resolution: {integrity: sha512-2e5qKlXzmL9LAetncFp7xkHEX/UAJsU3bo1iL3idloH3HALf5fIdg3lgOKwCNDMdLjbfiKJ6lVwFaeFfFpcDKw==}
'@formkit/themes@1.6.7':
resolution: {integrity: sha512-TIiWr4TMAFUg1pQz2E4GErfAhBv2Q2VbWlk6pqXPWI8UyPTjmcinEnCSIWDCX6FPPqiYShBnh8123nTO7pyvjA==}
peerDependencies:
tailwindcss: ^3.2.0
unocss: 0.x.x
windicss: ^3.0.0
peerDependenciesMeta:
tailwindcss:
optional: true
unocss:
optional: true
windicss:
optional: true
'@formkit/themes@1.7.2':
resolution: {integrity: sha512-AaZHy7l9D44Ya3cQRqZ8RIpaTTsCjTB1gX13NlpqYSs6yG0yBTmJ7L7kW60bOGdhs/vmIwSel578EBwz98nybQ==}
'@formkit/utils@1.6.7':
resolution: {integrity: sha512-aU3CDLzCkC5Dnx6iS3swbsIbys7E+2VOaLWFRnS7wk7kFa8EnENi67qc2E2KFE05RT4UCEAIYMAQY6wvek29gA==}
'@formkit/utils@1.7.2':
resolution: {integrity: sha512-bBF6alUBOqFfJHjVB95Vck0hp36vlw4QfFJxGfTO6BX68AEaFzzzabtpwfy0DbcHtwHh4Yn7l/rOWGxXEve+QQ==}
'@formkit/validation@1.6.7':
resolution: {integrity: sha512-4wUUG+Pz3hPeiLccYiXAzsrF7SXk28PYAeHJeBngIv9K82ieljBJpvvuCJDyA6SeSMOvmbI92TG4wx4u5cDLOw==}
'@formkit/validation@1.7.2':
resolution: {integrity: sha512-JJsLP7AI/++VujdwtBeUcPhCqY3FXSKZ7hajT7Ow5Feab7JLqVPjxVMFbXDgEOLjq0ex1NX/I+EQHfm5sqMRsA==}
'@formkit/vue@1.6.7':
resolution: {integrity: sha512-w3kjQD0lvtImyyTiy+fLaIZM/4r6sLDbpBIwke8ZA/d5orzNE96JPaloVKbH+HwCo6+z/1mclh1pW1S+7RgMcw==}
'@formkit/vue@1.7.2':
resolution: {integrity: sha512-jrvXl2ZhS6X5klTbP4N5zc0Dg37AjbAf8SCVoz7mfligVkmwrrF4SN3waMBcH+n9gqT+vrkOYb6bQXdvjUdnCQ==}
peerDependencies:
vue: ^3.4.0
@@ -985,8 +973,8 @@ packages:
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
'@iconify/utils@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
'@iconify/utils@3.1.0':
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
'@intlify/core-base@9.14.2':
resolution: {integrity: sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==}
@@ -1291,10 +1279,10 @@ packages:
peerDependencies:
size-limit: 8.2.6
'@tailwindcss/typography@0.5.15':
resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==}
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20'
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tanstack/table-core@8.20.5':
resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==}
@@ -1579,8 +1567,8 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -1712,8 +1700,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
axios@1.13.6:
resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1745,8 +1733,8 @@ packages:
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -1922,9 +1910,6 @@ packages:
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
@@ -2166,8 +2151,9 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.2.4:
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
dompurify@3.3.2:
resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==}
engines: {node: '>=20'}
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
@@ -2450,9 +2436,6 @@ packages:
resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==}
engines: {node: '>=12.0.0'}
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
@@ -2645,10 +2628,6 @@ packages:
resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==}
engines: {node: '>=18'}
globals@15.15.0:
resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
engines: {node: '>=18'}
globalthis@1.0.3:
resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
engines: {node: '>= 0.4'}
@@ -2796,8 +2775,8 @@ packages:
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
engines: {node: '>= 4'}
immutable@4.3.7:
resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
immutable@4.3.8:
resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==}
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
@@ -3001,6 +2980,10 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
js-beautify@1.15.1:
resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==}
engines: {node: '>=14'}
@@ -3077,9 +3060,6 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
launch-editor@2.9.1:
resolution: {integrity: sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==}
@@ -3097,8 +3077,8 @@ packages:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
@@ -3131,10 +3111,6 @@ packages:
lit@2.2.6:
resolution: {integrity: sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==}
local-pkg@1.1.2:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -3147,12 +3123,6 @@ packages:
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3287,15 +3257,11 @@ packages:
min-document@2.19.0:
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minimatch@9.0.1:
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
@@ -3308,8 +3274,8 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
mlly@1.8.1:
resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==}
mpd-parser@0.21.0:
resolution: {integrity: sha512-NbpMJ57qQzFmfCiP1pbL7cGMbVTD0X1hqNgL0VYP1wLlZXLf/HtmvQpNkOA1AHkPVeGQng+7/jEtSvNUzV7Gdg==}
@@ -3606,9 +3572,6 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
@@ -3915,9 +3878,6 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
@@ -4269,8 +4229,8 @@ packages:
resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
engines: {node: '>=10.0.0'}
tailwindcss@3.4.13:
resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==}
tailwindcss@3.4.19:
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
engines: {node: '>=14.0.0'}
hasBin: true
@@ -4432,8 +4392,8 @@ packages:
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@@ -4622,10 +4582,10 @@ packages:
'@vue/composition-api':
optional: true
vue-dompurify-html@5.1.0:
resolution: {integrity: sha512-616o2/PBdOLM2bwlRWLdzeEC9NerLkwiudqNgaIJ5vBQWXec+u7Kuzh+45DtQQrids67s4pHnTnJZLVfyPMxbA==}
vue-dompurify-html@5.3.0:
resolution: {integrity: sha512-HJQGBHbfSPcb6Mu97McdKbX7TqRHZa6Ji8OCpCNyuHca5QvQZ8IiuwghFPSO8OkSQfqXPNPKFMZdCOrnGGmOSQ==}
peerDependencies:
vue: ^3.0.0
vue: ^3.4.36
vue-eslint-parser@9.4.3:
resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
@@ -4944,8 +4904,6 @@ snapshots:
package-manager-detector: 1.6.0
tinyexec: 1.0.2
'@antfu/utils@8.1.1': {}
'@asamuzakjp/css-color@4.1.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@@ -5262,12 +5220,10 @@ snapshots:
dependencies:
postcss-selector-parser: 6.1.1
'@egoist/tailwindcss-icons@1.9.0(tailwindcss@3.4.13)':
'@egoist/tailwindcss-icons@1.9.2(tailwindcss@3.4.19)':
dependencies:
'@iconify/utils': 2.3.0
tailwindcss: 3.4.13
transitivePeerDependencies:
- supports-color
'@iconify/utils': 3.1.0
tailwindcss: 3.4.19
'@esbuild/aix-ppc64@0.21.5':
optional: true
@@ -5354,7 +5310,7 @@ snapshots:
ignore: 5.2.4
import-fresh: 3.3.0
js-yaml: 4.1.0
minimatch: 3.1.2
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
@@ -5368,7 +5324,7 @@ snapshots:
ignore: 5.2.4
import-fresh: 3.3.0
js-yaml: 4.1.0
minimatch: 3.1.2
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
@@ -5385,67 +5341,61 @@ snapshots:
'@floating-ui/utils@0.2.7': {}
'@formkit/core@1.6.7':
'@formkit/core@1.7.2':
dependencies:
'@formkit/utils': 1.6.7
'@formkit/utils': 1.7.2
'@formkit/dev@1.6.7':
'@formkit/dev@1.7.2':
dependencies:
'@formkit/core': 1.6.7
'@formkit/utils': 1.6.7
'@formkit/core': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/i18n@1.6.7':
'@formkit/i18n@1.7.2':
dependencies:
'@formkit/core': 1.6.7
'@formkit/utils': 1.6.7
'@formkit/validation': 1.6.7
'@formkit/core': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/validation': 1.7.2
'@formkit/inputs@1.6.7':
'@formkit/inputs@1.7.2':
dependencies:
'@formkit/core': 1.6.7
'@formkit/utils': 1.6.7
'@formkit/core': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/observer@1.6.7':
'@formkit/observer@1.7.2':
dependencies:
'@formkit/core': 1.6.7
'@formkit/utils': 1.6.7
'@formkit/core': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/rules@1.6.7':
'@formkit/rules@1.7.2':
dependencies:
'@formkit/core': 1.6.7
'@formkit/utils': 1.6.7
'@formkit/validation': 1.6.7
'@formkit/core': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/validation': 1.7.2
'@formkit/themes@1.6.7(tailwindcss@3.4.13)':
'@formkit/themes@1.7.2':
dependencies:
'@formkit/core': 1.6.7
optionalDependencies:
tailwindcss: 3.4.13
'@formkit/core': 1.7.2
'@formkit/utils@1.6.7': {}
'@formkit/utils@1.7.2': {}
'@formkit/validation@1.6.7':
'@formkit/validation@1.7.2':
dependencies:
'@formkit/core': 1.6.7
'@formkit/observer': 1.6.7
'@formkit/utils': 1.6.7
'@formkit/core': 1.7.2
'@formkit/observer': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/vue@1.6.7(tailwindcss@3.4.13)(vue@3.5.12(typescript@5.6.2))':
'@formkit/vue@1.7.2(vue@3.5.12(typescript@5.6.2))':
dependencies:
'@formkit/core': 1.6.7
'@formkit/dev': 1.6.7
'@formkit/i18n': 1.6.7
'@formkit/inputs': 1.6.7
'@formkit/observer': 1.6.7
'@formkit/rules': 1.6.7
'@formkit/themes': 1.6.7(tailwindcss@3.4.13)
'@formkit/utils': 1.6.7
'@formkit/validation': 1.6.7
'@formkit/core': 1.7.2
'@formkit/dev': 1.7.2
'@formkit/i18n': 1.7.2
'@formkit/inputs': 1.7.2
'@formkit/observer': 1.7.2
'@formkit/rules': 1.7.2
'@formkit/themes': 1.7.2
'@formkit/utils': 1.7.2
'@formkit/validation': 1.7.2
vue: 3.5.12(typescript@5.6.2)
transitivePeerDependencies:
- tailwindcss
- unocss
- windicss
'@hcaptcha/vue3-hcaptcha@1.3.0(vue@3.5.12(typescript@5.6.2))':
dependencies:
@@ -5511,7 +5461,7 @@ snapshots:
dependencies:
'@humanwhocodes/object-schema': 2.0.3
debug: 4.3.5
minimatch: 3.1.2
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@@ -5549,18 +5499,11 @@ snapshots:
'@iconify/types@2.0.0': {}
'@iconify/utils@2.3.0':
'@iconify/utils@3.1.0':
dependencies:
'@antfu/install-pkg': 1.1.0
'@antfu/utils': 8.1.1
'@iconify/types': 2.0.0
debug: 4.4.3
globals: 15.15.0
kolorist: 1.8.0
local-pkg: 1.1.2
mlly: 1.8.0
transitivePeerDependencies:
- supports-color
mlly: 1.8.1
'@intlify/core-base@9.14.2':
dependencies:
@@ -5847,13 +5790,10 @@ snapshots:
semver: 7.5.3
size-limit: 8.2.6
'@tailwindcss/typography@0.5.15(tailwindcss@3.4.13)':
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19)':
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.13
tailwindcss: 3.4.19
'@tanstack/table-core@8.20.5': {}
@@ -6230,7 +6170,7 @@ snapshots:
acorn@8.14.0: {}
acorn@8.15.0: {}
acorn@8.16.0: {}
activestorage@5.2.8:
dependencies:
@@ -6391,7 +6331,7 @@ snapshots:
dependencies:
possible-typed-array-names: 1.0.0
axios@1.13.2:
axios@1.13.6:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
@@ -6431,7 +6371,7 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.0.1:
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
@@ -6633,8 +6573,6 @@ snapshots:
confbox@0.1.8: {}
confbox@0.2.2: {}
config-chain@1.1.13:
dependencies:
ini: 1.3.8
@@ -6850,7 +6788,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.4:
dompurify@3.3.2:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -6877,7 +6815,7 @@ snapshots:
dependencies:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
minimatch: 9.0.9
semver: 7.6.3
ee-first@1.1.1: {}
@@ -7144,7 +7082,7 @@ snapshots:
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
minimatch: 3.1.2
minimatch: 3.1.5
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.0
@@ -7224,7 +7162,7 @@ snapshots:
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
minimatch: 3.1.2
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.3
strip-ansi: 6.0.1
@@ -7270,8 +7208,6 @@ snapshots:
expect-type@1.1.0: {}
exsolve@1.0.8: {}
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
@@ -7456,7 +7392,7 @@ snapshots:
dependencies:
foreground-child: 3.3.0
jackspeak: 3.4.3
minimatch: 9.0.5
minimatch: 9.0.9
minipass: 7.1.2
package-json-from-dist: 1.0.0
path-scurry: 1.11.1
@@ -7466,7 +7402,7 @@ snapshots:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
minimatch: 3.1.5
once: 1.4.0
path-is-absolute: 1.0.1
@@ -7487,8 +7423,6 @@ snapshots:
globals@15.14.0: {}
globals@15.15.0: {}
globalthis@1.0.3:
dependencies:
define-properties: 1.2.0
@@ -7684,7 +7618,7 @@ snapshots:
ignore@6.0.2: {}
immutable@4.3.7:
immutable@4.3.8:
optional: true
import-fresh@3.3.0:
@@ -7874,6 +7808,8 @@ snapshots:
jiti@1.21.6: {}
jiti@1.21.7: {}
js-beautify@1.15.1:
dependencies:
config-chain: 1.1.13
@@ -7990,8 +7926,6 @@ snapshots:
kind-of@6.0.3: {}
kolorist@1.8.0: {}
launch-editor@2.9.1:
dependencies:
picocolors: 1.1.0
@@ -8008,7 +7942,7 @@ snapshots:
lilconfig@2.1.0: {}
lilconfig@3.1.2: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@@ -8059,12 +7993,6 @@ snapshots:
lit-element: 3.3.3
lit-html: 2.8.0
local-pkg@1.1.2:
dependencies:
mlly: 1.8.0
pkg-types: 2.3.0
quansync: 0.2.11
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -8077,10 +8005,6 @@ snapshots:
dependencies:
p-locate: 6.0.0
lodash.castarray@4.4.0: {}
lodash.isplainobject@4.0.6: {}
lodash.merge@4.6.2: {}
lodash.truncate@4.4.2: {}
@@ -8217,17 +8141,13 @@ snapshots:
dependencies:
dom-walk: 0.1.2
minimatch@3.1.2:
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.11
minimatch@9.0.1:
minimatch@9.0.9:
dependencies:
brace-expansion: 2.0.1
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
brace-expansion: 2.0.2
minimist@1.2.8: {}
@@ -8235,12 +8155,12 @@ snapshots:
mitt@3.0.1: {}
mlly@1.8.0:
mlly@1.8.1:
dependencies:
acorn: 8.15.0
acorn: 8.16.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.1
ufo: 1.6.3
mpd-parser@0.21.0:
dependencies:
@@ -8511,13 +8431,7 @@ snapshots:
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.8.0
pathe: 2.0.3
pkg-types@2.3.0:
dependencies:
confbox: 0.2.2
exsolve: 1.0.8
mlly: 1.8.1
pathe: 2.0.3
pngjs@5.0.0: {}
@@ -8634,7 +8548,7 @@ snapshots:
postcss-load-config@4.0.2(postcss@8.4.47):
dependencies:
lilconfig: 3.1.2
lilconfig: 3.1.3
yaml: 2.5.1
optionalDependencies:
postcss: 8.4.47
@@ -8889,8 +8803,6 @@ snapshots:
pngjs: 5.0.0
yargs: 15.4.1
quansync@0.2.11: {}
querystringify@2.2.0: {}
queue-microtask@1.2.3: {}
@@ -9035,7 +8947,7 @@ snapshots:
sass@1.79.3:
dependencies:
chokidar: 4.0.3
immutable: 4.3.7
immutable: 4.3.8
source-map-js: 1.2.1
optional: true
@@ -9289,7 +9201,7 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
tailwindcss@3.4.13:
tailwindcss@3.4.19:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -9299,12 +9211,12 @@ snapshots:
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 2.1.0
jiti: 1.21.7
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.0
picocolors: 1.1.1
postcss: 8.4.47
postcss-import: 15.1.0(postcss@8.4.47)
postcss-js: 4.0.1(postcss@8.4.47)
@@ -9324,7 +9236,7 @@ snapshots:
terser@5.33.0:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.15.0
acorn: 8.16.0
commander: 2.20.3
source-map-support: 0.5.21
optional: true
@@ -9333,7 +9245,7 @@ snapshots:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 10.4.5
minimatch: 9.0.5
minimatch: 9.0.9
text-segmentation@1.0.3:
dependencies:
@@ -9487,7 +9399,7 @@ snapshots:
uc.micro@2.1.0: {}
ufo@1.6.1: {}
ufo@1.6.3: {}
unbox-primitive@1.0.2:
dependencies:
@@ -9672,9 +9584,9 @@ snapshots:
dependencies:
vue: 3.5.12(typescript@5.6.2)
vue-dompurify-html@5.1.0(vue@3.5.12(typescript@5.6.2)):
vue-dompurify-html@5.3.0(vue@3.5.12(typescript@5.6.2)):
dependencies:
dompurify: 3.2.4
dompurify: 3.3.2
vue: 3.5.12(typescript@5.6.2)
vue-eslint-parser@9.4.3(eslint@8.57.0):
@@ -145,5 +145,24 @@ describe ContactIdentifyAction do
expect(contact.phone_number).to be_nil
end
end
context 'when params have not changed' do
it 'skips save and does not issue an UPDATE query' do
contact.update!(name: 'test', identifier: 'test_id', custom_attributes: { test: 'test', test1: 'test1' })
params = { name: 'test', identifier: 'test_id', custom_attributes: { test: 'test', test1: 'test1' } }
# any_instance is needed because merge lookup can reassign @contact to a different Ruby object
expect_any_instance_of(Contact).not_to receive(:save!) # rubocop:disable RSpec/AnyInstance
described_class.new(contact: contact, params: params).perform
end
it 'still enqueues avatar job even when attributes have not changed' do
contact.update!(name: 'test')
params = { name: 'test', avatar_url: 'https://chatwoot-assets.local/sample.png' }
expect(Avatar::AvatarFromUrlJob).to receive(:perform_later).with(contact, params[:avatar_url]).once
described_class.new(contact: contact, params: params).perform
end
end
end
end
@@ -25,6 +25,7 @@ RSpec.describe 'Agents API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.parsed_body.size).to eq(account.users.count)
end
@@ -122,6 +123,7 @@ RSpec.describe 'Agents API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(other_agent.reload.name).to eq(params[:name])
end
@@ -171,6 +173,7 @@ RSpec.describe 'Agents API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.parsed_body['email']).to eq(params[:email])
expect(account.users.last.name).to eq('NewUser')
end
@@ -45,6 +45,7 @@ RSpec.describe 'Contacts API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
response_body = response.parsed_body
contact_emails = response_body['payload'].pluck('email')
contact_inboxes_source_ids = response_body['payload'].flat_map { |c| c['contact_inboxes'].pluck('source_id') }
@@ -331,6 +332,7 @@ RSpec.describe 'Contacts API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.body).to include(contact2.email)
expect(response.body).not_to include(contact1.email)
end
@@ -443,6 +445,7 @@ RSpec.describe 'Contacts API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.body).to include(contact2.email)
expect(response.body).to include(contact1.email)
end
@@ -497,6 +500,7 @@ RSpec.describe 'Contacts API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.body).to include(contact.name)
end
end
@@ -620,6 +624,7 @@ RSpec.describe 'Contacts API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(contact.reload.name).to eq('Test Blub')
# custom attributes are merged properly without overwriting existing ones
expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' })
@@ -31,6 +31,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.first.content).to eq(params[:content])
end
@@ -182,6 +183,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:contact][:id]).to eq(conversation.contact_id)
end
end
@@ -27,6 +27,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
body = JSON.parse(response.body, symbolize_names: true)
expect(body[:data][:meta][:all_count]).to eq(1)
expect(body[:data][:meta].keys).to include(:all_count, :mine_count, :assigned_count, :unassigned_count)
@@ -165,6 +166,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data.count).to eq(2)
end
@@ -234,6 +236,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(conversation.display_id)
end
@@ -282,6 +285,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(JSON.parse(response.body, symbolize_names: true)[:priority]).to eq('high')
end
@@ -342,6 +346,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:additional_attributes]).to eq(additional_attributes)
end
@@ -449,9 +454,11 @@ RSpec.describe 'Conversations API', type: :request do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
params: { status: 'open' },
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(conversation.reload.status).to eq('open')
end
@@ -32,6 +32,7 @@ RSpec.describe 'Inboxes API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(JSON.parse(response.body, symbolize_names: true)[:payload].size).to eq(2)
end
@@ -95,6 +96,7 @@ RSpec.describe 'Inboxes API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(inbox.id)
end
@@ -383,6 +385,7 @@ RSpec.describe 'Inboxes API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.body).to include('test.com')
end
@@ -478,6 +481,7 @@ RSpec.describe 'Inboxes API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(inbox.reload.enable_auto_assignment).to be_falsey
expect(inbox.reload.portal_id).to eq(portal.id)
expect(response.parsed_body['name']).to eq 'new test inbox'
@@ -22,6 +22,7 @@ RSpec.describe 'Teams API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.parsed_body.first['id']).to eq(account.teams.first.id)
end
end
@@ -45,6 +46,7 @@ RSpec.describe 'Teams API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.parsed_body['id']).to eq(team.id)
end
end
@@ -83,6 +85,7 @@ RSpec.describe 'Teams API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(Team.count).to eq(2)
end
end
@@ -121,6 +124,7 @@ RSpec.describe 'Teams API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(team.reload.name).to eq('new-team')
end
end
@@ -149,6 +149,7 @@ RSpec.describe 'Accounts API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.body).to include(account.name)
expect(response.body).to include(account.locale)
expect(response.body).to include(account.domain)
@@ -184,22 +185,22 @@ RSpec.describe 'Accounts API', type: :request do
end
end
describe 'PUT /api/v1/accounts/{account.id}' do
describe 'PATCH /api/v1/accounts/{account.id}' do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:admin) { create(:user, account: account, role: :administrator) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}"
patch "/api/v1/accounts/#{account.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthorized user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}",
headers: agent.create_new_auth_token
patch "/api/v1/accounts/#{account.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
@@ -219,11 +220,20 @@ RSpec.describe 'Accounts API', type: :request do
company_size: '1-10'
}
it 'returns a valid schema' do
patch "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to conform_schema(200)
end
it 'modifies an account' do
put "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
patch "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(account.reload.name).to eq(params[:name])
@@ -242,19 +252,19 @@ RSpec.describe 'Accounts API', type: :request do
it 'updates onboarding step to invite_team if onboarding step is present in account custom attributes' do
account.update(custom_attributes: { onboarding_step: 'account_update' })
put "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
patch "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team')
end
it 'will not update onboarding step if onboarding step is not present in account custom attributes' do
put "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
patch "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(account.reload.custom_attributes['onboarding_step']).to be_nil
end
@@ -262,10 +272,10 @@ RSpec.describe 'Accounts API', type: :request do
it 'Throws error 422' do
params[:name] = 'test' * 999
put "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
patch "/api/v1/accounts/#{account.id}",
params: params,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
@@ -21,6 +21,7 @@ RSpec.describe 'Profile API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
json_response = response.parsed_body
expect(json_response['id']).to eq(agent.id)
expect(json_response['email']).to eq(agent.email)
@@ -50,6 +51,7 @@ RSpec.describe 'Profile API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
json_response = response.parsed_body
agent.reload
expect(json_response['id']).to eq(agent.id)
@@ -64,6 +66,7 @@ RSpec.describe 'Profile API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
agent.reload
expect(agent.custom_attributes['phone_number']).to eq('+123456789')
@@ -91,6 +94,7 @@ RSpec.describe 'Profile API', type: :request do
as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(agent.reload.valid_password?('Test1234!')).to be true
end
@@ -144,6 +144,7 @@ RSpec.describe 'Platform Accounts API', type: :request do
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
expect(response).to conform_schema(200)
expect(response.body).to include(account.name)
end
end
@@ -259,10 +259,12 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
message_content: 'Hello assistant',
message_history: [
{ role: 'user', content: 'Previous message' },
{ role: 'assistant', content: 'Previous response' }
{ role: 'assistant', content: 'Previous response', agent_name: 'billing_scenario' }
]
}
end
let(:chat_service) { instance_double(Captain::Llm::AssistantChatService) }
let(:agent_runner_service) { instance_double(Captain::Assistant::AgentRunnerService) }
context 'when it is an un-authenticated user' do
it 'returns unauthorized' do
@@ -274,11 +276,14 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
end
end
context 'when it is an agent' do
it 'generates a response' do
chat_service = instance_double(Captain::Llm::AssistantChatService)
allow(Captain::Llm::AssistantChatService).to receive(:new).with(assistant: assistant).and_return(chat_service)
context 'when captain v2 is disabled' do
it 'generates a response with the legacy assistant chat service' do
allow(Captain::Llm::AssistantChatService).to receive(:new).with(
assistant: assistant,
source: 'playground'
).and_return(chat_service)
allow(chat_service).to receive(:generate_response).and_return({ content: 'Assistant response' })
expect(Captain::Assistant::AgentRunnerService).not_to receive(:new)
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: valid_params,
@@ -292,14 +297,15 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
)
expect(json_response[:content]).to eq('Assistant response')
end
end
context 'when message_history is not provided' do
it 'uses empty array as default' do
params_without_history = { message_content: 'Hello assistant' }
chat_service = instance_double(Captain::Llm::AssistantChatService)
allow(Captain::Llm::AssistantChatService).to receive(:new).with(assistant: assistant).and_return(chat_service)
allow(Captain::Llm::AssistantChatService).to receive(:new).with(
assistant: assistant,
source: 'playground'
).and_return(chat_service)
allow(chat_service).to receive(:generate_response).and_return({ content: 'Assistant response' })
expect(Captain::Assistant::AgentRunnerService).not_to receive(:new)
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: params_without_history,
@@ -313,5 +319,53 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
)
end
end
context 'when captain v2 is enabled' do
before do
account.enable_features('captain_integration_v2')
end
it 'generates a response with the agent runner service' do
allow(Captain::Assistant::AgentRunnerService).to receive(:new).with(
assistant: assistant,
source: 'playground'
).and_return(agent_runner_service)
allow(agent_runner_service).to receive(:generate_response).and_return({ response: 'Assistant response' })
expect(Captain::Llm::AssistantChatService).not_to receive(:new)
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: valid_params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(agent_runner_service).to have_received(:generate_response).with(
message_history: valid_params[:message_history] + [{ role: 'user', content: valid_params[:message_content] }]
)
expect(json_response[:response]).to eq('Assistant response')
end
it 'does not duplicate the latest user message if it is already in history' do
params_with_latest_message = {
message_content: 'Hello assistant',
message_history: [{ role: 'user', content: 'Hello assistant' }]
}
allow(Captain::Assistant::AgentRunnerService).to receive(:new).with(
assistant: assistant,
source: 'playground'
).and_return(agent_runner_service)
allow(agent_runner_service).to receive(:generate_response).and_return({ response: 'Assistant response' })
post "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/playground",
params: params_with_latest_message,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(agent_runner_service).to have_received(:generate_response).with(
message_history: params_with_latest_message[:message_history]
)
end
end
end
end
@@ -42,6 +42,45 @@ RSpec.describe Captain::Scenario, type: :model do
end
end
describe '#handoff_key' do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
it 'uses id plus readable slug for persisted scenarios' do
scenario = create(:captain_scenario, assistant: assistant, account: account,
title: 'Handle complex refund requests requiring manager approval steps')
expect(scenario.handoff_key).to start_with("scenario_#{scenario.id}_")
expect(scenario.handoff_key).to end_with('_agent')
expect("handoff_to_#{scenario.handoff_key}".length).to be <= 60
end
it 'uses a truncated slug key for unsaved scenarios' do
scenario = build(:captain_scenario, assistant: assistant, account: account,
title: 'Troubleshoot payment gateway errors for recurring subscription charges')
expect(scenario.handoff_key).to match(/\Ascenario_draft_[a-z0-9_]+_agent\z/)
expect("handoff_to_#{scenario.handoff_key}".length).to be <= 60
end
it 'stays within length budget even for large ids' do
scenario = build(:captain_scenario, assistant: assistant, account: account,
title: 'A very long scenario title used only for budget verification')
allow(scenario).to receive(:id).and_return(1_234_567_890_123_456_789)
expect("handoff_to_#{scenario.handoff_key}".length).to be <= 60
end
it 'exposes handoff keys in assistant prompt context' do
scenario = create(:captain_scenario, assistant: assistant, account: account)
prompt_context = assistant.send(:prompt_context)
scenario_config = prompt_context[:scenarios].find { |entry| entry[:title] == scenario.title }
expect(scenario_config[:key]).to eq(scenario.handoff_key)
end
end
describe 'tool validation and population' do
let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account) }
@@ -24,7 +24,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'schedules captain response job for incoming messages on pending conversations' do
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
@@ -43,7 +43,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'schedules captain response job outside business hours (Captain always responds when configured)' do
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
it 'performs captain handoff when quota is exceeded (OOO template will kick in after handoff)' do
@@ -52,7 +52,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
expect(conversation.reload.status).to eq('open')
end
@@ -62,7 +62,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
end
@@ -76,7 +76,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'schedules captain response job regardless of time' do
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
@@ -95,7 +95,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
end
it 'performs handoff within business hours when quota exceeded' do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
expect(conversation.reload.status).to eq('open')
end
@@ -110,7 +110,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'does not schedule captain response job' do
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
@@ -122,7 +122,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'does not schedule captain response job' do
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end
end
@@ -130,7 +130,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'does not schedule captain response job' do
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
create(:message, conversation: conversation, message_type: :outgoing)
create(:message, conversation: conversation, message_type: :outgoing, account: account)
end
end
@@ -144,7 +144,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.not_to(change { conversation.reload.messages.template.count })
end
@@ -160,7 +160,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
)
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.not_to(change { conversation.reload.messages.template.count })
end
end
@@ -174,7 +174,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.to change { conversation.reload.messages.template.count }.by(1)
greeting_message = conversation.reload.messages.template.last
@@ -193,7 +193,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
)
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.to change { conversation.reload.messages.template.count }.by(1)
out_of_office_message = conversation.reload.messages.template.last
@@ -211,7 +211,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.to change { conversation.reload.messages.template.count }.by(1)
greeting_message = conversation.reload.messages.template.last
@@ -230,7 +230,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
)
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.to change { conversation.reload.messages.template.count }.by(1)
out_of_office_message = conversation.reload.messages.template.last
@@ -245,7 +245,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'schedules captain response job for incoming messages on pending campaign conversations' do
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(campaign_conversation, assistant)
create(:message, conversation: campaign_conversation, message_type: :incoming)
create(:message, conversation: campaign_conversation, message_type: :incoming, account: account)
end
it 'does not send greeting template on campaign conversations' do
@@ -255,7 +255,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::Greeting).to receive(:new).and_return(greeting_service)
allow(greeting_service).to receive(:perform).and_return(true)
create(:message, conversation: campaign_conversation, message_type: :incoming)
create(:message, conversation: campaign_conversation, message_type: :incoming, account: account)
expect(MessageTemplates::Template::Greeting).not_to have_received(:new)
end
@@ -271,7 +271,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: campaign_conversation, message_type: :incoming)
create(:message, conversation: campaign_conversation, message_type: :incoming, account: account)
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
end
@@ -284,7 +284,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::EmailCollect).to receive(:new).and_return(email_collect_service)
allow(email_collect_service).to receive(:perform).and_return(true)
create(:message, conversation: campaign_conversation, message_type: :incoming)
create(:message, conversation: campaign_conversation, message_type: :incoming, account: account)
expect(MessageTemplates::Template::EmailCollect).not_to have_received(:new)
end
@@ -304,7 +304,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
)
expect do
create(:message, conversation: campaign_conversation, message_type: :incoming)
create(:message, conversation: campaign_conversation, message_type: :incoming, account: account)
end.not_to(change { campaign_conversation.messages.template.count })
end
end
@@ -332,7 +332,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'sends out of office message after handoff due to quota exceeded' do
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.to change { conversation.messages.template.count }.by(1)
expect(conversation.reload.status).to eq('open')
@@ -356,7 +356,7 @@ RSpec.describe MessageTemplates::HookExecutionService do
it 'does not send out of office message after handoff' do
expect do
create(:message, conversation: conversation, message_type: :incoming)
create(:message, conversation: conversation, message_type: :incoming, account: account)
end.not_to(change { conversation.messages.template.count })
expect(conversation.reload.status).to eq('open')
+4
View File
@@ -75,6 +75,10 @@ RSpec.configure do |config|
config.include ActiveSupport::Testing::TimeHelpers
config.include ActionCable::TestHelper
config.include ActiveJob::TestHelper
# OpenAPI response validation via Skooma
path_to_openapi = Rails.root.join('swagger/swagger.json')
config.include Skooma::RSpec[path_to_openapi], type: :request
end
Shoulda::Matchers.configure do |config|
+20
View File
@@ -50,6 +50,26 @@ describe ActionService do
action_service.assign_agent(['nil'])
expect(conversation.reload.assignee).to be_nil
end
context 'when agent is confirmed' do
it 'assigns the agent to the conversation' do
inbox_member
action_service.assign_agent([agent.id])
expect(conversation.reload.assignee).to eq(agent)
end
end
context 'when agent is unconfirmed' do
let(:unconfirmed_agent) { create(:user, account: account, role: :agent, skip_confirmation: false) }
let(:unconfirmed_inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: unconfirmed_agent) }
it 'does not assign unconfirmed agent to the conversation' do
unconfirmed_inbox_member
original_assignee = conversation.assignee
action_service.assign_agent([unconfirmed_agent.id])
expect(conversation.reload.assignee).to eq(original_assignee)
end
end
end
describe '#assign_team' do
@@ -15,7 +15,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::Greeting).to receive(:new)
# described class gets called in message after commit
create(:message, conversation: conversation, message_type: 'activity', content: 'Conversation marked resolved!!')
create(:message, conversation: conversation, account: conversation.account, message_type: 'activity', content: 'Conversation marked resolved!!')
expect(MessageTemplates::Template::Greeting).not_to have_received(:new)
expect(MessageTemplates::Template::EmailCollect).not_to have_received(:new)
@@ -36,7 +36,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::Greeting).to receive(:new)
# described class gets called in message after commit
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::Greeting).not_to have_received(:new)
expect(MessageTemplates::Template::EmailCollect).to have_received(:new).with(conversation: message.conversation)
@@ -54,7 +54,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::Greeting).to receive(:new).and_return(greeting_service)
allow(greeting_service).to receive(:perform).and_return(true)
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::Greeting).not_to have_received(:new).with(conversation: message.conversation)
end
end
@@ -75,7 +75,7 @@ describe MessageTemplates::HookExecutionService do
allow(greeting_service).to receive(:perform).and_return(true)
# described class gets called in message after commit
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::Greeting).to have_received(:new).with(conversation: message.conversation)
expect(greeting_service).to have_received(:perform)
@@ -90,7 +90,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::EmailCollect).to receive(:new).and_return(true)
# described class gets called in message after commit
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::EmailCollect).not_to have_received(:new).with(conversation: message.conversation)
end
@@ -105,7 +105,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::EmailCollect).to receive(:new).and_return(true)
# described class gets called in message after commit
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::EmailCollect).not_to have_received(:new).with(conversation: message.conversation)
end
@@ -123,7 +123,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::Greeting).to receive(:new).and_return(greeting_service)
allow(greeting_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation)
create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::Greeting).not_to have_received(:new)
end
@@ -139,7 +139,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation)
create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
end
@@ -151,7 +151,7 @@ describe MessageTemplates::HookExecutionService do
conversation = create(:conversation, contact: contact)
conversation.inbox.update(greeting_enabled: true, enable_email_collect: true, greeting_message: 'Hi, this is a greeting message')
message = create(:message, conversation: conversation, content_type: :incoming_email)
message = create(:message, conversation: conversation, account: conversation.account, content_type: :incoming_email)
message.content_attributes = { email: { auto_reply: true } }
message.save!
@@ -188,7 +188,7 @@ describe MessageTemplates::HookExecutionService do
allow(out_of_office_service).to receive(:perform).and_return(true)
# described class gets called in message after commit
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::OutOfOffice).to have_received(:new).with(conversation: message.conversation)
expect(out_of_office_service).to have_received(:perform)
@@ -202,13 +202,13 @@ describe MessageTemplates::HookExecutionService do
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
conversation.inbox.working_hours.today.update!(closed_all_day: true)
create(:message, conversation: conversation, message_type: :outgoing, created_at: 2.minutes.ago)
create(:message, conversation: conversation, account: conversation.account, message_type: :outgoing, created_at: 2.minutes.ago)
out_of_office_service = double
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation)
create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
expect(out_of_office_service).not_to have_received(:perform)
@@ -221,13 +221,13 @@ describe MessageTemplates::HookExecutionService do
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
conversation.inbox.working_hours.today.update!(closed_all_day: true)
create(:message, conversation: conversation, private: true, message_type: :outgoing, created_at: 2.minutes.ago)
create(:message, conversation: conversation, account: conversation.account, private: true, message_type: :outgoing, created_at: 2.minutes.ago)
out_of_office_service = double
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation)
create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::OutOfOffice).to have_received(:new).with(conversation: conversation)
expect(out_of_office_service).to have_received(:perform)
@@ -247,7 +247,7 @@ describe MessageTemplates::HookExecutionService do
allow(out_of_office_service).to receive(:perform).and_return(true)
# described class gets called in message after commit
message = create(:message, conversation: conversation, message_type: 'outgoing')
message = create(:message, conversation: conversation, account: conversation.account, message_type: 'outgoing')
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new).with(conversation: message.conversation)
expect(out_of_office_service).not_to have_received(:perform)
@@ -265,7 +265,7 @@ describe MessageTemplates::HookExecutionService do
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(false)
message = create(:message, conversation: conversation)
message = create(:message, conversation: conversation, account: conversation.account)
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new).with(conversation: message.conversation)
expect(out_of_office_service).not_to receive(:perform)
end
+7
View File
@@ -0,0 +1,7 @@
require 'rails_helper'
RSpec.describe 'OpenAPI document', type: :request do
it 'is valid against the OpenAPI 3.1.0 meta-schema' do
expect(skooma_openapi_schema).to be_valid_document
end
end
+6
View File
@@ -70,6 +70,8 @@ platform_account:
$ref: ./resource/platform_account.yml
team:
$ref: ./resource/team.yml
label:
$ref: ./resource/label.yml
integrations_app:
$ref: ./resource/integrations/app.yml
integrations_hook:
@@ -142,6 +144,10 @@ inbox_update_payload:
team_create_update_payload:
$ref: ./request/team/create_update_payload.yml
# Label
label_create_update_payload:
$ref: ./request/label/create_update_payload.yml
# Custom Filter
custom_filter_create_update_payload:
$ref: ./request/custom_filter/create_update_payload.yml
@@ -18,20 +18,23 @@ properties:
example: 'support@example.com'
# Settings parameters (stored in settings JSONB column)
auto_resolve_after:
type: integer
type:
- integer
- 'null'
minimum: 10
maximum: 1439856
nullable: true
description: Auto resolve conversations after specified minutes
example: 1440
auto_resolve_message:
type: string
nullable: true
type:
- string
- 'null'
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
type:
- boolean
- 'null'
description: Whether to ignore waiting conversations for auto resolve
example: false
# Custom attributes parameters (stored in custom_attributes JSONB column)
@@ -1,7 +1,6 @@
type: object
required:
- source_id
- inbox_id
properties:
source_id:
type: string
@@ -0,0 +1,18 @@
type: object
properties:
title:
type: string
description: The label title
example: support
description:
type: string
description: A short description for the label
example: Conversations that need support follow-up
color:
type: string
description: Hex color code for the label
example: '#1f93ff'
show_on_sidebar:
type: boolean
description: Whether the label should appear in the sidebar
example: true
@@ -26,9 +26,7 @@ properties:
type: object
description: Cache keys for the account
features:
type: array
items:
type: string
type: object
description: Enabled features for the account
settings:
type: object
@@ -48,16 +46,24 @@ properties:
description: Custom attributes of the account
properties:
plan_name:
type: string
type:
- string
- 'null'
description: Subscription plan name
subscribed_quantity:
type: number
type:
- number
- 'null'
description: Subscribed quantity
subscription_status:
type: string
type:
- string
- 'null'
description: Subscription status
subscription_ends_on:
type: string
type:
- string
- 'null'
format: date
description: Subscription end date
industry:
@@ -3,7 +3,9 @@ allOf:
- type: object
properties:
latest_chatwoot_version:
type: string
type:
- string
- 'null'
description: Latest version of Chatwoot available
example: "3.0.0"
subscribed_features:
+3 -1
View File
@@ -31,5 +31,7 @@ properties:
type: string
description: The thumbnail of the agent
custom_role_id:
type: integer
type:
- integer
- 'null'
description: The custom role id of the agent
+6 -4
View File
@@ -38,8 +38,9 @@ properties:
type: integer
description: Version number of the audit log entry
comment:
type: string
nullable: true
type:
- string
- 'null'
description: Optional comment associated with the audit log entry
request_uuid:
type: string
@@ -48,6 +49,7 @@ properties:
type: integer
description: Unix timestamp when the audit log entry was created
remote_address:
type: string
nullable: true
type:
- string
- 'null'
description: IP address from which the action was performed
@@ -31,9 +31,10 @@ properties:
type: string
description: Status of the message
source_id:
type: string
type:
- string
- 'null'
description: Source ID of the message
nullable: true
content_type:
type: string
description: Type of the content
@@ -41,13 +42,15 @@ properties:
type: object
description: Attributes of the content
sender_type:
type: string
type:
- string
- 'null'
description: Type of the sender
nullable: true
sender_id:
type: integer
type:
- integer
- 'null'
description: ID of the sender
nullable: true
external_source_ids:
type: object
description: External source IDs
@@ -55,9 +58,10 @@ properties:
type: object
description: Additional attributes of the message
processed_message_content:
type: string
type:
- string
- 'null'
description: Processed message content
nullable: true
sentiment:
type: object
description: Sentiment analysis of the message
@@ -66,9 +70,10 @@ properties:
description: Conversation details
properties:
assignee_id:
type: integer
type:
- integer
- 'null'
description: ID of the assignee
nullable: true
unread_count:
type: integer
description: Count of unread messages
@@ -11,7 +11,9 @@ properties:
type: string
description: Country of the contact
country_code:
type: string
type:
- string
- 'null'
description: Country code of the contact
created_at_ip:
type: string
@@ -26,16 +28,18 @@ properties:
type: integer
description: The ID of the contact
identifier:
type: string
type:
- string
- 'null'
description: The identifier of the contact
nullable: true
name:
type: string
description: The name of the contact
phone_number:
type: string
type:
- string
- 'null'
description: The phone number of the contact
nullable: true
thumbnail:
type: string
description: The thumbnail of the contact
@@ -22,6 +22,7 @@ properties:
type: string
description: Type of channel
provider:
type: string
description: Provider of the inbox
nullable: true
type:
- string
- 'null'
description: Provider of the inbox
@@ -11,7 +11,9 @@ properties:
type: string
description: Country of the contact
country_code:
type: string
type:
- string
- 'null'
description: Country code of the contact
created_at_ip:
type: string
@@ -21,9 +23,10 @@ properties:
description: Availability status of the contact
enum: ["online", "offline"]
email:
type: string
type:
- string
- 'null'
description: The email address of the contact
nullable: true
id:
type: integer
description: The ID of the contact
@@ -31,16 +34,18 @@ properties:
type: string
description: The name of the contact
phone_number:
type: string
type:
- string
- 'null'
description: The phone number of the contact
nullable: true
blocked:
type: boolean
description: Whether the contact is blocked
identifier:
type: string
type:
- string
- 'null'
description: The identifier of the contact
nullable: true
thumbnail:
type: string
description: The thumbnail of the contact
@@ -48,9 +53,10 @@ properties:
type: object
description: The custom attributes of the contact
last_activity_at:
type: integer
type:
- integer
- 'null'
description: Timestamp of last activity
nullable: true
created_at:
type: integer
description: Timestamp when contact was created
@@ -4,5 +4,7 @@ properties:
type: integer
description: Total number of contacts
current_page:
type: string
description: Current page number
type:
- string
- integer
description: Current page number
+19 -8
View File
@@ -43,7 +43,9 @@ properties:
type: boolean
description: Whether the conversation is muted
snoozed_until:
type: number
type:
- number
- 'null'
description: The time at which the conversation will be unmuted
status:
type: string
@@ -56,29 +58,38 @@ properties:
type: number
description: The time at which conversation was updated
timestamp:
type: string
type: number
description: The time at which conversation was created
first_reply_created_at:
type: number
type:
- number
- 'null'
description: The time at which the first reply was created
unread_count:
type: number
description: The number of unread messages
last_non_activity_message:
type: object
$ref: '#/components/schemas/message'
oneOf:
- $ref: '#/components/schemas/message'
- type: 'null'
description: The last non activity message
last_activity_at:
type: number
description: The last activity at of the conversation
priority:
type: string
type:
- string
- 'null'
description: The priority of the conversation
waiting_since:
type: number
type:
- number
- 'null'
description: The time at which the conversation was waiting
sla_policy_id:
type: number
type:
- number
- 'null'
description: The ID of the SLA policy
applied_sla:
type: object
@@ -51,10 +51,12 @@ properties:
description: The agent assigned to the conversation
nullable: true
agent_last_seen_at:
type: string
type:
- string
- 'null'
description: Timestamp when the agent last saw the conversation
nullable: true
assignee_last_seen_at:
type: string
description: Timestamp when the assignee last saw the conversation
nullable: true
type:
- string
- 'null'
description: Timestamp when the assignee last saw the conversation
@@ -13,7 +13,9 @@ properties:
type: string
description: The availability status of the sender
email:
type: string
type:
- string
- 'null'
description: The email of the sender
id:
type: number
@@ -22,16 +24,22 @@ properties:
type: string
description: The name of the sender
phone_number:
type: string
type:
- string
- 'null'
description: The phone number of the sender
blocked:
type: boolean
description: Whether the sender is blocked
identifier:
type: string
type:
- string
- 'null'
description: The identifier of the sender
thumbnail:
type: string
type:
- string
- 'null'
description: Avatar URL of the contact
custom_attributes:
type: object
+44 -14
View File
@@ -28,16 +28,22 @@ properties:
type: string
description: Script used to load the website widget
welcome_title:
type: string
type:
- string
- 'null'
description: Welcome title to be displayed on the widget
welcome_tagline:
type: string
type:
- string
- 'null'
description: Welcome tagline to be displayed on the widget
greeting_enabled:
type: boolean
description: The flag which shows whether greeting is enabled
greeting_message:
type: string
type:
- string
- 'null'
description: A greeting message when the user starts the conversation
channel_id:
type: number
@@ -55,7 +61,9 @@ properties:
type: object
description: Configuration settings for auto assignment
out_of_office_message:
type: string
type:
- string
- 'null'
description: Message to show when agents are out of office
working_hours:
type: array
@@ -70,16 +78,24 @@ properties:
type: boolean
description: Whether the inbox is closed for the entire day
open_hour:
type: number
type:
- number
- 'null'
description: Hour when inbox opens (0-23)
open_minutes:
type: number
type:
- number
- 'null'
description: Minutes of the hour when inbox opens (0-59)
close_hour:
type: number
type:
- number
- 'null'
description: Hour when inbox closes (0-23)
close_minutes:
type: number
type:
- number
- 'null'
description: Minutes of the hour when inbox closes (0-59)
open_all_day:
type: boolean
@@ -88,7 +104,9 @@ properties:
type: string
description: Timezone configuration for the inbox
callback_webhook_url:
type: string
type:
- string
- 'null'
description: Webhook URL for callbacks
allow_messages_after_resolved:
type: boolean
@@ -100,26 +118,38 @@ properties:
type: string
description: Type of sender name to display (e.g., friendly)
business_name:
type: string
type:
- string
- 'null'
description: Business name associated with the inbox
hmac_mandatory:
type: boolean
description: Whether HMAC verification is mandatory
selected_feature_flags:
type: object
type:
- array
- 'null'
description: Selected feature flags for the inbox
items:
type: string
reply_time:
type: string
description: Expected reply time
messaging_service_sid:
type: string
type:
- string
- 'null'
description: Messaging service SID for SMS providers
phone_number:
type: string
type:
- string
- 'null'
description: Phone number associated with the inbox
medium:
type: string
description: Medium of communication (e.g., sms, email)
provider:
type: string
type:
- string
- 'null'
description: Provider of the channel
+17
View File
@@ -0,0 +1,17 @@
type: object
properties:
id:
type: number
description: The ID of the label
title:
type: string
description: The title of the label
description:
type: string
description: The description of the label
color:
type: string
description: Hex color code for the label
show_on_sidebar:
type: boolean
description: Whether the label should appear in the sidebar
+34 -14
View File
@@ -17,37 +17,49 @@ properties:
description: The ID of the conversation
message_type:
type: integer
enum: [0, 1, 2]
enum: [0, 1, 2, 3]
description: The type of the message
created_at:
type: integer
description: The time at which message was created
updated_at:
type: integer
type:
- integer
- string
description: The time at which message was updated
private:
type: boolean
description: The flags which shows whether the message is private or not
status:
type: string
enum: ["sent", "delivered", "read", "failed"]
type:
- string
- 'null'
enum: ["sent", "delivered", "read", "failed", null]
description: The status of the message
source_id:
type: string
type:
- string
- 'null'
description: The source ID of the message
content_type:
type: string
enum: ["text", "input_select", "cards", "form"]
type:
- string
- 'null'
enum: ["text", "input_text", "input_textarea", "input_email", "input_select", "cards", "form", "article", "incoming_email", "input_csat", "integrations", "sticker", "voice_call", null]
description: The type of the template message
content_attributes:
type: object
description: The content attributes for each content_type
sender_type:
type: string
enum: ["contact", "agent", "agent_bot"]
type:
- string
- 'null'
enum: ["Contact", "User", "AgentBot", "Captain::Assistant", null]
description: The type of the sender
sender_id:
type: number
type:
- number
- 'null'
description: The ID of the sender
external_source_ids:
type: object
@@ -56,16 +68,24 @@ properties:
type: object
description: The additional attributes of the message
processed_message_content:
type: string
type:
- string
- 'null'
description: The processed message content
sentiment:
type: object
type:
- object
- 'null'
description: The sentiment of the message
conversation:
type: object
type:
- object
- 'null'
description: The conversation object
attachment:
type: object
type:
- object
- 'null'
description: The file object attached to the image
sender:
type: object
@@ -18,7 +18,7 @@ properties:
description: "The type of the message (0: incoming, 1: outgoing, 2: activity, 3: template)"
content_type:
type: string
enum: ["text", "input_select", "cards", "form", "input_csat"]
enum: ["text", "input_text", "input_textarea", "input_email", "input_select", "cards", "form", "article", "incoming_email", "input_csat", "integrations", "sticker", "voice_call"]
description: The type of the message content
status:
type: string
@@ -29,13 +29,15 @@ properties:
description: The content attributes for each content_type
properties:
in_reply_to:
type: string
type:
- string
- 'null'
description: ID of the message this is replying to
nullable: true
echo_id:
type: string
type:
- string
- 'null'
description: The echo ID of the message, used for deduplication
nullable: true
created_at:
type: integer
description: The timestamp when message was created
@@ -43,9 +45,10 @@ properties:
type: boolean
description: The flag which shows whether the message is private or not
source_id:
type: string
type:
- string
- 'null'
description: The source ID of the message
nullable: true
sender:
$ref: '#/components/schemas/contact_detail'
description: The sender of the message (only for incoming messages)
@@ -76,4 +79,4 @@ properties:
description: The thumbnail URL of the attached file
file_size:
type: number
description: The size of the attached file in bytes
description: The size of the attached file in bytes
+9 -6
View File
@@ -4,16 +4,19 @@ properties:
type: integer
description: Total number of articles
archived_articles_count:
type: integer
nullable: true
type:
- integer
- 'null'
description: Number of archived articles
published_count:
type: integer
nullable: true
type:
- integer
- 'null'
description: Number of published articles
draft_articles_count:
type: integer
nullable: true
type:
- integer
- 'null'
description: Number of draft articles
categories_count:
type: integer
@@ -26,16 +26,19 @@ properties:
type: number
description: ID of the account
conversation_id:
type: number
nullable: true
type:
- number
- 'null'
description: ID of the conversation
inbox_id:
type: number
nullable: true
type:
- number
- 'null'
description: ID of the inbox
user_id:
type: number
nullable: true
type:
- number
- 'null'
description: ID of the user/agent
created_at:
type: string
@@ -13,16 +13,19 @@ items:
type: number
description: Number of conversations resolved by the agent during the date range
avg_resolution_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) to resolve conversations. Null if no data available.
avg_first_response_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) for the first response. Null if no data available.
avg_reply_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) between replies. Null if no data available.
example:
- id: 1
@@ -13,16 +13,19 @@ items:
type: number
description: Number of conversations resolved in the inbox during the date range
avg_resolution_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) to resolve conversations. Null if no data available.
avg_first_response_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) for the first response. Null if no data available.
avg_reply_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) between replies. Null if no data available.
example:
- id: 1
@@ -13,16 +13,19 @@ items:
type: number
description: Number of conversations resolved by the team during the date range
avg_resolution_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) to resolve conversations. Null if no data available.
avg_first_response_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) for the first response. Null if no data available.
avg_reply_time:
type: number
nullable: true
type:
- number
- 'null'
description: Average time (in seconds) between replies. Null if no data available.
example:
- id: 1
+3 -1
View File
@@ -7,7 +7,9 @@ properties:
type: string
description: The name of the team
description:
type: string
type:
- string
- 'null'
description: The description about the team
allow_auto_assign:
type: boolean
+21 -12
View File
@@ -13,17 +13,21 @@ properties:
confirmed:
type: boolean
display_name:
type: string
nullable: true
type:
- string
- 'null'
message_signature:
type: string
nullable: true
type:
- string
- 'null'
email:
type: string
hmac_identifier:
type: string
inviter_id:
type: number
type:
- number
- 'null'
name:
type: string
provider:
@@ -38,8 +42,9 @@ properties:
uid:
type: string
type:
type: string
nullable: true
type:
- string
- 'null'
custom_attributes:
type: object
description: Available for users who are created through platform APIs and has custom attributes associated.
@@ -55,7 +60,9 @@ properties:
status:
type: string
active_at:
type: string
type:
- string
- 'null'
format: date-time
role:
type: string
@@ -71,8 +78,10 @@ properties:
auto_offline:
type: boolean
custom_role_id:
type: number
nullable: true
type:
- number
- 'null'
custom_role:
type: object
nullable: true
type:
- object
- 'null'
+4 -1
View File
@@ -1,4 +1,4 @@
openapi: '3.0.4'
openapi: '3.1.0'
info:
title: Chatwoot
description: This is the API documentation for Chatwoot server.
@@ -67,6 +67,8 @@ tags:
description: Communication channels setup
- name: Integrations
description: Third-party integrations
- name: Labels
description: Account label management APIs
- name: Messages
description: Message management APIs
- name: Profile
@@ -112,6 +114,7 @@ x-tagGroups:
- Custom Filters
- Inboxes
- Integrations
- Labels
- Messages
- Profile
- Reports
@@ -38,10 +38,14 @@ responses:
assignee:
$ref: '#/components/schemas/agent'
agent_last_seen_at:
type: string
type:
- string
- 'null'
format: date-time
assignee_last_seen_at:
type: string
type:
- string
- 'null'
format: date-time
payload:
type: array
@@ -25,6 +25,10 @@ requestBody:
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/conversation'
'401':
description: Unauthorized
content:
@@ -33,3 +33,38 @@ get:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
post:
tags:
- Inboxes
operationId: inboxCreation
summary: Create an inbox
description: You can create more than one website inbox in each account
security:
- userApiKey: []
parameters:
- $ref: '#/components/parameters/account_id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/inbox_create_payload'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/inbox'
'404':
description: Inbox not found
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
+35 -2
View File
@@ -1,3 +1,38 @@
get:
tags:
- Inboxes
operationId: GetInbox
summary: Get an inbox
security:
- userApiKey: []
description: Get an inbox available in the current account
parameters:
- $ref: '#/components/parameters/account_id'
- name: id
in: path
schema:
type: number
description: ID of the inbox
required: true
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/inbox'
'404':
description: Inbox not found
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
patch:
tags:
- Inboxes
@@ -26,8 +61,6 @@ patch:
content:
application/json:
schema:
type: object
description: 'Updated inbox object'
$ref: '#/components/schemas/inbox'
'404':
description: Inbox not found
@@ -0,0 +1,26 @@
tags:
- Labels
operationId: create-a-label
summary: Create a label
security:
- userApiKey: []
description: Create a label in the account
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/label_create_update_payload'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/label'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -0,0 +1,22 @@
tags:
- Labels
operationId: delete-a-label
summary: Delete a label
security:
- userApiKey: []
description: Delete a label from the account
responses:
'200':
description: Success
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'404':
description: The label does not exist in the account
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -0,0 +1,26 @@
tags:
- Labels
operationId: list-all-labels
summary: List all labels
security:
- userApiKey: []
description: List all labels available in the current account
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
payload:
type: array
description: Array of labels
items:
$ref: '#/components/schemas/label'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
+26
View File
@@ -0,0 +1,26 @@
tags:
- Labels
operationId: get-details-of-a-single-label
summary: Get a label
security:
- userApiKey: []
description: Get the details of a label in the account
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/label'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
'404':
description: The given label ID does not exist in the account
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -0,0 +1,26 @@
tags:
- Labels
operationId: update-a-label
summary: Update a label
security:
- userApiKey: []
description: Update a label's attributes
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/label_create_update_payload'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/label'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
+25 -5
View File
@@ -167,7 +167,7 @@
# ------------ Application API routes ------------#
# Accounts
/api/v1/accounts/{id}:
/api/v1/accounts/{account_id}:
parameters:
- $ref: '#/components/parameters/account_id'
get:
@@ -413,10 +413,6 @@
# Inboxes
/api/v1/accounts/{account_id}/inboxes:
$ref: ./application/inboxes/index.yml
/api/v1/accounts/{account_id}/inboxes/{id}/:
$ref: ./application/inboxes/show.yml
/api/v1/accounts/{account_id}/inboxes/:
$ref: ./application/inboxes/create.yml
/api/v1/accounts/{account_id}/inboxes/{id}:
$ref: ./application/inboxes/update.yml
/api/v1/accounts/{account_id}/inboxes/{id}/agent_bot:
@@ -442,6 +438,30 @@
delete:
$ref: ./application/inboxes/inbox_members/delete.yml
# Labels
/api/v1/accounts/{account_id}/labels:
parameters:
- $ref: '#/components/parameters/account_id'
get:
$ref: ./application/labels/index.yml
post:
$ref: ./application/labels/create.yml
/api/v1/accounts/{account_id}/labels/{id}:
parameters:
- $ref: '#/components/parameters/account_id'
- name: id
in: path
required: true
schema:
type: number
description: ID of the label
get:
$ref: ./application/labels/show.yml
patch:
$ref: ./application/labels/update.yml
delete:
$ref: ./application/labels/delete.yml
# Messages
/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages:
parameters:
+77
View File
@@ -19,3 +19,80 @@ get:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
put:
tags:
- Profile
operationId: updateProfile
summary: Update user profile
description: Update the user profile details
security:
- userApiKey: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- profile
properties:
profile:
type: object
properties:
name:
type: string
email:
type: string
display_name:
type: string
message_signature:
type: string
phone_number:
type: string
current_password:
type: string
password:
type: string
password_confirmation:
type: string
ui_settings:
type: object
multipart/form-data:
schema:
type: object
required:
- profile
properties:
profile:
type: object
properties:
name:
type: string
email:
type: string
display_name:
type: string
message_signature:
type: string
phone_number:
type: string
current_password:
type: string
password:
type: string
password_confirmation:
type: string
avatar:
type: string
format: binary
ui_settings:
type: object
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/user'
'401':
description: Unauthorized
+952 -243
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -1,4 +1,4 @@
openapi: '3.0.4'
openapi: '3.1.0'
info:
title: Chatwoot - Application API
description: Application API endpoints for Chatwoot
@@ -36,6 +36,8 @@ tags:
description: Manage inboxes
- name: Integrations
description: Manage integrations
- name: Labels
description: Manage account labels
- name: Messages
description: Manage messages
- name: Profile
@@ -62,4 +64,4 @@ components:
type: apiKey
in: header
name: api_access_token
description: This token can be obtained by visiting the profile page or via rails console. Provides access to endpoints based on the user permissions levels.
description: This token can be obtained by visiting the profile page or via rails console. Provides access to endpoints based on the user permissions levels.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
openapi: '3.0.4'
openapi: '3.1.0'
info:
title: Chatwoot - Client API
description: Client API endpoints for Chatwoot
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
openapi: '3.0.4'
openapi: '3.1.0'
info:
title: Chatwoot - Other APIs
description: Other API endpoints for Chatwoot
+1 -1
View File
@@ -1,4 +1,4 @@
openapi: '3.0.4'
openapi: '3.1.0'
info:
title: Chatwoot - Platform API
description: Platform API endpoints for Chatwoot
File diff suppressed because it is too large Load Diff