From 18a8e3db473213fbdf4f2a3fbca7e12eb9efad66 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:42:07 +0530 Subject: [PATCH 01/18] chore: Update Tehran timezone from GMT+04:30 to GMT+03:30 (#12427) --- .../routes/dashboard/settings/inbox/helpers/timezones.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json index b238add21..810415ffb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/helpers/timezones.json @@ -86,7 +86,7 @@ "Riyadh (GMT+03:00)": "Asia/Riyadh", "Nairobi (GMT+03:00)": "Africa/Nairobi", "Baghdad (GMT+03:00)": "Asia/Baghdad", - "Tehran (GMT+04:30)": "Asia/Tehran", + "Tehran (GMT+03:30)": "Asia/Tehran", "Abu Dhabi (GMT+04:00)": "Asia/Muscat", "Muscat (GMT+04:00)": "Asia/Muscat", "Baku (GMT+04:00)": "Asia/Baku", From 699731d351b83f6c4308d39c97cb9c5ef9144657 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 12 Sep 2025 17:46:27 +0530 Subject: [PATCH 02/18] fix: `display_id` to `id` mapping not handled (#12426) The frontend filtering didn't handle the `id` to `display_id` mapping of conversations. This PR fixes it --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../dashboard/components-next/filter/ConditionRow.vue | 8 +++++++- .../dashboard/components-next/filter/provider.js | 6 +++--- .../dashboard/components-next/input/Input.vue | 9 +++++++-- .../modules/conversations/helpers/filterHelpers.js | 4 +++- .../conversations/helpers/specs/filterHelpers.spec.js | 10 +++++----- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/javascript/dashboard/components-next/filter/ConditionRow.vue b/app/javascript/dashboard/components-next/filter/ConditionRow.vue index 1d53a3af2..b9b2d780c 100644 --- a/app/javascript/dashboard/components-next/filter/ConditionRow.vue +++ b/app/javascript/dashboard/components-next/filter/ConditionRow.vue @@ -103,6 +103,12 @@ const validationError = computed(() => { ); }); +const inputFieldType = computed(() => { + if (inputType.value === 'date') return 'date'; + if (inputType.value === 'number') return 'number'; + return 'text'; +}); + const resetModelOnAttributeKeyChange = newAttributeKey => { /** * Resets the filter values and operator when the attribute key changes. This ensures that @@ -182,7 +188,7 @@ defineExpose({ validate }); diff --git a/app/javascript/dashboard/components-next/filter/provider.js b/app/javascript/dashboard/components-next/filter/provider.js index f6d078d76..7fd600da9 100644 --- a/app/javascript/dashboard/components-next/filter/provider.js +++ b/app/javascript/dashboard/components-next/filter/provider.js @@ -164,8 +164,8 @@ export function useConversationFilterContext() { value: CONVERSATION_ATTRIBUTES.DISPLAY_ID, attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), - inputType: 'plainText', - datatype: 'number', + inputType: 'number', + dataType: 'number', filterOperators: containmentOperators.value, attributeModel: 'standard', }, @@ -179,7 +179,7 @@ export function useConversationFilterContext() { id: campaign.id, name: campaign.title, })), - datatype: 'number', + dataType: 'number', filterOperators: presenceOperators.value, attributeModel: 'standard', }, diff --git a/app/javascript/dashboard/components-next/input/Input.vue b/app/javascript/dashboard/components-next/input/Input.vue index f20c43449..f4bf5a94f 100644 --- a/app/javascript/dashboard/components-next/input/Input.vue +++ b/app/javascript/dashboard/components-next/input/Input.vue @@ -55,7 +55,12 @@ const inputOutlineClass = computed(() => { }); const handleInput = event => { - emit('update:modelValue', event.target.value); + let value = event.target.value; + // Convert to number if type is number and value is not empty + if (props.type === 'number' && value !== '') { + value = Number(value); + } + emit('update:modelValue', value); emit('input', event); }; @@ -114,7 +119,7 @@ onMounted(() => { ? max : undefined " - class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out" + class="block w-full reset-base text-sm h-10 !px-3 !py-2.5 !mb-0 outline outline-1 border-none border-0 outline-offset-[-1px] rounded-lg bg-n-alpha-black2 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 disabled:cursor-not-allowed disabled:opacity-50 text-n-slate-12 transition-all duration-500 ease-in-out [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" @input="handleInput" @focus="handleFocus" @blur="handleBlur" diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js index 3f1c32059..3d627e3ef 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js @@ -64,11 +64,13 @@ const getValueFromConversation = (conversation, attributeKey) => { switch (attributeKey) { case 'status': case 'priority': - case 'display_id': case 'labels': case 'created_at': case 'last_activity_at': return conversation[attributeKey]; + case 'display_id': + // Frontend uses 'id' but backend expects 'display_id' + return conversation.display_id || conversation.id; case 'assignee_id': return conversation.meta?.assignee?.id; case 'inbox_id': diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js index b36128819..096481c69 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js @@ -247,7 +247,7 @@ describe('filterHelpers', () => { // Text search tests - display_id it('should match conversation with equal_to operator for display_id', () => { - const conversation = { display_id: '12345' }; + const conversation = { id: '12345' }; const filters = [ { attribute_key: 'display_id', @@ -260,7 +260,7 @@ describe('filterHelpers', () => { }); it('should match conversation with contains operator for display_id', () => { - const conversation = { display_id: '12345' }; + const conversation = { id: '12345' }; const filters = [ { attribute_key: 'display_id', @@ -273,7 +273,7 @@ describe('filterHelpers', () => { }); it('should not match conversation with does_not_contain operator for display_id', () => { - const conversation = { display_id: '12345' }; + const conversation = { id: '12345' }; const filters = [ { attribute_key: 'display_id', @@ -286,7 +286,7 @@ describe('filterHelpers', () => { }); it('should match conversation with does_not_contain operator when value is not present', () => { - const conversation = { display_id: '12345' }; + const conversation = { id: '12345' }; const filters = [ { attribute_key: 'display_id', @@ -989,7 +989,7 @@ describe('filterHelpers', () => { it('should handle empty string values in conversation', () => { const conversation = { - display_id: '', + id: '', }; const filters = [ { From ca579bd62a3831c9813d3dd8ccc9840691cc5b68 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:42:55 +0530 Subject: [PATCH 03/18] feat: Agent capacity policy Create/Edit pages (#12424) # Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-5573/feat-createedit-agent-capacity-policy-page ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/8de9e3c5d8824cd998d242636540dd18?sid=1314536f-c8d6-41fd-8139-cae9bf94f942 ### Screenshots **Light mode** image image **Dark mode** image image ## 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 - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth --- .../components/AddDataDropdown.vue | 27 +- .../AssignmentPolicy/components/BaseInfo.vue | 3 +- .../AssignmentPolicy/components/DataTable.vue | 20 +- .../components/ExclusionRules.vue | 149 ++++++++ .../components/InboxCapacityLimits.vue | 163 +++++++++ .../story/AddDataDropdown.story.vue | 38 +- .../components/story/DataTable.story.vue | 27 +- .../components/story/ExclusionRules.story.vue | 67 ++++ .../story/InboxCapacityLimits.story.vue | 108 ++++++ .../Contacts/ContactLabels/ContactLabels.vue | 4 +- .../components-next/Label/LabelItem.vue | 3 +- .../dashboard/i18n/locale/en/settings.json | 86 +++++ .../assignmentPolicy.routes.js | 20 ++ .../pages/AgentCapacityCreatePage.vue | 87 +++++ .../pages/AgentCapacityEditPage.vue | 179 ++++++++++ .../components/AgentCapacityPolicyForm.vue | 214 ++++++++++++ .../store/modules/agentCapacityPolicies.js | 156 ++++++++- .../agentCapacityPolicies/actions.spec.js | 191 ++++++++++- .../specs/agentCapacityPolicies/fixtures.js | 126 ++++++- .../agentCapacityPolicies/mutations.spec.js | 324 +++++++++++++++++- .../dashboard/store/mutation-types.js | 6 + 21 files changed, 1965 insertions(+), 33 deletions(-) create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/ExclusionRules.story.vue create mode 100644 app/javascript/dashboard/components-next/AssignmentPolicy/components/story/InboxCapacityLimits.story.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityCreatePage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityEditPage.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue index c664d8929..7c98db791 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue @@ -4,6 +4,7 @@ import { useToggle } from '@vueuse/core'; import { vOnClickOutside } from '@vueuse/components'; import { picoSearch } from '@scmmishra/pico-search'; +import Avatar from 'next/avatar/Avatar.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import Input from 'dashboard/components-next/input/Input.vue'; @@ -36,8 +37,8 @@ const filteredItems = computed(() => { return picoSearch(props.items, query, ['name']); }); -const handleAdd = inbox => { - emit('add', inbox); +const handleAdd = item => { + emit('add', item); togglePopover(false); }; @@ -82,21 +83,35 @@ const handleClickOutside = () => {
+ +
- {{ item.name }} + {{ item.name || item.title }} { emit('validationChange', { isValid: isValid.value, + section: 'baseInfo', }); }, { immediate: true } @@ -108,7 +109,7 @@ watch(
-
+
+import Avatar from 'next/avatar/Avatar.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import Spinner from 'dashboard/components-next/spinner/Spinner.vue'; @@ -32,12 +33,14 @@ const handleDelete = itemId => { >
- - {{ emptyStateMessage }} - + + {{ emptyStateMessage }} + +
{ :icon="item.icon" class="size-4 text-n-slate-12 flex-shrink-0" /> - + {{ item.name }} diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue new file mode 100644 index 000000000..5412675bd --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/ExclusionRules.vue @@ -0,0 +1,149 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue new file mode 100644 index 000000000..6a799005e --- /dev/null +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/InboxCapacityLimits.vue @@ -0,0 +1,163 @@ + + + diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue index 2ac4d8854..e69aa798f 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/AddDataDropdown.story.vue @@ -34,6 +34,29 @@ const mockInboxes = [ }, ]; +const mockTags = [ + { + id: 1, + name: 'urgent', + color: '#ff4757', + }, + { + id: 2, + name: 'bug', + color: '#ff6b6b', + }, + { + id: 3, + name: 'feature-request', + color: '#4834d4', + }, + { + id: 4, + name: 'documentation', + color: '#26de81', + }, +]; + const handleAdd = item => { console.log('Add item:', item); }; @@ -42,9 +65,9 @@ const handleAdd = item => { diff --git a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue index 912b1fdfc..a81a29976 100644 --- a/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue +++ b/app/javascript/dashboard/components-next/AssignmentPolicy/components/story/DataTable.story.vue @@ -22,6 +22,21 @@ const mockItems = [ }, ]; +const mockAgentList = [ + { + id: 1, + name: 'John Doe', + email: 'john.doe@example.com', + avatarUrl: 'https://i.pravatar.cc/150?img=1', + }, + { + id: 2, + name: 'Jane Smith', + email: 'jane.smith@example.com', + avatarUrl: 'https://i.pravatar.cc/150?img=2', + }, +]; + const handleDelete = itemId => { console.log('Delete item:', itemId); }; @@ -30,7 +45,7 @@ const handleDelete = itemId => {