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/4] 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/4] 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/4] 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/4] 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}",