From d408f664cba32288869cb1886742153ec25036ca Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:44:05 +0700 Subject: [PATCH 1/5] fix: add linear integration for the startup plan (#13136) Co-authored-by: tanmay --- .../services/enterprise/billing/handle_stripe_event_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb index 390786ec8..eab6a81dc 100644 --- a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb +++ b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb @@ -18,6 +18,7 @@ class Enterprise::Billing::HandleStripeEventService captain_integration advanced_search_indexing advanced_search + linear_integration ].freeze # Additional features available starting with the Business plan From 53c21e6ad3a36364d42a9ba891c816a778d8e41e Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:17:35 +0530 Subject: [PATCH 2/5] fix: Prevent invalid attachments from blocking text paste (#13135) --- .../components/ActionButtons.vue | 11 ++++++---- .../components/widgets/WootWriter/Editor.vue | 15 ++++++++++---- .../widgets/conversation/ReplyBox.vue | 20 +++++++++---------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index c2971c60d..6def771a0 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -171,10 +171,13 @@ const onPaste = e => { const files = e.clipboardData?.files; if (!files?.length) return; - Array.from(files).forEach(file => { - const { name, type, size } = file; - onFileUpload({ file, name, type, size }); - }); + // Filter valid files (non-zero size) + Array.from(files) + .filter(file => file.size > 0) + .forEach(file => { + const { name, type, size } = file; + onFileUpload({ file, name, type, size }); + }); }; useEventListener(document, 'paste', onPaste); diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index 7a2a489ac..4b83556de 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -676,11 +676,18 @@ function createEditorView() { typingIndicator.stop(); emit('blur'); }, - paste: (_view, event) => { + paste: (view, event) => { if (props.disabled) return; - const data = event.clipboardData.files; - if (data.length > 0) { - event.preventDefault(); + const { files } = event.clipboardData; + if (!files.length) return; + event.preventDefault(); + // Paste text content alongside files (e.g., spreadsheet data from Numbers app) + // Numbers app includes invalid 0-byte attachments with text, so we paste the text here + // while ReplyBox filters and handles valid file attachments + const text = event.clipboardData.getData('text/plain'); + if (text) { + view.dispatch(view.state.tr.insertText(text)); + emitOnChange(); } }, }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 40127aca3..aa67cda0b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -692,20 +692,20 @@ export default { }, onPaste(e) { // Don't handle paste if compose new conversation modal is open - if (this.newConversationModalActive) { - return; - } + if (this.newConversationModalActive) return; + const data = e.clipboardData.files; if (!this.showRichContentEditor && data.length !== 0) { this.$refs.messageInput?.$el?.blur(); } - if (!data.length || !data[0]) { - return; - } - data.forEach(file => { - const { name, type, size } = file; - this.onFileUpload({ name, type, size, file: file }); - }); + + // Filter valid files (non-zero size) + Array.from(e.clipboardData.files) + .filter(file => file.size > 0) + .forEach(file => { + const { name, type, size } = file; + this.onFileUpload({ name, type, size, file }); + }); }, toggleUserMention(currentMentionState) { this.showUserMentions = currentMentionState; From d2e6d6aee3fad279f1988e08c93703b58058381b Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Mon, 22 Dec 2025 20:10:45 -0300 Subject: [PATCH 3/5] fix: Improve handling of empty custom attributes list in settings (#13127) ## Description This PR fixes a UX bug in the Custom Attributes settings page where switching tabs becomes impossible when the currently selected tab has no attributes. Closes #13120 ### The Problem When viewing the Custom Attributes settings page, if one tab (e.g., Conversation) has no attributes, users could not switch to the other tab (e.g., Contact) which might have attributes. ### Root Cause The `SettingsLayout` component receives `no-records-found` prop which, when true, hides the entire body content including the TabBar. Since the TabBar was inside the body slot, it would be hidden whenever the current tab had no attributes, preventing users from switching tabs. ### The Fix - Removed the `no-records-found` and `no-records-message` props from `SettingsLayout` - Moved the empty state message inline within the body, displayed below the TabBar - The TabBar is now always visible regardless of whether there are attributes in the selected tab ### Key Changes - Modified `Index.vue` to handle empty state inline while keeping TabBar accessible ## Type of change - [X] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? 1. Navigate to Settings > Custom Attributes 2. Ensure only one tab (e.g., Contact) has custom attributes 3. Switch to the empty tab (e.g., Conversation) 4. Verify the TabBar remains visible and the empty state message is shown 5. Switch back to the tab with attributes 6. Verify attributes are displayed correctly 7. Repeat with both tabs empty and both tabs with attributes --- .../routes/dashboard/settings/attributes/Index.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue index bf93ffa1f..13b980c75 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue @@ -150,8 +150,6 @@ const derivedAttributes = computed(() => Date: Mon, 22 Dec 2025 18:55:42 -0800 Subject: [PATCH 4/5] Bump version to 4.9.1 --- VERSION_CW | 2 +- config/app.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION_CW b/VERSION_CW index 88f181192..5b341fd79 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -4.8.0 +4.9.1 diff --git a/config/app.yml b/config/app.yml index cba7e1fa4..0c7a390f0 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '4.9.0' + version: '4.9.1' development: <<: *shared diff --git a/package.json b/package.json index e98dc665f..87f44528a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "4.9.0", + "version": "4.9.1", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From d57170712dcddf1656ce50601236d1008720a830 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:59:20 +0700 Subject: [PATCH 5/5] fix: increase the alfred connection pool size to 10 (#13138) ## Description Increased the alfred pool size to 10, so that each worker has enough redis connection thread pool to work ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) ## 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 --- > [!NOTE] > Introduces configurability for the Redis pool used by `alfred`. > > - `$alfred` `ConnectionPool` size now reads from `ENV['REDIS_ALFRED_SIZE']` with a default of `5` > - Adds `REDIS_ALFRED_SIZE=10` to `.env.example` > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 96cdff8c0ea40f82a57d70be053780e87384ed47. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: tanmay --- .env.example | 2 ++ config/initializers/01_redis.rb | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d5c7a76f9..55750b2f2 100644 --- a/.env.example +++ b/.env.example @@ -274,3 +274,5 @@ AZURE_APP_SECRET= # Set to true if you want to remove stale contact inboxes # contact_inboxes with no conversation older than 90 days will be removed # REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false + +# REDIS_ALFRED_SIZE=10 diff --git a/config/initializers/01_redis.rb b/config/initializers/01_redis.rb index 93c12fce7..84ae88d29 100644 --- a/config/initializers/01_redis.rb +++ b/config/initializers/01_redis.rb @@ -5,7 +5,8 @@ # Alfred # Add here as you use it for more features # Used for Round Robin, Conversation Emails & Online Presence -$alfred = ConnectionPool.new(size: 5, timeout: 1) do +alfred_size = ENV.fetch('REDIS_ALFRED_SIZE', 5) +$alfred = ConnectionPool.new(size: alfred_size, timeout: 1) do redis = Rails.env.test? ? MockRedis.new : Redis.new(Redis::Config.app) Redis::Namespace.new('alfred', redis: redis, warning: true) end