Compare commits

...

3532 Commits

Author SHA1 Message Date
RitvikSardana
794c7b0895 Merge pull request #3841 from frappe/ci/pilot-prebuilt-assets
ci: Publish prebuilt assets for Pilot
2026-10-06 17:34:15 +05:30
RitvikSardana
85d7d17f8d Merge pull request #3840 from RitvikSardana/fix/filter-paste-in-values
fix(filter): select every value pasted into the in filter
2026-10-05 14:00:23 +05:30
Ritvik Sardana
e08dd865b8 fix(filter): keep typed text when closing the filter, drop it only on clear all 2026-10-05 12:27:14 +05:30
Ritvik Sardana
a2d1c45cb4 fix(filter): drop typed text when closing the filter or switching operators 2026-10-05 12:20:46 +05:30
Ritvik Sardana
a0eb9ce5bf fix(filter): apply typed values before leaving the filter 2026-10-05 12:14:05 +05:30
Tanmoy Sarkar
dbc5d90bda ci: Publish prebuilt assets for Pilot
Build the app's assets in CI for each push and publish them on the release assets-<branch>, so Pilot benches download them instead of building on the server.
2026-10-05 12:12:56 +05:30
Ritvik Sardana
e2682b9a45 test(filter): cover pasting lists and every field type 2026-10-05 12:10:54 +05:30
Ritvik Sardana
e21dedd9ba fix(filter): keep the filter open when switching operators 2026-10-05 12:10:52 +05:30
Ritvik Sardana
9ade3ce18e fix(filter): clear the search after a paste and drop empty in lists 2026-10-05 11:45:02 +05:30
RitvikSardana
e938163cca Merge pull request #3839 from RitvikSardana/fix/customer-portal-label-translations
fix: show translated field labels in the customer portal
2026-10-05 11:44:02 +05:30
RitvikSardana
ab18470596 Merge pull request #3836 from frappe/pot_develop_2026-10-04
chore: update POT file
2026-10-05 11:29:50 +05:30
Ritvik Sardana
cd542d452a fix(filter): close the filter when clearing all filters 2026-10-05 11:24:56 +05:30
Ritvik Sardana
e9b92254a7 fix(filter): show the first three values and a count for long lists 2026-10-05 11:24:55 +05:30
Ritvik Sardana
7a2d47fbee fix(filter): select every value pasted into the in filter 2026-10-05 11:24:52 +05:30
Ritvik Sardana
3a575e21e5 fix: show translated field labels in the customer portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:54:32 +05:30
Sydney Gomes
fb19d721c8 Merge pull request #3815 from aerodeval/fix/user-email-permlevel-reset
fix(profile): save agent email links through the helpdesk api
2026-10-05 02:02:07 +05:30
Sydney
ee9c0ff8ba Merge remote-tracking branch 'upstream/develop' into fix/user-email-permlevel-reset
# Conflicts:
#	helpdesk/test_utils.py
2026-10-05 01:46:36 +05:30
Sydney
88dd98da71 test(profile): move email link helpers to test_utils 2026-10-05 01:36:37 +05:30
Sydney
57cbd6e121 fix(profile): keep unsaved email links and skip needless user save 2026-10-05 01:18:45 +05:30
Sydney Gomes
0ec451d274 Merge pull request #3823 from kaulith/fix/dashboard-sla-trend-percentage
fix(dashboard): ticket trend sla line shows a count, not a percentage
2026-10-04 22:43:42 +05:30
frappe-pr-bot
2658d10d3a chore: update POT file 2026-10-04 15:27:12 +00:00
RitvikSardana
be13e430dc Merge pull request #3820 from RitvikSardana/fix/pinned-jump-shift
fix: small layout fixes on the ticket page
2026-10-02 16:20:47 +05:30
Ritvik Sardana
3f89ecb03b fix: fade the timeline top instead of cutting it when there are no pins 2026-10-02 02:22:46 +05:30
Kaushal Shriwas
cac55d63d1 test(dashboard): format the sla trend test 2026-10-01 18:32:31 +05:30
Kaushal Shriwas
9643e178dc test(dashboard): freeze the clock in the sla trend test 2026-10-01 18:29:35 +05:30
Kaushal Shriwas
cb63d5b19f test(dashboard): sla line on ticket trend plots a percentage 2026-10-01 18:18:54 +05:30
Kaushal Shriwas
b674f9a068 fix(dashboard): ticket trend sla line shows a count, not a percentage 2026-10-01 18:18:51 +05:30
Ritvik Sardana
343dbb7490 test(e2e): stop the cc reply test from losing keystrokes to the cc picker 2026-10-01 15:07:12 +05:30
Ritvik Sardana
e95a63befd fix: match the recent tickets spacing to the other sidebar sections 2026-10-01 13:45:48 +05:30
Ritvik Sardana
63098d52cb fix(feedback): align show more with the comment text 2026-10-01 13:45:47 +05:30
Ritvik Sardana
a3546ecfa5 fix(pinned): stop the timeline from shifting after jumping to a pinned comment 2026-10-01 13:14:29 +05:30
Sydney Gomes
516700c45f Merge pull request #3819 from aerodeval/refactor/banners-composable
refactor(banners)!: serve sidebar banners from one API
2026-10-01 02:04:57 +05:30
Sydney
661e636f40 refactor(ticket): rename apply_portal_insert_rules to apply_ticket_insert_rules 2026-10-01 01:03:30 +05:30
Sydney
9615e9d465 chore: skip semgrep guest check on get_config 2026-10-01 01:01:30 +05:30
Sydney
c49341c740 fix(banners): reword the custom agent roles hint 2026-10-01 01:01:29 +05:30
Sydney
41b4b7d612 test(banners): move helpers to test_utils 2026-10-01 01:01:28 +05:30
Sydney
41ff44ecfc fix(banners): clear the promotion choice when the dialog closes 2026-10-01 01:01:27 +05:30
Sydney
aeefe064a4 fix(banners): treat a missing notice flag as off 2026-09-30 22:58:10 +05:30
Sydney
61e60518bd fix(banners): simplify field permission notice text 2026-09-30 22:42:30 +05:30
Sydney
66c8d6208c refactor(banners)!: serve sidebar banners from one API 2026-09-30 22:22:20 +05:30
Sydney Gomes
21fa9cdf6a Merge pull request #3544 from aerodeval/fix/ticket-field-level-perms
fix!: field-level permissions for agent-only ticket fields
2026-09-30 16:37:45 +05:30
Sydney
8642c3a699 test(e2e): cover the field permission notice and template field visibility 2026-09-30 15:54:57 +05:30
Sydney
e0e041dea7 fix(perms): hide the field permission notice in every open tab once dismissed 2026-09-30 15:54:56 +05:30
Sydney Gomes
c1d0010c4a Merge pull request #3814 from aerodeval/fix/sent-email-author-name
fix(ticket): name a reply sent from a shared email after the agent
2026-09-30 15:27:03 +05:30
Sydney
85d7d2a87f Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms
# Conflicts:
#	e2e/tests/customer/portal.spec.ts
2026-09-30 15:21:48 +05:30
RitvikSardana
7f54082cfa Merge pull request #3817 from RitvikSardana/refactor/e2e-page-layout
refactor(e2e): group specs by page and shard UI tests
2026-09-30 14:59:14 +05:30
Sydney
d6ce0e15bb fix(perms): read the field permission notice flag outside the guest config 2026-09-30 14:56:25 +05:30
Sydney
e7dd63887e feat(perms): tell managers once that ticket fields follow permission levels 2026-09-30 14:26:34 +05:30
Ritvik Sardana
4044a1a309 ci(e2e): shard Playwright tests across two runners 2026-09-30 14:24:25 +05:30
Ritvik Sardana
0a538f3274 docs(e2e): describe the page based layout 2026-09-30 14:23:55 +05:30
Ritvik Sardana
c1beb6d14c refactor(e2e): split portal spec by page 2026-09-30 14:23:37 +05:30
Ritvik Sardana
b6775d0543 refactor(e2e): group specs by page instead of role 2026-09-30 14:23:06 +05:30
RitvikSardana
611fbfc558 Merge pull request #3813 from RitvikSardana/feat/pinned-comments
feat(ticket): pinned comments
2026-09-30 14:14:24 +05:30
Ritvik Sardana
ba0e0a7b8f fix(pinned): keep the scroll helper doc comment to two lines 2026-09-30 13:30:57 +05:30
Ritvik Sardana
0b24393480 fix: pick the typed contact when enter is pressed before search results load 2026-09-30 13:26:38 +05:30
Sydney
40cd3bea08 fix(perms): grant level 7 to every role that reads the ticket 2026-09-30 13:13:04 +05:30
Ritvik Sardana
f1b3f9ac25 docs: remove the why-only comment rule from AGENTS.md 2026-09-30 12:51:50 +05:30
Ritvik Sardana
081f7c6241 fix(template): disable renaming ticket templates on the doctype 2026-09-30 04:07:37 +05:30
Ritvik Sardana
ab8d0e85d2 fix(perms): let customers close a ticket but not resolve it 2026-09-30 03:47:22 +05:30
Ritvik Sardana
33ced6aba0 refactor(ticket): move customer edit rules into CustomerEditController 2026-09-30 03:47:09 +05:30
Ritvik Sardana
27dec9adf8 refactor(perms): move write rules to HDTicket and name TicketFields for what it does 2026-09-30 03:26:42 +05:30
Sydney
b372f0a7f8 fix(profile): save agent email links through the helpdesk api
User Emails is permlevel 1, so agents never got it from frappe.client.get
and their saves were silently reverted. The settings screen now reads the
list from get_current_user_email_info and saves it through a new agent-only
set_user_email_accounts endpoint, which only touches outgoing accounts.
2026-09-30 03:06:26 +05:30
Sydney
b80f3b9995 fix(ticket): name a reply sent from a shared email after the agent 2026-09-30 02:31:36 +05:30
Sydney
bfd5bd45f1 fix(perms): return readable fields from new and honour the opened template 2026-09-30 01:42:28 +05:30
Sydney
22a581ff5f chore(ticket): remove unused attachment and content_type fields 2026-09-30 00:50:11 +05:30
Sydney
b9c9d9d187 Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms
# Conflicts:
#	helpdesk/api/doc.py
2026-09-30 00:39:32 +05:30
Ritvik Sardana
3304889e39 fix(pinned): keep the pinned bar doc comment to two lines 2026-09-30 00:06:46 +05:30
RitvikSardana
13a813c703 Merge pull request #3812 from RitvikSardana/test/api-coverage
test(api): cover untested API modules and fix what they found
2026-09-30 00:02:46 +05:30
Ritvik Sardana
285b82d76e test(e2e): close invite suggestions with Escape instead of an outside click 2026-09-29 23:51:12 +05:30
Ritvik Sardana
5b32269e81 fix(pinned): remove the comment that restates the bar transition 2026-09-29 23:30:00 +05:30
Ritvik Sardana
11a9f56d5a refactor(article): rename only_published to filter_public_articles 2026-09-29 23:23:04 +05:30
Ritvik Sardana
4257a0b809 refactor(email): move service name map above its caller 2026-09-29 23:23:04 +05:30
Ritvik Sardana
8572b0b5e4 fix(pinned): make the pinned badge gray 2026-09-29 23:11:02 +05:30
Ritvik Sardana
d76264b05d fix(pinned): float the pinned bar over the activity 2026-09-29 23:11:00 +05:30
Ritvik Sardana
4a2f1d19a6 fix: dont carry pinned comments from the source ticket while merging 2026-09-29 22:45:29 +05:30
Ritvik Sardana
6fd24be817 refactor(ticket): drop the activity header and move call actions to the tab row 2026-09-29 22:19:33 +05:30
Sydney
f9b9248d45 fix(perms): keep the email sender when a system manager pulls emails 2026-09-29 21:56:47 +05:30
Ritvik Sardana
7e94cef894 fix(pinned): show pinned comment with badge instead of just icon 2026-09-29 21:20:56 +05:30
Ritvik Sardana
d3b2f61b09 refactor(ticket): read pinned comments from the comment extras 2026-09-29 21:05:03 +05:30
Ritvik Sardana
0aa6e01571 fix(ticket): pass the class through to the activity heading 2026-09-29 21:05:01 +05:30
Ritvik Sardana
f72ebdecd6 refactor(ticket): keep the pin marker as a plain indicator 2026-09-29 21:04:59 +05:30
Sydney
8b9c901f8c fix(perms): address review feedback 2026-09-29 19:42:12 +05:30
Sydney
bc77387f58 fix(template): match template fieldnames case-sensitively 2026-09-29 15:38:06 +05:30
Sydney
b3885c16e3 refactor(template): rename Customer can edit to Editable after creation 2026-09-29 14:33:21 +05:30
Ritvik Sardana
61838ed0dc test(e2e): cover unpinning from the marker and the five pin limit 2026-09-29 14:13:54 +05:30
Ritvik Sardana
c3c28d6b3b fix(ticket): stretch the pinned bar track only past two pins 2026-09-29 14:13:52 +05:30
Ritvik Sardana
bedbdba511 fix(icons): cut the unpin slash out of the pin instead of painting over it 2026-09-29 14:13:51 +05:30
Ritvik Sardana
2e62e31e0c feat(ticket): unpin a comment from its pin marker 2026-09-29 14:13:49 +05:30
Sydney
d735d1d4b3 test: restore every template column after permission tests 2026-09-29 14:12:19 +05:30
Sydney
744f7133d9 fix(template): widen the Customer can edit column 2026-09-29 14:06:02 +05:30
Ritvik Sardana
7617ab7bfd test(e2e): stop ticket list tests racing the default view autosave 2026-09-29 13:59:59 +05:30
Ritvik Sardana
7127d2aca5 test(e2e): close invite suggestions before clicking Invite 2026-09-29 13:59:59 +05:30
Ritvik Sardana
070e01720d test(email): reject bad credentials when incoming is off 2026-09-29 13:57:16 +05:30
Ritvik Sardana
ab3caff2d0 fix(email): check credentials for every service with an incoming server 2026-09-29 13:57:16 +05:30
Ritvik Sardana
c8c140a9d6 docs(agents): allow get_all for child tables and guard it elsewhere 2026-09-29 13:57:16 +05:30
Ritvik Sardana
634784840e test: move api test helpers into test_utils 2026-09-29 13:57:16 +05:30
Ritvik Sardana
923ee81055 docs(tests): keep all test helpers in test_utils 2026-09-29 13:57:16 +05:30
Ritvik Sardana
24e2773198 fix(email): show provider icons for Frappe's service names 2026-09-29 13:57:16 +05:30
Ritvik Sardana
e087af08b2 fix(article): check published articles with get_list 2026-09-29 13:57:16 +05:30
Ritvik Sardana
b3158671ee test(settings): cover email accounts and email notifications 2026-09-29 13:57:16 +05:30
Ritvik Sardana
a0e6ece014 test(portal-notice): cover dismissing and restoring ticket access 2026-09-29 13:57:16 +05:30
Ritvik Sardana
8a63cf6f63 test(article): cover article stats and search 2026-09-29 13:57:16 +05:30
Ritvik Sardana
f5e0598738 test(doc): cover list data, filters and filterable fields 2026-09-29 13:57:16 +05:30
Ritvik Sardana
b867e9dd82 fix(email): save Outlook, Yahoo and Yandex under Frappe's service names 2026-09-29 13:57:16 +05:30
Ritvik Sardana
9d0aa81004 fix(email-notifications): validate the feedback status before saving 2026-09-29 13:57:16 +05:30
Ritvik Sardana
ea2dd0819a fix(email): create outgoing-only email accounts without an incoming check 2026-09-29 13:57:16 +05:30
Ritvik Sardana
b064ae303f fix(portal-notice): dismiss the notice when ticket access is restored 2026-09-29 13:57:16 +05:30
Ritvik Sardana
3f248973ad fix(article): hide stats of unpublished articles from non-agents 2026-09-29 13:57:16 +05:30
Ritvik Sardana
11f7531346 fix(article): keep unpublished articles out of search results 2026-09-29 13:57:16 +05:30
Ritvik Sardana
029f319454 fix(doc): check permissions before the cached filterable fields 2026-09-29 13:57:16 +05:30
Ritvik Sardana
554a76dd97 fix(doc): stop customers removing ticket assignments 2026-09-29 13:57:16 +05:30
Ritvik Sardana
0d401ccf69 fix(doc): apply @me and assigned on to a view's default filters 2026-09-29 13:57:16 +05:30
Ritvik Sardana
b5bdc783bb fix(doc): replace every @me in list and nested filters 2026-09-29 13:57:16 +05:30
Ritvik Sardana
c2dec3ff19 fix(doc): compare the "assigned on" filter by whole days 2026-09-29 13:57:16 +05:30
Ritvik Sardana
20a7422cca test(dashboard): cover the tag charts 2026-09-29 13:57:16 +05:30
Ritvik Sardana
810c47681d test(assignment-rule): cover the rules list and who may read it 2026-09-29 13:57:16 +05:30
Ritvik Sardana
cb2e008c75 fix(assignment-rule): list only ticket assignment rules 2026-09-29 13:57:16 +05:30
Ritvik Sardana
caf769a1cc test: move api tests to helpdesk/tests/api 2026-09-29 13:57:16 +05:30
Ritvik Sardana
20e12d68da docs(tests): add backend test conventions 2026-09-29 13:57:16 +05:30
Sydney
3fbd0587ac feat(template): let a template row open a field to customer edits 2026-09-29 13:41:52 +05:30
Ritvik Sardana
bfd00a53a9 fix(ticket): group the pinned bar icon with its text 2026-09-29 13:41:35 +05:30
Ritvik Sardana
6c1b0c380c refactor(comments): drop the row lock around the pin limit check 2026-09-29 13:27:09 +05:30
Ritvik Sardana
336ad9383e feat(ticket): disable pinning once a ticket has five pins 2026-09-29 13:27:08 +05:30
Ritvik Sardana
6ce7553230 test(e2e): cover pinning comments from the timeline 2026-09-29 13:11:06 +05:30
Ritvik Sardana
a7514171da feat(ticket): pin comments to a bar above the timeline 2026-09-29 13:11:04 +05:30
Ritvik Sardana
a89e4c67a0 feat(icons): redraw the pin and unpin icons from Figma 2026-09-29 13:11:03 +05:30
Ritvik Sardana
53f95b78c6 feat(comments): cap pinned comments at five per ticket 2026-09-29 13:11:01 +05:30
Sydney
df721618f3 fix(perms): say permission, not timing, when a customer cannot write a field 2026-09-29 13:07:10 +05:30
Sydney
870be0ebb8 fix(perms): refuse a customer's edit instead of silently reverting it 2026-09-29 12:38:17 +05:30
RitvikSardana
483f718012 Merge pull request #3811 from RitvikSardana/test/e2e-customers-contacts
test(e2e): cover customers and contacts, fix manager edits on user contacts
2026-09-29 11:58:53 +05:30
Sydney
9f128ba5b7 fix(template): link the helpdesk permission levels guide 2026-09-29 11:28:29 +05:30
Ritvik Sardana
6ed1c9ac6b fix(contact): keep a user's photo and timezone when an edit omits them 2026-09-29 11:17:44 +05:30
Ritvik Sardana
7b5d30bab4 test(e2e): cover customers and contacts for managers and agents 2026-09-29 11:01:42 +05:30
Ritvik Sardana
1bf173ed19 fix(contact): let agent managers edit a contact that has a user 2026-09-29 11:01:41 +05:30
RitvikSardana
488992b1b2 Merge pull request #3810 from RitvikSardana/feat/playwright-e2e
test(e2e): Playwright suite for the core user journeys
2026-09-29 03:13:01 +05:30
Ritvik Sardana
cfeabb11b3 docs(e2e): document how to write a spec 2026-09-29 02:52:09 +05:30
Ritvik Sardana
73e030549e test(e2e): add a ticket fixture for tests that need any one ticket 2026-09-29 02:52:09 +05:30
Ritvik Sardana
1810916414 refactor(e2e): group specs into area folders under tests/ 2026-09-29 02:45:49 +05:30
Ritvik Sardana
d77599f4a0 test(e2e): cover every non-integration settings page 2026-09-29 02:40:56 +05:30
Ritvik Sardana
eb858b00f6 fix(profile): give the remove email button an accessible name 2026-09-29 02:40:56 +05:30
Ritvik Sardana
a5dcd7ee42 fix(holiday): open the calendar on the list's first year 2026-09-29 02:40:55 +05:30
Ritvik Sardana
c1f0016bcf fix(holiday): count recurring weeks as the nth weekday of the month 2026-09-29 02:40:54 +05:30
Ritvik Sardana
dd311d8d29 fix(editor): let a mouse click pick a field suggestion 2026-09-29 02:25:12 +05:30
Ritvik Sardana
e7cc710016 fix(settings): revert only the toggles a failed save sent 2026-09-29 01:49:23 +05:30
Sydney
3cc8c89ddb Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms
# Conflicts:
#	helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.json
#	helpdesk/helpdesk/doctype/hd_ticket/test_hd_ticket.py
2026-09-29 01:42:49 +05:30
Ritvik Sardana
1cc683cd14 fix(settings): keep unsaved edits on a failed toggle and hide other teams' assignees 2026-09-29 01:39:30 +05:30
Ritvik Sardana
8d8f9e57d4 fix(merge): close the merge dialog when a lost response still merged 2026-09-29 01:27:31 +05:30
Ritvik Sardana
c9e0becfc8 fix(merge): reject merging a ticket that is already merged 2026-09-29 01:27:31 +05:30
Ritvik Sardana
0915d78f2e test: limit bulk_reply cleanup to the files it attached 2026-09-29 01:27:31 +05:30
Ritvik Sardana
0081152d42 fix(merge): move emails and every attachment when an agent merges 2026-09-29 01:27:31 +05:30
Ritvik Sardana
085eedc314 test(e2e): cover saved views end to end 2026-09-29 00:42:06 +05:30
Ritvik Sardana
bf9023d2ce test(e2e): run the browser in UTC so timezone bugs surface locally 2026-09-29 00:31:55 +05:30
Ritvik Sardana
ccdd68d4ff ci(e2e): serve the site on localhost and build frappe assets 2026-09-29 00:31:55 +05:30
Ritvik Sardana
25b354fbbe fix(list): show datetime columns in the viewer's timezone 2026-09-29 00:31:54 +05:30
Ritvik Sardana
6a6fe28645 test(e2e): cover the core agent, admin and customer journeys 2026-09-29 00:13:03 +05:30
Ritvik Sardana
4d2424d326 fix(assignment): offer only team members when assignment is restricted to the team 2026-09-29 00:12:26 +05:30
Ritvik Sardana
09c9d96313 fix(settings): save general toggles on their own and one at a time 2026-09-29 00:12:25 +05:30
Ritvik Sardana
c61c30355f fix(onboarding): complete the invite agents step on invite 2026-09-29 00:12:25 +05:30
Ritvik Sardana
f5fa276383 fix(editor): treat a body with only an image as content 2026-09-29 00:12:25 +05:30
Ritvik Sardana
9ebf9f577d fix(form-script): read createToast duration in seconds 2026-09-29 00:12:17 +05:30
Ritvik Sardana
faadfa79a9 test(e2e): document local runs and keep persona passwords stable 2026-09-28 23:25:15 +05:30
Ritvik Sardana
75e698ea9b test(e2e): add Playwright setup, personas and CI workflow 2026-09-28 23:23:34 +05:30
Ritvik Sardana
8650d04715 fix(router): send customers from an agent ticket link to their ticket 2026-09-28 23:23:34 +05:30
RitvikSardana
f2641606a6 Merge pull request #3803 from frappe/pot_develop_2026-09-27
chore: update POT file
2026-09-28 20:32:45 +05:30
RitvikSardana
b273218637 Merge pull request #3807 from RitvikSardana/fix/sla-paused-flips-to-failed
fix(sla): keep a ticket paused past its deadlines while on hold
2026-09-28 20:32:30 +05:30
Ritvik Sardana
4b67db0dd8 test(sla): cover a resolution breach from before the hold 2026-09-28 20:18:09 +05:30
Ritvik Sardana
56cadd897a docs(agents): cap comments at two lines 2026-09-28 19:28:38 +05:30
Ritvik Sardana
9ca4b1098b fix(sla): keep a ticket paused past its deadlines while on hold 2026-09-28 19:28:37 +05:30
RitvikSardana
208e32eb00 Merge pull request #3806 from RitvikSardana/fix/agent-ticket-perms
fix(perms): agents & tickets
2026-09-28 19:19:54 +05:30
RitvikSardana
25ee45e4c3 Merge pull request #3805 from RitvikSardana/fix/timeline-optimistic-updates
fix(ticket): pending row avatar and comment delete toast
2026-09-28 19:13:33 +05:30
Ritvik Sardana
6e7d5dabb9 fix(perms): agents & tickets 2026-09-28 18:14:59 +05:30
Ritvik Sardana
63fffa9843 fix(ticket): take the pending row's name and avatar from the session 2026-09-28 18:10:33 +05:30
Sydney Gomes
d05e5b5d4f Merge pull request #3800 from aerodeval/fix/docker-image-missing-apps
fix(ci): include helpdesk and telephony in docker image
2026-09-28 18:08:43 +05:30
Sydney
f53fecc699 fix(ci): match hrms job permissions 2026-09-28 17:43:40 +05:30
Sydney
2c189b3c93 fix(ci): build with frappe_docker main 2026-09-28 17:39:28 +05:30
frappe-pr-bot
c304643b86 chore: update POT file 2026-09-27 15:13:16 +00:00
Sydney
e0f2398a31 fix(ci): include helpdesk and telephony in docker image
frappe_docker's layered Containerfile dropped the APPS_JSON_BASE64
build-arg in ae275df1 and now reads apps.json from a BuildKit secret.
The ignored build-arg made every image since then ship with frappe
only, while the build still passed.

Pin frappe_docker to a release, check the image contains helpdesk and
telephony before pushing, and drop the arch matrix, which built amd64
twice and raced both images onto the same tags.

Fixes #3227
2026-09-26 03:12:08 +05:30
Sydney Gomes
bf92d47c2c Merge pull request #3776 from aerodeval/fix/sla-timezone-and-hold-time
fix(sla): correct SLA deadline timezone and hold-time deduction
2026-09-26 02:31:18 +05:30
Sydney Gomes
ae8c540013 Merge pull request #3792 from aerodeval/fix/skip-persona-landing
fix(onboarding): open the welcome ticket after skipping the questionnaire
2026-09-26 02:11:21 +05:30
Sydney Gomes
903a535dd6 Merge branch 'develop' into fix/sla-timezone-and-hold-time 2026-09-26 02:02:46 +05:30
Sydney
57b5bfc30d fix(onboarding): open the welcome ticket after skipping the questionnaire 2026-09-26 02:00:56 +05:30
Sydney Gomes
417c2862e9 Merge pull request #3793 from aerodeval/chore/frappe-ui-rc1
chore: migrate frappe-ui to v1.0.0-rc.1
2026-09-26 01:09:35 +05:30
Sydney
0bb8070779 fix(sla): count resolved time as hold only on reopen 2026-09-25 01:31:49 +05:30
Sydney
1f0dc529ce fix: close editor suggestion menus on outside click 2026-09-24 15:32:50 +05:30
Sydney
2aa4644825 fix: refresh mobile timeline after an update 2026-09-24 14:25:09 +05:30
Sydney
41f77fba8c fix: draw form-script toast icons from the sprite only 2026-09-24 14:21:07 +05:30
Sydney
6c62424ce4 refactor: style mobile tab panels directly 2026-09-24 14:05:33 +05:30
Sydney
7213e51e3c chore: shorten comments 2026-09-24 13:49:04 +05:30
Ritvik Sardana
96ece7bbd8 fix(ticket): track a comment delete in one toast 2026-09-24 13:41:02 +05:30
Ritvik Sardana
8ea319a68f fix(ticket): show your avatar on a comment or reply before it is saved 2026-09-24 13:41:01 +05:30
Sydney
97a6be8dc9 fix: load kb search results on the third character 2026-09-24 13:36:56 +05:30
Sydney
4fc61d4d44 fix: show field dependency borders in dark mode 2026-09-24 13:36:53 +05:30
Sydney
10c7c15894 fix: close mobile sidebar on outside tap 2026-09-24 13:36:45 +05:30
Sydney
1f914ebadb fix: widen mobile ticket pickers and scroll its tab bar 2026-09-24 13:36:42 +05:30
Sydney
0251b3d8f9 fix: stop editor toolbar overlapping send and discard 2026-09-24 13:36:40 +05:30
Sydney
3bd28aa269 fix: keep selected editor nodes from being clipped 2026-09-24 13:36:25 +05:30
RitvikSardana
9ff6a864a4 Merge pull request #3791 from RitvikSardana/fix/ticket-field-saved-value
fix(ticket): show a field value the fetched options do not carry
2026-09-24 11:08:42 +05:30
RitvikSardana
3e0512ba59 Merge pull request #3782 from frappe/pot_develop_2026-09-20
chore: update POT file
2026-09-24 11:06:56 +05:30
Sydney
d3f51690ed chore: declare unplugin-icons as a dev dependency 2026-09-24 02:52:51 +05:30
Sydney
b496da2e50 fix: render form-script toast icons from the sprite 2026-09-24 02:47:35 +05:30
Sydney
4768eb27a1 fix: show hover-only controls on touch devices 2026-09-24 02:47:33 +05:30
Sydney
b8c1e0ec38 fix: label icon-only tab buttons 2026-09-24 02:32:59 +05:30
Sydney
322697d1e0 fix: restore checkbox spacing in multi-selects 2026-09-24 02:32:57 +05:30
Sydney
775797732d fix: restore rating filter star size 2026-09-24 02:32:55 +05:30
Sydney
70f261c79e fix: keep the mobile sidebar drawer expanded 2026-09-24 02:23:18 +05:30
Sydney
c69fcda6b7 fix: convert tooltip and hovercard delays to milliseconds 2026-09-24 02:13:39 +05:30
Sydney
fc8ba633c4 fix: restore saved-reply field autocomplete and table of contents 2026-09-24 02:05:00 +05:30
Sydney
b2f3de5eff fix: pass mention items as label and value 2026-09-24 01:58:45 +05:30
Sydney
25e2981ef7 fix: use setOpen and close from popover slots 2026-09-24 01:50:07 +05:30
Sydney
e138ed2e26 chore: bump frappe-ui to 1.0.0-rc.1 2026-09-23 23:59:15 +05:30
Sydney
15d717ec6c chore: bump vite to 5 2026-09-23 23:41:38 +05:30
Sydney
2967bf400d fix(template): plain words for the Visible to messages 2026-09-23 15:31:06 +05:30
Sydney
a0ed458050 refactor(perms): let TicketFields filter every list
visible() replaces the by-hand subtractions in the list API and in the
class itself; details becomes layout, the list a layout editor will grow;
the template endpoint reads its row as a dict.
2026-09-23 14:35:57 +05:30
Sydney
1792df7570 fix(perms): strip unreadable fields from every ticket echo 2026-09-23 12:57:01 +05:30
Sydney
296dd33d4d refactor(perms): move field visibility into TicketFields
Also trims the portal ticket payload to what the page reads and picks
the new-ticket form script by audience.
2026-09-23 12:22:51 +05:30
Sydney
3f27c7b789 fix(portal): show falsy field values and fix the date tooltip
Test helpers moved to test_utils.
2026-09-22 16:06:10 +05:30
Sydney
06ecfb52b0 fix: disallow subject in template, tidy mobile rows 2026-09-22 15:23:46 +05:30
RitvikSardana
1436448b68 Merge pull request #3790 from RitvikSardana/fix/dialog-legacy-close-context
fix(form-script): keep the pre-v1 dialog and grouped action APIs working
2026-09-22 15:09:39 +05:30
Sydney Gomes
544c091179 Merge pull request #3785 from aerodeval/fix/portal-email-styles
fix: render email HTML correctly in the customer portal
2026-09-22 15:08:09 +05:30
Ritvik Sardana
7baf3f5ebb fix(ticket): show a field value the fetched options do not carry 2026-09-22 14:59:51 +05:30
RitvikSardana
fb3bc88edb Merge pull request #3789 from RitvikSardana/fix/ticket-list-view-polish
fix(ticket): assignee filter, unseen subject weight, loading indicator
2026-09-22 14:57:41 +05:30
Sydney
6a401a8233 refactor: improve code quality 2026-09-22 14:56:06 +05:30
Ritvik Sardana
97ecc8c871 fix(form-script): render a grouped action that still uses items 2026-09-22 14:38:39 +05:30
Ritvik Sardana
3eaf1b5d18 fix(dialog): keep the legacy close callback working in form scripts 2026-09-22 14:38:39 +05:30
Sydney
512c40a97b refactor(perms): read the template rows once, uncached
@redis_cache returned None whenever a concurrent request stored the key
between the wrapper's get_value and exists, so get_list_data raised a
TypeError. The query is one indexed child-table read.
2026-09-22 14:13:31 +05:30
Sydney
9e01f775d5 fix(portal): show the full date on hover for custom date fields
A Date or Datetime template field on the customer sidebar now carries the
full date in a tooltip, the way the SLA rows above it do. Custom rows also
get their `fieldname`, which the list key was already reading.
2026-09-22 13:08:46 +05:30
Ritvik Sardana
64f41c7173 fix(ticket): use the same loading indicator as the ticket page 2026-09-22 13:04:00 +05:30
Ritvik Sardana
e39a38ab66 fix(ticket): show an unseen ticket's subject in semibold 2026-09-22 13:04:00 +05:30
Ritvik Sardana
25aa1b3132 fix(ticket): filter by assignee when a ticket has only one 2026-09-22 13:03:49 +05:30
Sydney
575d57efd4 fix(perms): warn only about the template rows a save hides
The template's info dialog listed every Agents-only row on any save; it
now names just the rows the save turned Agents-only. Trims the comments
on the reply-reopen flags and marks `_comments` for removal from
`AGENT_WORKFLOW_FIELDS`.
2026-09-22 12:43:56 +05:30
Sydney
8c0ed88dbe Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms 2026-09-22 12:24:48 +05:30
Sydney
e08852d817 refactor(perms): return the agent form fields as one filtered list
`get_ticket_customizations` now returns every field the details tab may
show, already filtered: visible template rows plus a bare entry for each
visible core field without a row. The pages partition it by name instead
of reading a separate hidden list.
2026-09-22 12:21:28 +05:30
Sydney
9523b6b392 refactor(perms): make the template helper answer for the caller's audience
`get_template_hidden_fields(audience)` returns what the template hides from
that audience, so callers stop asking for the other side's rows.
2026-09-22 11:41:25 +05:30
Sydney
d1d5934cee refactor(perms): compute unreadable fields inline off the meta
`get_unreadable_fields` had one caller and a constant argument. The meta's
permlevel helpers make the same test `apply_fieldlevel_read_permissions` does.
2026-09-22 11:40:25 +05:30
Sydney
ed499bd9df refactor(perms): share the customer-visible template fields lookup
`customer_fillable_template_fields` and `get_visible_custom_fields` ran the
same query on the Default template's rows and applied the same filter.
One helper in `field_visibility.py`, which already owns who sees which
template row, replaces both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
2026-09-21 14:28:07 +05:30
Sydney
8f8e463a28 fix(email): strip email colours only in dark mode 2026-09-21 13:06:30 +05:30
Sydney
3b752b0fb5 fix(portal): keep inline styles when sanitizing email HTML 2026-09-21 13:06:28 +05:30
Sydney
428f0b3517 fix(perms): drop the per-user cache on get_filterable_fields
The result became role-dependent, so the cache went `user=True`. That
splits one entry into one per user, and `clear_cache()` only reaches the
calling user's key, so a Visible to change left everyone else stale for an
hour. The frontend fetches this once per session under frappe-ui's own
cache, so nothing is lost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
2026-09-21 03:05:11 +05:30
Sydney
fa419b9892 refactor(perms): filter hidden list fields once, at construction
`get_list_data` looked the hidden set up twice and filtered `rows` twice:
once before the query, and again after the standard fields loop put
`_assign` back. Compute it once, keep hidden standard fields out of that
loop, and drop the `fields` cleanup pass. The set is `hidden_fields` at
every site now; `hidden` named an adjective.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
2026-09-21 03:05:08 +05:30
Sydney
df89609b3e refactor(perms): prefix the visibility fetchers with get_
`hidden_ticket_fields`, `unreadable_fields` and `fields_visible_to` were the
only noun-named fetchers beside seventy-odd `get_` ones, including the
framework's `get_permitted_fields` they wrap. The docstring on
`get_hidden_ticket_fields` restated the new name, so it states the contract
instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
2026-09-21 03:05:04 +05:30
RitvikSardana
a8e858bb15 Merge pull request #3780 from RitvikSardana/fix/ticket-field-duplicate-set-value
fix(ticket): save a link field once when the value is cleared then picked
2026-09-21 00:27:11 +05:30
frappe-pr-bot
97e9aaf6db chore: update POT file 2026-09-20 14:17:53 +00:00
Ritvik Sardana
de56caaba2 fix(ticket): save a link field once when the value is cleared then picked 2026-09-19 19:42:24 +05:30
RitvikSardana
59c32bc4bb Merge pull request #3777 from RitvikSardana/fix/activity-timeline-live-updates
fix(ticket): add optimistic reply and comment in the timeline
2026-09-19 16:11:36 +05:30
Ritvik Sardana
2e04c70855 fix(ticket): delete a pasted image's file when the editor no longer shows it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJHtad4acgJRq843yyCAxq
2026-09-19 14:54:50 +05:30
Sydney
34d08bdea9 chore: drop the SLA check script and its runner 2026-09-18 19:12:16 +05:30
Sydney
500c848422 Merge remote-tracking branch 'upstream/develop' into fix/sla-timezone-and-hold-time
# Conflicts:
#	desk/src/utils.ts
2026-09-18 19:10:57 +05:30
Sydney
e686680a81 test(sla): check deadline comparisons across timezones
useSLA imports frappe-ui, whose index loads .vue files that tsx cannot,
so the check runs through Vite via a small runner.
2026-09-18 19:03:19 +05:30
Sydney
79b9cce118 test(perms): drop the set_value override test 2026-09-18 17:24:25 +05:30
Sydney
37b925f974 test(perms): cover the set_value reply, reset the user in tearDown
tearDown runs before every addCleanup, so the deletions no longer need a
wrapper that switches back to Administrator first.
2026-09-18 17:21:14 +05:30
RitvikSardana
3431b3a007 Merge pull request #3768 from frappe/pot_develop_2026-09-13
chore: update POT file
2026-09-18 15:11:48 +05:30
Ritvik Sardana
cd99cf6079 fix(timeline): retry the activity deep link until the row renders
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJHtad4acgJRq843yyCAxq
2026-09-18 14:04:35 +05:30
Ritvik Sardana
0efb8bc2e2 fix(ticket): keep the quoted reply when a send fails 2026-09-18 12:51:21 +05:30
Ritvik Sardana
74b5570411 fix(ticket): cap the header breadcrumb at 60% width 2026-09-18 12:44:34 +05:30
Ritvik Sardana
79d8c489ae fix(ticket): no attachment chip for a comment's inline images 2026-09-18 12:44:34 +05:30
Ritvik Sardana
34b50346a0 fix(ticket): show a sent reply or comment without waiting for the server
The composer's @update emit was never wired, so the feed only moved when the
socket happened to deliver a doc_update. With the socket down a sent message
stayed invisible until a page refresh.

Wire @update to reload the feed, and send optimistically through the timeline's
addPendingActivity: the row renders on submit, the composer clears, and the
draft comes back if the call fails. new_comment and reply_via_agent now return
the new document's name so the pending row can be swapped for the real one by
key instead of lingering beside it. A pending comment carries its attachments on
the row, since the extras lookup is keyed by a name it does not have yet.

Drop the emit("update") in CommunicationArea's keyboard handlers.
CommentTextEditor.submitComment is async, so the truthiness check around it was
always true: ctrl+enter reloaded once before the write returned and again on
success, fetching comment extras twice, and reloaded even when the guards had
rejected the send. Both editors already emit submit once the write lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJHtad4acgJRq843yyCAxq
2026-09-18 11:58:53 +05:30
RitvikSardana
3cb81c506c Merge pull request #3769 from RitvikSardana/fix/bump-frappe-ui-beta
fix: bump frappe-ui to 1.0.0-beta.63
2026-09-17 18:41:28 +05:30
Ritvik Sardana
9d276403f4 fix: hold send until editor media uploads finish 2026-09-17 18:26:22 +05:30
Ritvik Sardana
bdc5fd50ac fix: let the team and ticket type chips be picked again 2026-09-17 17:38:11 +05:30
Ritvik Sardana
f512817f02 fix: show the sidebar notification dot only when collapsed 2026-09-17 16:34:13 +05:30
Ritvik Sardana
cb771d8c9a fix: better ux for link 2026-09-17 16:07:27 +05:30
Ritvik Sardana
8dc29681ff refactor: drop the tailwind theme extends the preset already covers 2026-09-17 15:48:49 +05:30
Ritvik Sardana
2a2058f789 fix: use link fields for saved reply team and ticket type 2026-09-17 15:48:48 +05:30
Ritvik Sardana
858d800d6d fix: handle field variant 2026-09-17 15:09:04 +05:30
Ritvik Sardana
f877dd80da fix: use the right control for each ticket field type 2026-09-17 12:38:06 +05:30
Sydney
935dcd2876 fix(sla): bank hold time before computing resolution time
set_resolution_time subtracts total_hold_time, and set_hold_time is what
accumulates it, but the latter only banks a hold window when the ticket
leaves a paused status. Resolving straight out of a paused status does
both in one save, and the old order computed the resolution time first,
off a total that did not yet include the window closing in that very
save. The final hold was never deducted.

Paused -> Open -> Resolved was unaffected, because the intermediate save
banked the hold before the resolve read it, which is why the existing
tests pass either way. A ticket held for months and then resolved in one
transition reported the whole span as its resolution time.

Running set_hold_time first also means the into-paused branch clears
resolution_date before set_resolution_time reads it, so that call now
no-ops instead of computing a value the old order immediately nulled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:43:17 +05:30
Sydney
a8d38e4d48 fix(sla): compare SLA deadlines in the site timezone
response_by and resolution_by come off the API as naive strings in the
site's timezone, but plain dayjs() parses them as browser-local. When an
agent's browser sits east of the site, every deadline reads earlier than
it is, so a new ticket can show a red Failed badge while the backend
still has agreement_status as due. West of the site it fails the other
way: a breached SLA keeps showing a countdown.

dayjsLocal() parses in systemTimezone and converts to the viewer's, so
both sides of each comparison land on the same clock. Only comparisons
against "now" were wrong; server-to-server ones shifted equally and are
switched over for consistency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:43:11 +05:30
Ritvik Sardana
5893a3d95d fix(editor): patch frappe-ui suggestion popup positioning 2026-09-17 11:11:53 +05:30
Sydney
99d251cbb1 refactor(perf): update copy and show toast only on child table change 2026-09-17 01:35:30 +05:30
Sydney
102b9605bf refactor(perms): name the customer-fillable helpers for who they serve
creation_fillable_template_fields -> customer_fillable_template_fields,
customer_may_fill_at_creation -> can_customer_fill_at_creation, and the
permlevel tuple they read to match.
2026-09-16 18:01:38 +05:30
Ritvik Sardana
129d0c26cf fix: show a link field's saved value when the search does not return it 2026-09-16 17:58:12 +05:30
Sydney
6957f2c901 feat(perms): link the permission level docs from the hide warning
The warning tells an admin to raise a permission level but not what that
means. The link opens in a new tab so an unsaved template is not lost.
2026-09-16 17:51:47 +05:30
Sydney
73fada0560 refactor(perms): drop client-side visible_to filtering
get_fields_meta already strips agent-only rows for customers, so the three
portal filters never removed anything. Visibility now has one owner on the
server and the payload no longer carries visible_to.
2026-09-16 17:51:12 +05:30
Ritvik Sardana
eda11d24b8 fix: unseen ticket font weight 2026-09-16 14:50:14 +05:30
Ritvik Sardana
ecdb8a73bd fix: raise the agent and email pickers above the dialog layer 2026-09-16 14:23:21 +05:30
Ritvik Sardana
be2e78fe8a fix: open the preference dropdowns above their fields 2026-09-16 14:08:43 +05:30
Ritvik Sardana
5d2bc5055e fix: align the language dropdown and name its placeholder 2026-09-16 14:05:00 +05:30
Ritvik Sardana
bf7ed63282 fix: keep the timezone dropdown inside the settings dialog 2026-09-16 14:02:49 +05:30
Ritvik Sardana
2a36299f1d fix: put a space between vs and the comparison period 2026-09-16 13:56:07 +05:30
Ritvik Sardana
1ee15c55e9 fix: keep the add column dropdown inside the window 2026-09-16 13:49:21 +05:30
Ritvik Sardana
89d5eade51 fix: give lucide icons an explicit width so their labels line up 2026-09-16 13:49:20 +05:30
Ritvik Sardana
595532922e fix: keep grouped form script actions in the ticket kebab menu 2026-09-16 13:23:52 +05:30
Ritvik Sardana
84e679c871 fix: keep the real fieldtype when adding a list column 2026-09-16 13:23:52 +05:30
Ritvik Sardana
aea0cc6fd0 fix: align the dashboard filter row and use the framework link field 2026-09-16 13:20:54 +05:30
Sydney
7ed25d10b4 fix: update doctype.ts 2026-09-15 21:03:12 +05:30
Sydney
5163d60f67 fix(perms): repair ticket permissions on customised sites
Custom DocPerm rows make Frappe ignore the JSON perms, so these sites
never get the new permlevel grants and read the protected fields as empty.

- write the level 7 and 8 rows for roles that can already read a ticket
- add the customer roles, which a June 2026 JSON-only change never gave them
- hide template rows the validator would refuse to save after the backfill

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwkkywUhXWkgjQuf5ad9XV
2026-09-15 13:14:34 +05:30
Sydney
05d96777cb fix(perms): close field visibility gaps found in review
- prune permlevel-hidden fields in hidden_ticket_fields, so every caller
  drops both sets and not just the template tier
- keep the contact card when raised_by is hidden
- let a portal reply reopen a replied or resolved ticket
- let the closing email's rating reach the closed ticket
- keep the merge link when the agent has no write at the internal level
- stop treating System Manager as an agent
- refuse renaming the Default template

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwkkywUhXWkgjQuf5ad9XV
2026-09-15 12:40:58 +05:30
Ritvik Sardana
a4edff132e fix: colour the declined call status in the call log modal 2026-09-14 16:09:28 +05:30
Ritvik Sardana
67de9922b1 perf: drop the tailwind safelist in favour of literal status colors 2026-09-14 16:09:27 +05:30
Ritvik Sardana
6c33986659 fix: stop call() responses from raising server message toasts 2026-09-14 02:08:55 +05:30
frappe-pr-bot
bb678e1987 chore: update POT file 2026-09-13 14:30:54 +00:00
Ritvik Sardana
b5c870bdb4 fix: restore the tab row's pre-upgrade vertical spacing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 18:35:06 +05:30
Ritvik Sardana
2c0899220e fix: give the condition row three equal-width controls
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 18:27:33 +05:30
Ritvik Sardana
7f5edf116e fix: stop a loading field list from blanking the settings detail pages
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 18:15:25 +05:30
Ritvik Sardana
7737d04fcc refactor: replace NestedPopover with the frappe-ui Popover
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 18:06:44 +05:30
Ritvik Sardana
4d53e43dee fix: match single-title page headers to the breadcrumb text size
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:39:51 +05:30
Ritvik Sardana
b39f7d7a31 fix: give the tab rows their pre-upgrade height back
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:39:49 +05:30
Ritvik Sardana
fbd5cb1098 fix: match the view breadcrumb text size to every other breadcrumb
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:32:58 +05:30
Ritvik Sardana
fab46b2725 fix: close the view menu before its dialog, and settle its row hover
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:23:00 +05:30
Ritvik Sardana
16ab46898b fix: move the profile status dot to the bottom right
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:12:41 +05:30
Ritvik Sardana
dd8d968506 fix: close the remaining dead ends the audit confirmed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:08:36 +05:30
Ritvik Sardana
cb30673e37 fix: repair the data-loss and dead-control paths the audit confirmed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 13:01:51 +05:30
Ritvik Sardana
8e9e853a49 fix: close the emoji picker once a reaction is picked
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:53:30 +05:30
Ritvik Sardana
4774d6168b fix: keep the ticket kebab menu when merge is unavailable
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:53:29 +05:30
Ritvik Sardana
5dd6e87c42 fix: let a changed feedback status enable the save button
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:53:29 +05:30
Ritvik Sardana
8622b7b88c fix: let the SLA enabled switch toggle once per click
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:53:28 +05:30
Ritvik Sardana
f2b47ebc34 fix: stop a custom field committing the text typed into its search box
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:53:28 +05:30
Ritvik Sardana
0455163a92 fix: put the width where a custom trigger slot cannot drop it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:53:27 +05:30
Ritvik Sardana
0aaf1966dd fix: let MultipleAvatar take the array its callers pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:44:18 +05:30
Ritvik Sardana
f3fd4f20d5 fix: drop the dead clear button from the language field
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:37:07 +05:30
Ritvik Sardana
fa3d78eb0a fix: stop the tags row from opening the colour picker
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:37:05 +05:30
Ritvik Sardana
f50e0957f2 fix: give the saved reply and signature editors an uploader
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:24:27 +05:30
Ritvik Sardana
5f12a70c41 feat: mount the table menu so inserted tables can be edited
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:23:00 +05:30
Ritvik Sardana
7cb87d7185 fix: hand the editor's upload signal and progress callback to the uploader
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:20:49 +05:30
Ritvik Sardana
5439daad78 fix: pass form-script toast icons through as components
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:17:37 +05:30
Ritvik Sardana
4fde6a2a57 chore: drop the QA screenshots that slipped into the branch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:15:34 +05:30
Ritvik Sardana
e20c5f0eb9 fix: make the SLA card hover-only and give AssignTo its layout box back
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:13:48 +05:30
Ritvik Sardana
28f60f059d fix: drop the dead bindings the date pickers and preset menu carried
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:09:21 +05:30
Ritvik Sardana
67c446f8b2 fix: clear the second round of audit findings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:07:43 +05:30
Ritvik Sardana
20a7b137b6 fix: replace the props and tokens beta.63 no longer has
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:02:47 +05:30
Ritvik Sardana
a0ebcc200d fix: reset the right delete flags and stop the KB search opening on click
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 12:01:25 +05:30
Ritvik Sardana
d8a103495f fix: repair the beta.63 contract breaks the audit found
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 11:59:23 +05:30
Ritvik Sardana
963e82ec03 fix: focus inputs through the component API, not $el
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 11:55:31 +05:30
Ritvik Sardana
24438c9f3e fix: give Switch real booleans where the source is a doctype check field
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 11:45:16 +05:30
Ritvik Sardana
845cefcb67 fix: clear the runtime warnings the beta.63 QA sweep surfaced
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 11:40:58 +05:30
Ritvik Sardana
808c84df15 refactor: replace the vendored autocompletes with frappe-ui Combobox
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 11:28:46 +05:30
Ritvik Sardana
55b02def4e fix: stop popover triggers from toggling twice
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 11:19:41 +05:30
Ritvik Sardana
c60d549184 fix: close dialogs from the action context and restore the page length picker
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 01:03:03 +05:30
Ritvik Sardana
db8851aa9a fix: reset the delete confirmation from the dropdown open state
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 00:57:49 +05:30
Ritvik Sardana
a62daf7c8d fix: restore button element refs, date quick filters and combobox query resets
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 00:56:10 +05:30
Ritvik Sardana
f6cf255d19 fix: make component-based dropdown rows real menu options again
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 00:54:09 +05:30
Ritvik Sardana
abbc0430f8 fix: restore tab labels, input focus refs and the change password fields
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 00:46:14 +05:30
Ritvik Sardana
8a21e8291f fix: import HoverCard so the hover panels resolve
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-12 00:40:20 +05:30
Ritvik Sardana
78160ca54b fix: rename dropdown group items to options so grouped menus render
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:31:40 +05:30
Ritvik Sardana
99863e6c7c fix: repair an inert ink class on the reaction button
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:24:32 +05:30
Ritvik Sardana
312e6a9240 fix: drop props v1 overlays no longer read
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:20:07 +05:30
Ritvik Sardana
bfb6fac112 chore: drop a screenshot committed by mistake
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:17:56 +05:30
Ritvik Sardana
8729f9cdf3 fix: scan frappe-ui experimental for Tailwind classes and bind date pickers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:17:03 +05:30
Ritvik Sardana
fd6e22de75 fix: keep alias icon names rendering and pin prosemirror to tiptap's floor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:14:44 +05:30
Ritvik Sardana
6dd1d4f1e3 fix: render the call log and article list icons again
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 23:10:45 +05:30
Ritvik Sardana
5fdff36113 fix: restore tab panel layout and the grouped list icon
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:58:59 +05:30
Ritvik Sardana
c65b1aee20 fix: replace toast.create, prefix lucide icon names and dedupe prosemirror
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:47:20 +05:30
Ritvik Sardana
938de94638 fix: guard useOnboarding for guests and restore the tag search shortcut
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:38:31 +05:30
Ritvik Sardana
d513667c3f fix: migrate Tabs, HoverCard, Menu groups and Badge themes to v1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:36:26 +05:30
Ritvik Sardana
59fadaefa7 fix: migrate Popover, Tooltip and Dropdown to the v1 slot and prop names
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:32:06 +05:30
Ritvik Sardana
eb67904747 refactor: replace FeatherIcon with lucide icons
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:25:16 +05:30
Ritvik Sardana
a56fd4d430 refactor: move frappe-ui imports to their beta.63 homes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 22:13:55 +05:30
Ritvik Sardana
43d9ae3f2b chore: bump frappe-ui to 1.0.0-beta.63
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qR2bFcCSBEXQXtXws8oas
2026-09-11 21:59:55 +05:30
Sydney
795b75c861 Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms 2026-09-11 21:18:59 +05:30
Sydney
1b9166bb24 refactor(perms): clarify the visibility lookup name and comments 2026-09-11 21:02:25 +05:30
Sydney
50d8596104 refactor(perms): improve code quality and trim comments 2026-09-11 20:52:55 +05:30
Sydney
05f3b9b416 refactor: code quality 2026-09-11 15:25:22 +05:30
RitvikSardana
3ab76d795f Merge pull request #3765 from RitvikSardana/fix/assignment-notification-backlog
fix: assignment notification backlog, deprecate replaced doctypes
2026-09-10 02:09:03 +05:30
Ritvik Sardana
e6b7a6d74e chore: reduce comments in the assignment backlog patch 2026-09-10 01:56:59 +05:30
Ritvik Sardana
f91eec298d test: move the notification log seeder into test_utils
AGENTS.md requires backend test helpers to live there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFVLhhv1VXBVfSP6gZ71Lp
2026-09-10 01:47:33 +05:30
Ritvik Sardana
3b2296960d refactor: stop writing to the doctypes core replaced
Bounce and auto-reply notes still went to HD Ticket Comment, which nothing
displays, so agents never saw that their reply had bounced. They are core
Comments now, and the test that covered them was querying the old table.

Assignment was logged as an HD Ticket Activity row from two places in the
SPA. Core assign_to already writes that entry to the timeline, so both were
invisible duplicates. Both call sites also destructured `activities` off
useTicket, which no longer provides it; they use reloadTicketFeed now.

Also drops the dead HD Notification type from the SPA, the unused
log_ticket_activity, and the split and on_trash blocks that only ever
touched legacy rows. Core delete_doc already clears a document's Comment
and Notification Log rows, and split moves the Comment rows the activities
were migrated into.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFVLhhv1VXBVfSP6gZ71Lp
2026-09-10 01:46:21 +05:30
Ritvik Sardana
b947a41ec3 fix: make the sidebar notification dot black
Blue read as a link affordance next to a bell that is not one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFVLhhv1VXBVfSP6gZ71Lp
2026-09-10 01:00:13 +05:30
Ritvik Sardana
1ba03d358a chore: deprecate the doctypes replaced by core
HD Ticket Comment, HD Ticket Activity and HD Notification are no longer
written to. Nothing calls log_ticket_activity, and comments and
notifications go to Comment and Notification Log. The rows stay readable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFVLhhv1VXBVfSP6gZ71Lp
2026-09-10 00:57:01 +05:30
Ritvik Sardana
607c5babcb fix: mark pre-upgrade assignment notifications as read
Helpdesk only notified on manual assignment. Core assign_to now owns
assignments and notifies on every one, so the first load after the upgrade
shows an unread row for every ticket the Assignment Rule had ever routed.

Clears that backlog once. Assignments made after the patch still arrive
unread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFVLhhv1VXBVfSP6gZ71Lp
2026-09-10 00:56:55 +05:30
RitvikSardana
d96e9e39a0 Merge pull request #3700 from RitvikSardana/refactor/comments-notifications-migration
refactor: comments and notifications doctype
2026-09-09 23:53:12 +05:30
Ritvik Sardana
a979e843df feat: print per-chunk progress while migrating comments and notifications 2026-09-09 23:35:49 +05:30
Ritvik Sardana
dd9911a883 fix: delete a comment's notifications along with it 2026-09-09 16:48:06 +05:30
Ritvik Sardana
8d20dae571 fix: remount the ticket page when navigating to an already visited ticket
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLRT2UCFYoGRxyahBphFbY
2026-09-09 16:25:29 +05:30
Ritvik Sardana
298a69758c fix: fetch comment extras and calls once per ticket instead of on every tab change
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLRT2UCFYoGRxyahBphFbY
2026-09-09 16:02:50 +05:30
Ritvik Sardana
265a8c90b3 refactor: drop the desk form wrappers now that core honours Comment permissions
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLRT2UCFYoGRxyahBphFbY
2026-09-09 13:28:59 +05:30
Ritvik Sardana
bb998cce65 docs: prefer framework fixes over helpdesk workarounds in agent guidelines 2026-09-09 11:03:40 +05:30
Ritvik Sardana
d4fa0b2b61 fix: lock the comment row so concurrent reaction toggles cannot double insert 2026-09-09 11:02:34 +05:30
Ritvik Sardana
84f6b9ae52 fix: address review findings in comment migration and notifications 2026-09-09 10:41:50 +05:30
Sydney
f88a61bd8c refactor(perms): replace the role ladder with audiences 2026-09-08 16:27:58 +05:30
Sydney
bfd72acdb7 style(perms): trim comments to a line or two 2026-09-08 15:26:00 +05:30
RitvikSardana
c0017a446a Merge pull request #3697 from RitvikSardana/fix/dynamic-timeline-legend
fix: render ticket timeline legend from what the rail draws
2026-09-08 12:12:17 +05:30
RitvikSardana
2487a3f499 Merge pull request #3710 from RitvikSardana/fix/ui-changes
fix: polish UI in a few screens
2026-09-08 12:11:08 +05:30
Ritvik Sardana
ab9fbeae92 fix: resolve session user in the Comment permission hook 2026-09-08 11:23:53 +05:30
Ritvik Sardana
c8553a953d fix: gate comment extras behind the Comment permission hook 2026-09-08 10:57:38 +05:30
Ritvik Sardana
5619ce706f fix: comment tests 2026-09-07 17:46:58 +05:30
Ritvik Sardana
79e9b39964 Merge branch 'develop' of https://github.com/frappe/helpdesk into refactor/comments-notifications-migration 2026-09-07 16:52:56 +05:30
RitvikSardana
41ad71c69f Merge pull request #3760 from frappe/pot_develop_2026-09-06
chore: update POT file
2026-09-07 16:32:07 +05:30
Sydney
f1126f96fc refactor(perms): read the visibility tier for creation-fillable fields
creation_fillable_template_fields keyed on hide_from_customer, the flag
synced from visible_to. Read the tier directly so the write path uses the
same source of truth as the rest of the field-visibility code.
2026-09-07 14:11:53 +05:30
frappe-pr-bot
7a46faa747 chore: update POT file 2026-09-06 13:44:55 +00:00
Sydney
fc51371e55 refactor(perms): drive ticket field visibility from template tiers
The Default template used to write custom-field permission levels directly,
which raced with Customize Form and needed a base_permlevel column to undo it.
Permission levels now stay a static security floor; the template's visible_to
tier only narrows what each role sees on helpdesk pages, enforced by the
HDTicket read override and the app's own endpoints. Agent-only fields are
dropped from hardcoded portal rows via a _hidden_fields list, and a cosmetic
patch backfills visible_to on rows that predate the column.
2026-09-04 19:54:12 +05:30
Sydney Gomes
9b1023c861 Merge pull request #3628 from aerodeval/fix/bounce-notification-loop
fix: stop mail loops from bounces and autoresponders
2026-09-03 16:40:57 +05:30
Sydney
4592c40f4e chore: shorten comments 2026-09-03 16:19:27 +05:30
Sydney Gomes
567965ed50 Merge pull request #3746 from aerodeval/fix/portal-settings-patch-link-validation
fix: write portal defaults without saving the whole document
2026-09-03 12:41:25 +05:30
Sydney
acd2e28600 fix: let auto-replied mail thread onto its ticket
An out of office should reach agents on the ticket, and auto-replied
senders rate-limit themselves so they cannot sustain a loop the way
bounces and auto-generated feeds can. Those still get parked.
2026-09-03 02:53:36 +05:30
Sydney
f2ac96a4c8 fix: write portal defaults without saving the whole document
Saving Portal Settings validates every link on it, so a menu row left
behind by a deleted doctype failed the write and stopped migrate. Write
the two Single values directly, from one function install and the patch
now share.
2026-09-03 02:41:26 +05:30
Sydney
6a1693dcc6 Revert "refactor: move overrides into per-doctype folders"
This reverts commit b3b717d183.
2026-09-03 02:12:57 +05:30
Sydney
b3b717d183 refactor: move overrides into per-doctype folders 2026-09-03 02:04:58 +05:30
Sydney
8fdbf36add fix: ci lint error 2026-09-03 01:46:27 +05:30
Sydney
f80a004ef6 fix: record parked mail and note it on the ticket
Record Unhandled Email on every transport, not just IMAP, and leave an
internal comment on the matched ticket when a bounce or auto-reply is
parked, so agents still see failed deliveries and out-of-office replies.
2026-09-03 01:41:35 +05:30
Sydney Gomes
36b7490f07 Merge pull request #3433 from aerodeval/handle-disabled-status
fix: move agents off an agent status when it is disabled
2026-09-02 21:49:24 +05:30
Sydney
295d13a421 fix: import userStorage from its new composables home 2026-09-02 20:17:15 +05:30
Sydney
2711f8a613 Merge remote-tracking branch 'upstream/develop' into handle-disabled-status 2026-09-02 20:17:03 +05:30
Sydney
41f7cd80b6 feat: greet a login whose status was changed while away
The toast only reaches agents who are online when it happens. Stamp who
changed the availability, send it with the login payload, and greet on
the next visit if it was someone else. First visit on a device only
records the baseline.
2026-09-02 20:09:53 +05:30
Sydney
9a5267893e fix: show the default agent status below the priority section 2026-09-02 19:17:32 +05:30
Sydney
bd526715ea fix(build): dedupe vue-router and dompurify for the linked @framework/ui
They are peers of @framework/ui that nothing installs next to its
source, so a build where the frameworkUI plugin's dedupe is missing
cannot resolve them.
2026-09-02 17:15:10 +05:30
Sydney Gomes
7b5d512e30 Merge pull request #3743 from aerodeval/fix/desk-build-vue-router
fix(build): dedupe vue-router and dompurify for the linked @framework/ui
2026-09-02 17:13:21 +05:30
Sydney
02d708f961 fix(build): dedupe vue-router and dompurify for the linked @framework/ui
They are peers of @framework/ui that nothing installs next to its
source, so a build where the frameworkUI plugin's dedupe is missing
cannot resolve them.
2026-09-02 16:56:18 +05:30
Sydney
8fa4f9d555 style: tighten and simplify comments 2026-09-02 12:26:54 +05:30
Sydney
8eb157d11d test: pin that force-delete cannot remove the default status 2026-09-02 03:35:28 +05:30
Sydney
6a8971baab fix: harden the disable path against two edge cases
A status disabled behind the controller's back could not save unrelated
fields once it was the default; guard on the transition, matching HD
Agent's availability check. And on_update runs on insert too, so a
status created disabled consulted the default for agents it cannot
have; query the agents first.
2026-09-02 03:21:46 +05:30
Sydney
4953b2a860 fix: spell out the ordering the migration freezes
The patch reproduced the old lookup's implicit ORDER BY, which is the
right value to freeze but the wrong thing to leave unsaid in a change
about not relying on implicit ordering. Pinned with a test.
2026-09-02 02:57:04 +05:30
Sydney
a8e10a11f5 fix: name the default agent status in HD Settings
Picking it with an implicit ORDER BY decided where agents get moved when
their status is retired. HD Settings names it now, like it already does
for the ticket statuses.
2026-09-02 02:48:17 +05:30
Sydney Gomes
bf050e7301 Merge pull request #3742 from aerodeval/fix/grammarly-click-outside
fix: avoid  improper reply box closing for Grammarly suggestions
2026-09-02 02:02:44 +05:30
Sydney
beacb925f4 fix: keep reply box open when using a Grammarly suggestion
Grammarly renders its suggestion card outside the reply box, so accepting one counted as a click outside and closed the box.
2026-09-02 01:30:25 +05:30
Sydney Gomes
6f32222675 Merge pull request #3741 from aerodeval/fix/settings-ui-polish
fix(settings): blank empty states, always-visible Update Account, false typing indicator
2026-09-01 14:46:59 +05:30
Sydney
e2e5a3c198 fix(email): don't broadcast typing when the signature loads
The watcher on newEmail called onUserType() for any change, including
programmatic ones. An agent with a signature configured has it dropped
into the empty editor on load, which emitted helpdesk_ticket_typing - so
merely opening a ticket showed "is typing" to everyone else viewing it.

Skip content that is empty or only the signature. This also covers agents
with no signature, whose editor is filled with an empty paragraph.
2026-09-01 12:20:12 +05:30
Sydney
a989a6bafc fix(settings): show Update Account only when the account changed
The button was always visible, so clicking it with no edits only produced
a "No changes made" toast.

isDirty could not be reused as it stood: the form normalises what the
server sends (a 0 becomes false, a null becomes ""), then compared those
values back against the raw account, so every custom account reported
itself as changed. Compare against values built by the same initialisers
instead, and iterate the fields rather than listing them - the old chain
omitted use_tls, use_ssl_for_outgoing and four others, so editing only
those said "No changes made" and dropped the edit.
2026-09-01 12:20:02 +05:30
Sydney
b8d701a56b fix(settings): import EmptyState so empty screens render
Four settings screens use <EmptyState> in their template but never import
it. There is no component auto-import plugin, so Vue cannot resolve the tag
and renders nothing, leaving a blank pane where the empty state should be.

Email Accounts also showed its empty state while the list was still
loading; guard it on loading like the other three screens do.
2026-09-01 12:19:52 +05:30
Sydney Gomes
2574d2f1f7 Merge pull request #3739 from aerodeval/fix/attachment-download-fallback
fix: download attachments the browser cannot preview
2026-08-31 22:22:23 +05:30
Sydney
56947b47b8 fix: download mov attachments instead of previewing 2026-08-31 17:23:32 +05:30
Sydney
8fe4b398cd fix: keep mov, m4v, ico and apng previewable 2026-08-31 16:29:58 +05:30
Sydney
5a3bff0630 fix: download attachments the browser cannot preview 2026-08-31 16:25:24 +05:30
Sydney Gomes
27f459fff2 Merge pull request #3737 from frappe/pot_develop_2026-08-30
chore: update POT file
2026-08-31 12:51:17 +05:30
frappe-pr-bot
f954dac6a3 chore: update POT file 2026-08-30 14:54:54 +00:00
Sydney
c1eace1d2e fix: never show the ticket key and lift shown fields at migrate
The template refuses to show key at any permission level, since it
authenticates guest feedback links. The migration also lifts shown
custom fields below the visible level to 7, still never lowering.
2026-08-30 00:40:46 +05:30
RitvikSardana
081e9d9204 Merge pull request #3718 from frappe/pot_develop_2026-08-27
chore: update POT file
2026-08-29 02:40:34 +05:30
Ritvik Sardana
5b81fd1ae3 fix: revoke comment permission for customer user 2026-08-29 00:31:22 +05:30
Sydney
895d09d04a fix: never lower a permlevel when a template row is removed
A stale base could undo a restriction the admin set while the field
was listed. Removal now restores the base only when that raises the
level, so exposing a removed field always takes Customize Form.
2026-08-28 19:46:06 +05:30
Sydney
a0f7599aa5 fix: keep customize form levels on template field removal
Removing a row now only undoes the sync's own write. A level an
admin set in Customize Form after the field was added is newer
than the recorded base, so it stands.
2026-08-28 19:31:57 +05:30
Sydney
003f07b6a2 feat: sync custom field permlevels with template visibility
Hiding a custom field in the default template raises it to the
internal level and showing it lowers it to the visible level.
Removing a row returns the field to the level it arrived with,
remembered on the row. The migration patch records those base
levels and stays raise only, so an upgrade never exposes a field.
2026-08-28 19:14:52 +05:30
Sydney
5d04af5d8d refactor: make hide from customer form-only for custom fields
Hiding a custom field no longer raises its permission level. Like
standard fields, it only comes off the form now, and Customize Form is
the one place that changes what customers may read. Templates never
touch a permission level.
2026-08-28 17:03:24 +05:30
Sydney
45422c0cf1 refactor: rename customer_writable_now to customer_editable_fields 2026-08-28 17:03:13 +05:30
Sydney
d7a27be0f2 fix: raise the permlevel of custom fields hidden in the template
Hiding only moved the field off the form, so the API still served it.
Raise only: a template save can never expose a field, and showing one
again means lowering its level in Customize Form first.
2026-08-28 14:26:37 +05:30
Sydney
1546a0bf23 refactor: name the permission guards for what they do
is_staff -> is_agent_staff, CUSTOMER_WRITABLE_AFTER_CREATION ->
CUSTOMER_EDIT_EXEMPT_FIELDS, and shorter comments throughout.
2026-08-28 13:45:41 +05:30
Sydney
9a429e48b2 test: cover the form script and template permission rules 2026-08-28 13:15:17 +05:30
Sydney
5f4a7b6514 refactor: stop templates writing field permission levels
Saving the default template moved custom field permlevels, so one doctype's
save changed another's schema. Setting a permission level is a customization
and belongs in Customize Form.

The template now only validates: an internal field cannot be shown to
customers, standard and custom alike. Hiding a field means form only, which
the fields table now says.
2026-08-28 13:15:10 +05:30
Sydney
22da73bc0a refactor: move ticket permission constants to consts
PORTAL_INSERT_EXEMPT_FIELDS was SERVER_OWNED_INSERT_FIELDS, which described
neither what the list does nor when. It exempts fields from the permlevel
reset, and only on insert.
2026-08-28 13:15:01 +05:30
Sydney
f6482d5563 revert: drop the read check on the outside hours banner 2026-08-28 13:14:51 +05:30
Sydney
1fa8b45542 fix: keep agent form scripts off the customer portal
Customers held read on HD Form Script, so a portal login could list every
script including agent-only ones. get_one also took is_customer_portal from
the caller, so asking for the agent scripts produced them.

Delivery is unchanged: get_form_script reads through the query builder, so
portal scripts still reach customers without the doctype grant.
2026-08-28 13:14:43 +05:30
Sydney
4f9a505e4c Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms
check_update_perms was refactored on both sides. Kept develop's null-safe
old_doc guard and this branch's is_staff check, which drops the
via_customer_portal clause so an agent-raised ticket is frozen once it is
closed or rated.
2026-08-28 02:06:49 +05:30
Sydney
3c930bc172 refactor: limit template permlevel sync to custom fields
Saving a template rewrote permission levels on shipped HD Ticket fields
through Property Setters. That overlay competes with Customize Form over
the same value, and nothing clears a stale one on migrate.

Standard field levels are now static in the doctype and nothing writes
them at runtime. Custom fields keep taking their level from the template,
written straight onto the Custom Field row: that column is the field's
own definition, so there is no overlay and nothing to race. Hiding a
custom field still removes it from API responses, which is the point.

Hiding a standard field is now a form choice only. The levels the app
ships already say what customers may read, and Customize Form is where
that is changed.

No Property Setter is written or deleted anywhere any more.
2026-08-28 01:34:27 +05:30
Sydney
11120774c3 feat: stop customers editing tickets after they are raised
Permission levels cannot do this on their own. A customer needs write
access at level 0 to raise a ticket at all, which leaves subject,
description, feedback_rating and five more editable for the life of the
ticket. The level check is also skipped entirely when a save passes
ignore_permissions, so a whitelisted method that saves that way hands a
customer every field regardless of level.

prevent_customer_edits runs in before_validate and refuses any change a
customer did not make while raising the ticket. They may still close it
and rate it, nothing else. Closing means moving to a status whose
category is Resolved: Replied means an agent answered, so accepting it
would let a customer misrepresent the queue.

Server-side writers acting for the customer set ignore_customer_edit_guard.
The one on on_communication_update is required rather than defensive, as
after_insert reaches it on every ticket that has a description.

check_update_perms no longer skips tickets an agent raised, which let a
customer revise their rating afterwards. is_staff covers System Managers,
who are not agents but hold write at every level.
2026-08-28 01:34:11 +05:30
Sydney
532ac17049 fix: check read permission on the outside hours banner
Every other endpoint in this module checks it. Without it any signed-in
user could pass a ticket name they have no access to and learn that it
exists, that it was raised outside working hours, and the response-time
message.
2026-08-28 01:33:33 +05:30
frappe-pr-bot
891abbef44 chore: update POT file 2026-08-27 07:56:22 +00:00
Sydney
30af657041 fix: show response and resolution times on tickets with no SLA
The sidebar hid both rows unless an SLA metric existed, so a ticket
raised without a priority showed nothing. Fall back to the timestamp.
2026-08-27 13:06:06 +05:30
Sydney
5441b3050f fix: give HD Customer Manager the reads HD Customer has
The portal loads these three doctypes for every customer, but only the
HD Customer role could read them, so a customer manager hit a permission
error on the ticket form.
2026-08-27 13:06:01 +05:30
Sydney
63e8c3d11f feat: let customers read SLA timings
Moves the SLA display fields to the customer-readable permlevel so the
portal can show first response and resolution. That puts them at a level
customers may write at creation, so SERVER_COMPUTED_FIELDS keeps them out
of the creation exemption and off the template form.
2026-08-27 13:05:55 +05:30
Sydney
b5b7c10e94 fix: parenthesize the exception tuple 2026-08-27 13:05:23 +05:30
Sydney Gomes
715e368e80 Merge pull request #3711 from aerodeval/fix/outside-hours-holiday
fix: consider holidays for outside working hours banner
2026-08-20 16:47:30 +05:30
Sydney
54e4587eb4 fix: consider holidays for outside working hours banner 2026-08-20 15:33:41 +05:30
Ritvik Sardana
c2a454ee0d fix: polish UI in a few screens 2026-08-20 13:38:51 +05:30
Ritvik Sardana
7e6a94b163 fix(ticket): keep one loading state on the agent ticket page
The page gated on prefetchActivityTimeline, which no longer exists — the
timeline store is only reachable through useActivityTimeline. TicketAgent now
starts the feed itself and latches its loading flag, so the doc and the feed
load behind a single spinner instead of two in sequence. Latched because a
doc_update reloads the feed, which must not blank the page.

Saved reply actions reload the feed through the mounted timeline, which
registers its refresh on the ticketMap entry, keyed per ticket so a reload
after a ticket switch hits the right feed.

Also moves the timeline imports to the @framework/ui/ActivityTimeline entry
point and picks up AttachmentChip for comment attachments.
2026-08-18 02:06:14 +05:30
Ritvik Sardana
03a8d28289 fix: update VERSION_FIELDS 2026-08-17 23:45:54 +05:30
Ritvik Sardana
d71eb249ba fix: saved reply comment actions with core comments
- assert the Add Comment action lands in core Comment, not HD Ticket Comment
- reload the timeline after actions apply: the send-triggered reload races
  the action comment's socket append and can drop it until a refresh
- drop contact from the version fields worth a feed row
2026-08-17 23:40:23 +05:30
Ritvik Sardana
69fb59c899 Merge branch 'develop' of https://github.com/frappe/helpdesk into refactor/comments-notifications-migration 2026-08-17 23:02:05 +05:30
Ritvik Sardana
787d9b2dac refactor(timeline): use framework/ui ActivityTimeline component
Replace the hand-rolled ticket activity feed (TicketAgentActivities,
EmailArea, CommentBox, HistoryBox, FeedbackBox) with the framework's
ActivityTimeline + useActivityTimeline on both desktop and mobile.

- one shared fetch per ticket (SHARED_VISIBLE_TYPES); tabs filter client-side
- TimelineContainer bounds the feed; mobile page is a clamped column
- comment extras (reactions/attachments) via helpdesk.api.timeline
- deep links map ?highlight=comment-/communication- to timeline row keys
- drop dead server endpoints (get_ticket_activities, get_history,
  get_version_history, get_views)
2026-08-17 23:01:28 +05:30
Pratham Sharma
20eb1fd7c1 Merge pull request #3701 from ps173/auto-generate-pot
chore: auto generate POT files for main-hotfix
2026-08-17 16:01:05 +05:30
Pratham
9c709ac2f8 chore: auto generate POT files for main-hotfix 2026-08-17 15:31:01 +05:30
Ritvik Sardana
c1b8c2c534 fix: pop out comments form api get_one 2026-08-17 00:57:46 +05:30
Ritvik Sardana
0db605ef36 fix(comments): guard reactions to ticket comments, handle missing search index, port reaction tests 2026-08-16 00:31:22 +05:30
Ritvik Sardana
462f1e30f3 test: cover the search index relabel patch 2026-08-16 00:05:02 +05:30
Ritvik Sardana
a4bed869dd fix: don't bump modified when back-filling erpnext_customer 2026-08-15 22:12:30 +05:30
Ritvik Sardana
b4e97540f3 refactor: read notifications from core Notification Log 2026-08-15 22:11:59 +05:30
Ritvik Sardana
e02179371b refactor: codebase to use core comments instead of hd_ticket_comment 2026-08-15 21:38:55 +05:30
Ritvik Sardana
ec828d9993 fix: set up core comments schema on fresh install 2026-08-15 14:30:17 +05:30
Ritvik Sardana
aed32274ad refactor: migrate ticket comments and notifications to core doctypes 2026-08-15 14:28:21 +05:30
Ritvik Sardana
fe2ebaed5b fix: render timeline legend from what the rail draws 2026-08-14 05:18:13 +05:30
RitvikSardana
34e0e8f41a Merge pull request #3696 from RitvikSardana/fix/hide-bulk-actions-for-customers
fix: hide bulk reply, assign and edit from customer roles
2026-08-14 00:07:06 +05:30
Ritvik Sardana
c3ae467da1 fix: hide bulk reply, assign and edit from customer roles
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 23:59:54 +05:30
RitvikSardana
c74c98e4ff Merge pull request #3693 from RitvikSardana/feat/macros
feat: actions in saved replies
2026-08-13 14:08:40 +05:30
Ritvik Sardana
ea1a302fb1 fix: add telemetry event for saved reply actions 2026-08-13 13:23:43 +05:30
Ritvik Sardana
b32a2f0eac fix(ux): adding actions 2026-08-13 13:11:43 +05:30
Ritvik Sardana
729607fb68 fix: unified collapsed state for saved reply actions 2026-08-13 11:20:29 +05:30
Ritvik Sardana
7e06f188c4 fix: reject non string action values on save 2026-08-12 22:39:22 +05:30
Ritvik Sardana
f574a2caf6 fix: paginate saved reply selector 2026-08-12 22:11:34 +05:30
Ritvik Sardana
dccc7acc73 fix: skip shadowed saved reply actions and silence ssti lint 2026-08-12 22:05:13 +05:30
Ritvik Sardana
908c408847 fix: clear stale assignment rule cache after saved reply action tests 2026-08-12 21:21:31 +05:30
Ritvik Sardana
c19afe3496 refactor: sla style table for saved reply actions 2026-08-12 20:46:43 +05:30
Ritvik Sardana
a8addfb8b7 feat: editable comments from actions 2026-08-12 20:46:34 +05:30
Ritvik Sardana
8dc39e144b chore: truncate comment text 2026-08-12 14:05:45 +05:30
Sydney
e19b72965d feat: confirm customer access for the fields a template save touched 2026-08-12 14:05:12 +05:30
Ritvik Sardana
17e9393e96 fix: ux for saved reply action failures 2026-08-12 13:59:50 +05:30
Sydney
11072dcee3 refactor: plain-english template permission messages 2026-08-12 13:48:36 +05:30
Sydney
dc31abeedd refactor: extract customer_may_fill_at_creation helper 2026-08-12 13:15:52 +05:30
Ritvik Sardana
ede91aee21 fix(styles): settings modal 2026-08-12 12:58:23 +05:30
Ritvik Sardana
1365b139ba fix: keep staged saved reply actions when apply rolls back 2026-08-12 12:58:13 +05:30
Ritvik Sardana
972ad7012d fix: drop staged saved reply actions on failed apply, scope per agent 2026-08-12 12:29:21 +05:30
Ritvik Sardana
491573946f refactor: move replyComposer into modalStates 2026-08-12 11:42:32 +05:30
Ritvik Sardana
3f05c90c14 fix: update saved reply icon 2026-08-12 02:36:18 +05:30
Ritvik Sardana
2d17b29909 fix(styles): saved reply selector filter 2026-08-12 02:28:49 +05:30
Ritvik Sardana
8e1c071f14 fix: select field saving on every keystroke 2026-08-12 02:01:41 +05:30
Ritvik Sardana
169820f8e7 feat: open the action picker from the whole chip 2026-08-12 02:01:39 +05:30
Ritvik Sardana
199a43302e refactor: saved reply modal fetching 2026-08-12 02:01:31 +05:30
Ritvik Sardana
fd5704d128 chore: code cleanup 2026-08-12 00:21:54 +05:30
Ritvik Sardana
2c8a7c0e7d refactor: cut saved reply action network calls
Build the action options once instead of per component: frappe-ui reloads a
cached list resource on every createListResource call, so each action chip
fired its own tags, ticket type and team requests.

Team and ticket type are now searched like link fields (debounced name query)
rather than fetched whole.
2026-08-12 00:18:41 +05:30
RitvikSardana
83c5121b39 Merge pull request #3692 from RitvikSardana/fix/ticket-create-crash-on-missing-old-doc
fix(hd-ticket): don't check update perms without a previous version
2026-08-12 00:07:33 +05:30
Ritvik Sardana
e4589968d5 fix: replacing a saved reply closes the reply editor 2026-08-11 22:24:07 +05:30
Sydney
9da6353a0a fix: tighten which fields customers can fill at creation 2026-08-11 20:00:49 +05:30
Sydney
27650682f8 fix: make visible template fields fillable only at creation 2026-08-11 19:26:18 +05:30
Ritvik Sardana
1642385558 fix(hd-ticket): don't check update perms without a previous version
check_update_perms() ran off self.is_new() as a proxy for "there is a
previous version to compare against". __islocal can be cleared during
insert() (any before_insert hook that persists the doc calls db_insert,
which sets it to False), so is_new() was False on create while
_doc_before_save was still None, and old_doc.status raised AttributeError.

Guard on the previous doc itself, which is what the check actually needs.
2026-08-11 19:04:26 +05:30
Sydney
c53a78e4d7 fix: drive ticket permlevels from the default template only 2026-08-11 18:56:01 +05:30
Ritvik Sardana
72a3d5623b fix: make saved reply actions field read only in desk 2026-08-11 18:44:43 +05:30
Ritvik Sardana
10145f63c0 fix: actions clearing bug 2026-08-11 18:43:08 +05:30
Ritvik Sardana
3f66824512 feat: edit actions from email editor 2026-08-11 18:23:33 +05:30
Ritvik Sardana
7ddc78a7d0 feat: add actions in email editor
Saved reply actions are staged in the composer and applied once the reply
is sent. Chips are individually removable, the staged comment is editable,
and applying another reply replaces the staged set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 17:36:41 +05:30
Sydney
0f2ffc0856 feat: restore shipped permlevels when a template drops a field 2026-08-11 14:27:37 +05:30
Sydney
d09f7abd2d fix: leave desk inserts by system managers unstamped 2026-08-11 14:26:40 +05:30
Sydney
8260b84477 fix: let templates open only safe fields to customers 2026-08-11 14:25:33 +05:30
Sydney
71d7d1f163 Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms 2026-08-11 14:01:07 +05:30
Ritvik Sardana
3e87c14cb7 fix: add grouping in actions 2026-08-11 12:52:01 +05:30
Ritvik Sardana
e37d817b19 feat(saved-replies): add actions table in Saved Reply
Saved replies can now carry actions (macros) that update the ticket when
the reply is sent: set status, priority, team or ticket type, assign an
agent or yourself, add/remove tags, and add a comment.

Backend
- HD Saved Reply gets an `actions` JSON field, with validation for the
  action type, its value, duplicates, and the one-assignment-per-reply
  and no-tag-in-both-lists rules
- `get_rendered_saved_reply` returns title, rendered message and the
  reply's still-valid actions
- `apply_saved_reply_actions` applies them to a ticket, skipping (not
  raising on) actions that went stale, since the email is already sent
- tests covering validation, permissions and application

Frontend
- Actions table in the saved reply settings form: one row per action,
  action type and its value edited in cells
- Rendered with the shared Grid component from @framework/ui

Note: the Grid props used here (emptyText, addLabel, selectable,
reorderable, rowAction) ship in a companion frappe PR.
2026-08-11 12:44:02 +05:30
RitvikSardana
6d54842fd7 Merge pull request #3690 from RitvikSardana/fix/permission-checks-use-passed-user
fix: use the passed-in user for team permission checks
2026-08-11 12:25:04 +05:30
RitvikSardana
e7ac9a0b8c Merge pull request #3689 from RitvikSardana/feat/ticket-analysis
feat: ticket analytics tab
2026-08-11 12:08:00 +05:30
Ritvik Sardana
9a8adfa445 fix: use the passed-in user for team permission checks 2026-08-11 12:07:29 +05:30
Sydney
72ad711cab refactor: declare server-owned permlevel exemption fields 2026-08-11 11:25:59 +05:30
Sydney
4abe45a8e4 feat: warn when template save changes ticket permlevels 2026-08-11 11:25:49 +05:30
Ritvik Sardana
67df48e170 fix: always label a late first response as Responded late 2026-08-11 01:53:30 +05:30
Ritvik Sardana
2f8fbf8a3e fix: place the resolution in the timeline instead of always last 2026-08-11 01:32:40 +05:30
Sydney
7ef57e5ad7 feat: drive ticket field permlevels from template visibility
Template save syncs field permlevels (hidden -> 8, visible -> 0),
replacing the insert-time exemption dance. Tiers move to 7/8/9 to
stay clear of site-defined levels. get_one reads through the doc
with field-level read permissions applied.
2026-08-11 00:18:31 +05:30
Ritvik Sardana
99ae84ade7 fix: keep resolution date and redden the overdue leg 2026-08-11 00:15:23 +05:30
Ritvik Sardana
3f17dded78 refactor: variable names in TicketTimeline 2026-08-10 23:34:48 +05:30
Ritvik Sardana
422c39f6a1 fix: shorten Average to Avg. in home page widgets
Widget titles read Average while the values inside them already read
Avg., so the same metric was labelled two ways on one screen.
2026-08-10 21:23:26 +05:30
Ritvik Sardana
c45594a3b5 refactor: build agents_involved with get_user_info_for_avatar
The desk was resolving each agent through its user store. Use the standard
avatar helper so the payload carries name and image, matching how the rest
of the ticket APIs return users.
2026-08-10 21:23:15 +05:30
Ritvik Sardana
33635792e8 refactor: ticket_analytics api
- exclude SLA-paused time from the agent response gap so the metric card
  and the milestone badge measure the same interval
- drop the payload left over from the deleted SLA timeline and gap chart
  (exchanges node, leg_label, progress, first_response_target)
- include assignees in agents_involved, resolving sender emails to user ids
  so one person cannot appear twice
- return user ids only, the desk resolves names and avatars from its store
- move the resource into useTicket with a per-ticket cache, fetched by the
  tab rather than on every ticket open
- show an error state with retry instead of a permanently blank pane
2026-08-10 21:19:08 +05:30
Ritvik Sardana
016c5eb159 fix: isolate churn test from HD Team assignment rules
An HD Team insert leaks its auto-created assignment rule across test
transactions, breaking unrelated suites with 'Assignment Rule not
found'. The team change is now recorded directly as the Version diff
churn() reads. Also moves add_message and timeline_node helpers to
test_utils per repo convention.
2026-08-10 19:37:53 +05:30
Ritvik Sardana
b2cbc94357 feat: reload analytics on status or priority change 2026-08-10 18:58:55 +05:30
Ritvik Sardana
30f6b829a0 feat: show year on timeline date labels when it changes
Also removes the planning document from the repo.
2026-08-10 18:57:33 +05:30
Ritvik Sardana
8dcc83b732 feat: unified ticket timeline
Replaces the SLA stepper and response gap chart with a single event rail:
split breach legs with a deadline tick, overdue stretch ending at a Today
marker, reworked metric cards (agent/customer gaps, hold time) and a
trimmed conversation summary.
2026-08-10 18:23:45 +05:30
Sydney
3e516d7f9b refactor: drop hidden field warning from template save 2026-08-10 16:18:47 +05:30
Sydney
756e7034dc fix: leave custom field permlevels to the user 2026-08-10 15:50:38 +05:30
Sydney
58bfe3f4a6 fix: keep permlevel protection until explicitly lowered 2026-08-10 14:17:23 +05:30
Sydney
27ef110197 test: add template factory that replaces leftovers 2026-08-10 14:17:21 +05:30
RitvikSardana
041502c285 Merge pull request #3688 from frappe/pot_develop_2026-08-09
chore: update POT file
2026-08-10 14:12:58 +05:30
Sydney
24d38e58ca fix: scope permlevel release to unhide and template delete 2026-08-10 14:03:20 +05:30
Sydney
2271cc74fd test: move field permission helpers to test_utils 2026-08-10 13:47:03 +05:30
Sydney
7e940791a4 refactor: batch queries in field permission patch 2026-08-10 13:47:01 +05:30
Sydney
b30e808784 fix: drop permlevel when template field is no longer hidden 2026-08-10 13:46:53 +05:30
Sydney
1fa0e2463b feat: reserve permlevel 3 for system manager 2026-08-10 13:43:01 +05:30
Sydney
dedd99fed0 Merge remote-tracking branch 'upstream/develop' into fix/ticket-field-level-perms
# Conflicts:
#	helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.json
#	helpdesk/patches.txt
2026-08-10 13:14:21 +05:30
Sydney
46e6aa621f chore: shorten permlevel comments 2026-08-10 13:00:00 +05:30
Sydney
b24d7d686b test: cover creation response and customer link guard 2026-08-10 12:59:54 +05:30
Sydney
4fc624bf6f refactor: strip ticket creation response via read permissions 2026-08-10 12:59:48 +05:30
Sydney
fe0944c057 fix: validate customer link even when auto-set is off 2026-08-10 12:59:35 +05:30
frappe-pr-bot
712b630331 chore: update POT file 2026-08-09 10:59:25 +00:00
RitvikSardana
e17cb5601c Merge pull request #3680 from RitvikSardana/feat/bulk-actions2
feat(tickets): bulk edit and bulk assign from the selection banner
2026-08-07 21:33:59 +05:30
Ritvik Sardana
bf5a1bd83d fix(bulk-edit): match progress by task id, make status mandatory 2026-08-07 20:35:13 +05:30
Ritvik Sardana
852e58e62d refactor(tickets): surface Assign in the selection banner
Reply and Assign are the actions agents reach for on a selection, so they get
the banner buttons; Export, Edit and Delete move into the overflow menu.

Drops the icon scale classes: correcting one axis leaves the other wrong, and
correcting both distorts the glyph. Nothing CSS can do here.
2026-08-07 18:50:08 +05:30
Ritvik Sardana
f3c25b4524 feat(tickets): bulk edit and bulk assign from the selection banner
Adds two actions to the ticket list selection banner alongside the existing
bulk reply:

- Bulk Edit: sets a field on every selected ticket via the framework's
  `submit_cancel_or_update_docs`.
- Bulk Assign: assigns agents to every selected ticket via `add_multiple`.

Bulk Assign reuses the ticket view's own agent picker. `AssignTo` now treats a
missing ticket injection as standalone mode: it reports its selection through
v-model and leaves saving to the parent, so both call sites share one picker
instead of a second, worse one.

The banner shows Reply and Edit as buttons and keeps the rest in the overflow
menu; actions opt in with `inline`. Labels drop the redundant "Bulk" prefix
since the banner already states the selection count.

Also hardens the SLA cells to check `first_responded_on` / `resolution_date`
before the target date, matching `useSLA`, so a cleared target no longer
renders as a countdown.
2026-08-07 18:37:56 +05:30
Ritvik Sardana
5e097c4cb5 fix(list-view): fetch SLA fields for custom ticket views
Saved views send only their own column keys as `rows`, so `first_responded_on`
and `resolution_date` never reached the client. The First Response / Resolution
cells then fell through to the live-countdown branch, showing a ticking deadline
for tickets that had already been responded to or resolved.

Append the whole set of fields the SLA cells read, the way `sla` alone was
already being appended.
2026-08-07 18:37:13 +05:30
Sydney Gomes
2583ef1f90 Merge pull request #3677 from aerodeval/fix/email-recipient-popover-width
fix(email-select): truncate long recipient names in the popover
2026-08-06 13:08:41 +05:30
RitvikSardana
f71c556db8 Merge pull request #3679 from RitvikSardana/fix/ui-framework-plugin
fix: use framework ui plugin
2026-08-06 12:59:12 +05:30
RitvikSardana
587d6547bd chore: update package.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-06 12:39:08 +05:30
Ritvik Sardana
ae78b48f1b fix: remove framework ui installation script from dev command 2026-08-06 12:24:10 +05:30
Ritvik Sardana
a91e862358 fix: remove framework ui installation script 2026-08-06 12:18:13 +05:30
Ritvik Sardana
648f1f0c4d fix: use framework ui plugin 2026-08-06 12:14:49 +05:30
Sydney
dd22535ef8 fix: linter issues 2026-08-06 11:42:30 +05:30
Sydney Gomes
beb1de9545 Merge branch 'develop' into fix/email-recipient-popover-width 2026-08-06 01:12:29 +05:30
Sydney
0127ca4a5d fix(email-select): truncate long recipient names in the popover
A long contact name or address made the option row wider than the
popover's max-width, so the list scrolled sideways. Let the text column
shrink and truncate instead.
2026-08-06 01:04:48 +05:30
Sydney
5340d893e1 fix: key the availability toast on who made the change
Comparing the incoming value against the local one mistook an agent's own
change made in another tab for an admin change, because only the initiating
session applies the optimistic update. The event now carries changed_by, so
a client toasts only when someone else moved its status — which is also the
question the toast is actually asking.

Drops the pre-seed guard the value comparison needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 03:55:32 +05:30
Sydney
d528569268 fix: translate the status name in the availability toast
The picker renders status names through __(), so the toast that reports a
forced change should match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 03:44:30 +05:30
Sydney
3ff3c1cbef fix: move agents off an agent status when it is disabled
An agent left on a disabled status keeps one that is no longer offered.
The picker filters disabled statuses out, so their own menu renders empty
while everyone else still sees the retired status against their name.

HDAgentStatus.on_update now moves those agents to the Active status. They
are saved one at a time rather than bulk-updated, so HD Agent's controller
does the validation, the availability_changed_on stamp and the realtime
broadcast — the same path every other availability change takes, and no
change to the socket payload. helpdesk/patches/set_default_agent_statuses
already resets agents on an invalid status the same way.

Which Active status: get_active_status resolves ties by creation, the
frappe.db.get_value default. Not status_order — the shipped statuses are
10/20/30 and a new one defaults to 0, so it would promote any custom
status above "Active". Documented on the function.

The affected agent now gets a toast, since their menu changes under them.
An in-app notification waits on the notification refactor: HD Notification
is ticket-shaped (notification_type is a Select of Assignment/Mention/
Reaction, user_from is mandatory). Left as a TODO on the reset.

Also renames the doctype's `enable` field to `enabled`, matching HD Ticket
Status and HD Service Level Agreement — it was the odd one out in its own
app. Migrated with rename_field in post_model_sync, which needs the synced
field in meta and no-ops on sites installed after the rename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 03:41:16 +05:30
Sydney Gomes
9146da919c Merge pull request #3635 from aerodeval/refactor/agent-availability
refactor: agent availability through the document lifecycle
2026-08-05 03:18:22 +05:30
Sydney
c037dcf502 test: reset session user between HD Agent tests
frappe restores the session user per class, not per test, so the four
tests that call frappe.set_user leaked into every later one — the whole
class ran as test_user@test.com after the first of them.

Nothing fails today because the helpers save with ignore_permissions,
but test_manager_can_set_another_agents_availability calls add_roles(),
a permission-checked User.save(), under whatever session the previous
test left behind.

setUp/tearDown now restore Administrator, matching test_contact,
test_onboarding, test_tags, test_ticket_stats and test_agent_home.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 02:46:35 +05:30
Sydney Gomes
903abe0b16 Merge pull request #3638 from aerodeval/fix/agent-reply-email-threading
fix(email): give agent replies their own Message-Id so threads hold
2026-08-05 01:40:21 +05:30
Sydney
4d2afa9dbe refactor(email): name the send result, trim the comments around it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169cvSxbDNjSX57e12eGY9j
2026-08-05 01:24:42 +05:30
Sydney
974f01f3b6 fix(email): save a reply's Message-Id only once the mail has gone out
reply_via_agent saved the id before sending, so a reply that failed to send
kept an id the customer never received. bulk_reply logs failures instead of
raising, so that reply survived and the next reply answered it, putting an
In-Reply-To on the wire that resolves to nothing in the customer's mailbox.

Generate the id up front and pass it to sendmail as before, but write it to
the communication only after sendmail returns, and only if it queued
something - it returns nothing when every recipient is filtered out, which
is not a send either. A reply that never went out now keeps no id, so it can
never be answered. The row stays on the ticket so the agent still sees the
attempt.

A savepoint around each ticket was tried first and dropped: with HD Settings
instantly_send_email on, sendmail commits inside SendMailContext before the
SMTP attempt, which destroys the savepoint and makes the rollback throw,
hiding the real error.

Also reorder bulk_reply so nothing is written before everything is checked.
Permissions and get_doc ran per ticket inside the loop, so one bad id raised
out of the request and rolled back the replies that had already worked, and
attachments were linked ahead of the dedupe and the permission check.

Reported by Greptile on #3638.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 01:02:47 +05:30
RitvikSardana
f7aa34bda1 Merge pull request #3671 from RitvikSardana/telemetry/new-features
fix(telemetry): capture events for new features
2026-08-04 23:45:23 +05:30
Ritvik Sardana
efe0688b29 fix(telemetry): capture events for new features 2026-08-04 23:12:56 +05:30
RitvikSardana
04719048ea Merge pull request #3661 from RitvikSardana/fix/build-framework-ui
fix: install @framework/ui deps before dev and build
2026-08-04 22:57:57 +05:30
RitvikSardana
2e4699b6a6 Merge pull request #3668 from RitvikSardana/fix/snug-safari
fix: handle snug
2026-08-04 22:56:34 +05:30
Ritvik Sardana
4026cc3a8b fix: handle snug 2026-08-04 22:56:06 +05:30
Sydney
201f3187ae test(email): cover the email workflow, drop a duplicate test helper
Tickets raised by email are the common case and had no threading test.
Add one: the first agent reply answers the customer's mail, later replies
follow the reply before them. Add a second test for replies that are never
emailed, which must not store an id at all.

Drop the local get_ticket_communications helper. It duplicated
get_latest_ticket_communication and its docstring claimed the canonical
helper could not order results - that is wrong. frappe.get_all falls back
to the doctype sort meta, and Communication sorts by creation desc, so the
existing helper was always correct. Callers lose the trailing [-1].

Trim the comments in reply_via_agent and the test docstrings to plain
English. The PR archaeology they carried is in the commit messages already.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 22:01:04 +05:30
Sydney
c1a56258e0 refactor(email): carry the thread anchor on the Communication itself
get_last_threadable_communication returned a two-field dict of which only
the name was ever read, and the caller parked it in a local that stayed
live across ~75 lines and an early return, re-deriving its nullability in
both an if and a ternary.

Return just the name and assign it straight onto communication.in_reply_to
- the row frappe reads it back from anyway - then pass that field to
sendmail. The local, the branch and the ternary all go. Making the helper
private stops it reading as a variant of get_last_communication, which
differs from it in mechanism, return type and error handling.

No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:51:00 +05:30
Sydney
6bd8b705be fix(email): only store a Message-Id when the reply is actually sent
reply_via_agent stamped every Communication with a fresh Message-Id before
the early return for skip_email_workflow / enable_reply_email_via_agent. A
reply that was never emailed then qualified as a thread anchor, so the next
reply pointed In-Reply-To at an id that had reached no mailbox - failing in
exactly the same silent way as storing no id at all.

Gate the assignment on the same condition as the send, and name it
should_send_email, which also drops a duplicate HD Settings read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:16:16 +05:30
RitvikSardana
7d8f3e3e2c Merge pull request #3666 from RitvikSardana/fix/sr-line-height
fix: line-height saved reply
2026-08-04 16:58:11 +05:30
Ritvik Sardana
5db4bb9c83 fix: line-height saved reply 2026-08-04 16:55:32 +05:30
RitvikSardana
cbd4ceb48b Merge pull request #3663 from RitvikSardana/fix/email-rec
fix: email editor closing on selecting recipients
2026-08-04 14:02:30 +05:30
Ritvik Sardana
d289faead6 fix: email editor closing on selecting recipients 2026-08-04 14:01:31 +05:30
RitvikSardana
75f880efca Merge pull request #3662 from RitvikSardana/fix/cp-align
fix(cp): vertical axis alignment
2026-08-04 12:31:01 +05:30
Ritvik Sardana
5d16951788 fix(cp): vertical axis alignment 2026-08-04 12:22:28 +05:30
Ritvik Sardana
d763279734 fix: dedupe reka-ui so linked @framework/ui resolves it 2026-08-04 00:36:11 +05:30
Ritvik Sardana
f021dcc365 refactor: import Link from the @framework/ui package entry 2026-08-04 00:13:29 +05:30
Sydney
88348f3231 refactor: single predicate for "availability changed"
The controller was asking "did availability change?" in three different idioms:
a hand-rolled old_doc comparison in before_save, has_value_changed in validate
and publish, and has_value_changed plus a get_doc_before_save null check in the
telemetry capture. Collapse them onto one named predicate.

publish_availability_update previously fired on insert while the telemetry
capture did not, justified as letting clients learn a new agent's presence
without a refetch. That justification does not hold: liveStatuses is only ever
read as an overlay on agents already loaded from the list resource
(AssignTo.vue liveAvailability), and nothing iterates it, so an entry for an
agent the client has not fetched renders nowhere. Dropping the asymmetry lets
both side effects share availability_changed() and removes the comment that
existed only to explain the difference.

validate_availability deliberately keeps the raw has_value_changed — a new
agent must still be rejected for a disabled status — and now says so.

Tests: hoist the private helpers to the top of the class rather than leaving
one mid-file between tests, and collapse three copies of the client.set_value
payload onto a single helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuJruCwPQFwBA3HyXogjD7
2026-08-04 00:10:25 +05:30
Ritvik Sardana
50f15f6f83 fix: install @framework/ui deps before dev and build
`@framework/ui` is a `link:` dependency, so yarn never installs its own
dependencies and the build fails on missing imports. Run `yarn install`
in `frappe/ui` first, skipping it when `.yarn-integrity` shows a complete
install is already there.
2026-08-04 00:02:01 +05:30
Sydney
8c4914c2eb test: drop telemetry assertions for agent availability
The capture_event calls are fire-and-forget analytics; asserting on a mock
adds little over the guard being readable in the controller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuJruCwPQFwBA3HyXogjD7
2026-08-03 23:50:52 +05:30
Sydney
c3c1afa1cd feat: capture telemetry on agent availability change
Adds an agent_availability_updated telemetry event, following the existing
capture_update_telemetry_events shape in HD Ticket. Because it lives on the
controller it covers every write path, not just the desk UI.

Guarded on get_doc_before_save() so inserts are excluded: has_value_changed
returns True when there is no previous doc, so without it every agent created
would be counted as a status change. The realtime broadcast deliberately keeps
firing on insert — clients learn a new agent's presence without a refetch.

Also drops the unused trailing `return` from sent_invites, per review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuJruCwPQFwBA3HyXogjD7
2026-08-03 19:46:41 +05:30
Ritvik Sardana
999fca4bda feat: response gap chart
Line chart of agent response gaps over the ticket, business-hours
durations on round tick steps. Espresso oklch tokens crash echarts'
hover restyle, so the series disables the emphasis state.
2026-08-03 19:39:34 +05:30
Ritvik Sardana
3c0615ea4a feat: SLA timeline stepper
Figma-styled stepper for the analytics tab: donut milestones for
created, first response, hold, exchanges and resolution; dashed blue
SLA-paused leg with duration; progress gradients for pending and
breached legs; hover badges with SLA verdicts.
2026-08-03 19:39:33 +05:30
Ritvik Sardana
38a2289995 feat: analytics tab with metrics and conversation summary
New Analytics tab on the agent ticket view: metric cards (avg agent
response gap, time to resolution, longest agent silence) and a
conversation summary with churn flags. Restores the hash tab on soft
navigation via an immediate watcher in useActiveTabManager.
2026-08-03 19:39:16 +05:30
Ritvik Sardana
394a77e611 feat: per-ticket analytics API
get_ticket_analytics returns SLA timeline nodes, response-gap series,
metrics and conversation summary for the agent-facing Analytics tab.
Business-hours math via the ticket SLA calendar, wall-clock fallback
without one; churn from Version diffs and ToDo assignees.
2026-08-03 19:38:52 +05:30
RitvikSardana
cc0758268e Merge pull request #3639 from RitvikSardana/refactor/default-sla
refactor: allow tickets to not have any SLA
2026-08-03 18:13:14 +05:30
Ritvik Sardana
e80ee038a8 refactor: rename the shipped sla to "Standard" 2026-08-03 17:26:55 +05:30
Ritvik Sardana
c38af44f77 fix: keep sla fulfilled ratio within bounds, index ticket sla 2026-08-03 17:18:31 +05:30
Ritvik Sardana
321c3d55ee fix: clear fields when no sla is matched 2026-08-03 15:05:23 +05:30
Ritvik Sardana
d9a7a6975c fix: sla analytics should only include tickets with sla 2026-08-03 14:53:49 +05:30
Ritvik Sardana
ed0942d768 fix: list views and customer ticket view according to NO SLA 2026-08-03 14:23:24 +05:30
Ritvik Sardana
c06bf4cf65 fix: keep pause anchor when a paused ticket detaches 2026-08-03 13:34:05 +05:30
Ritvik Sardana
27e738bcc7 fix: remove on_hold_since if SLA is detached 2026-08-03 12:58:18 +05:30
Ritvik Sardana
9e33bdc029 fix: clear resolution_time if SLA is not attached 2026-08-03 12:42:41 +05:30
RitvikSardana
b2574a7388 Merge pull request #3640 from frappe/pot_develop_2026-08-02
chore: update POT file
2026-08-03 11:34:26 +05:30
frappe-pr-bot
102dc88eef chore: update POT file 2026-08-02 11:39:52 +00:00
Ritvik Sardana
b75761f044 refactor: allow tickets to not have any SLA
Removes three limitations that forced an SLA onto every ticket:

- every priority had to be present in every SLA
- exactly one SLA had to be marked default, and it could not be
  disabled or deleted
- every ticket had to carry an SLA

A policy now applies only when its condition matches and it includes the
ticket's priority. The Default SLA is optional, and is considered last.
When no policy applies, the ticket detaches: sla, response_by,
resolution_by and agreement_status are cleared, while the clock start and
any recorded breach are kept so flapping a condition buys no time.

Adds a rank field so the winner between two matching policies is explicit
rather than creation order. Removing a priority or unticking the Default
SLA now warns about the tickets that will detach, instead of blocking.

resolution_date is stamped by the ticket rather than the SLA, since a
ticket knows when it was resolved with or without a policy.
2026-08-02 04:03:09 +05:30
Sydney
67cd1dc16d fix(email): give agent replies their own Message-Id so threads hold
Every agent reply shipped without an In-Reply-To header, so each one landed
as a separate thread in the customer's mail client.

frappe#32651 made In-Reply-To resolve through Communication.message_id.
helpdesk#2308, merged 77 minutes later, removed the only code that wrote
that column - correctly, since it was copying the parent's Message-Id onto
every reply in the ticket. Together they left the column permanently empty,
so no reply could ever anchor the next one and the chain died after a single
hop off an inbound customer email.

Mint a fresh id per reply, store it on the Communication, and send that same
value, which is what communication.email._make already does for
desk-composed mail. Pick the parent from the newest communication that
actually has an id, so a portal reply - which sends no email and therefore
has none - doesn't break the chain for every reply after it.

Measured on an affected site before the fix: 1,752 of 1,911 agent replies in
one week carried no threading header at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:38:16 +05:30
RitvikSardana
d51b186dc5 Merge pull request #3637 from RitvikSardana/fix/frequent-replies-label
fix(command-palette): rename saved reply group to Frequent replies
2026-07-31 03:53:33 +05:30
Ritvik Sardana
d1c2c06e65 fix(command-palette): rename saved reply group to Frequent replies
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 03:44:19 +05:30
RitvikSardana
f452618612 Merge pull request #3636 from RitvikSardana/fix/line-height
fix: line height in feedback component
2026-07-31 03:03:25 +05:30
Ritvik Sardana
027f36d790 fix: line height in feedback component 2026-07-31 03:02:31 +05:30
RitvikSardana
aaf4d85521 Merge pull request #3626 from RitvikSardana/feat/command-palette
feat(command-palette): contextual Cmd+K palette for the agent portal
2026-07-31 02:09:41 +05:30
Ritvik Sardana
4d04820aa9 chore: code cleanup 2026-07-31 01:58:26 +05:30
Ritvik Sardana
70dc3edb70 fix: update readme 2026-07-31 01:51:58 +05:30
Ritvik Sardana
a29ddb0872 chore: remove animations 2026-07-31 01:46:28 +05:30
Ritvik Sardana
03feb833c2 fix(command-palette): keep rows on screen through the close fade
Emptying groups the moment isOpen flips false flashed "No commands found"
during the exit animation on an outside click.
2026-07-31 01:21:41 +05:30
Sydney
36c04bdc1f refactor: route agent availability through the document lifecycle
set_my_availability wrote with frappe.db.set_value, which skips the HD Agent
controller entirely. That put three invariants in one API function and left
every other write path to HD Agent.availability broken:

- availability_changed_on was stamped by hand in the API, duplicating
  HDAgent.before_save, which never ran
- the "status must be enabled" check lived in the API, so the desk form, REST
  and frappe.client.set_value could all write a disabled status silently
- the agent_availability_updated broadcast fired only from that API, so a
  status changed from the desk form never reached connected clients

Move all three onto the controller — validate() rejects a disabled or unknown
status (guarded on has_value_changed so an agent already on a since-disabled
status can still save unrelated fields), before_save keeps the timestamp, and
on_update publishes the broadcast. The endpoint then has nothing left to do,
so the store calls frappe.client.set_value directly, matching how Settings
already writes HD Agent.is_active.

Dropping the endpoint also drops its agent_only + get_agent_name() self-scoping,
and the Agent role has blanket write on HD Agent, so add a has_permission hook:
an agent may only modify their own record, reads stay open for presence dots
and assignment pickers, managers are unrestricted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuJruCwPQFwBA3HyXogjD7
2026-07-31 01:16:50 +05:30
Ritvik Sardana
08f175634b fix(command-palette): polish context chip, tag flow, view icons and highlight tracking
- render the context chip with frappe-ui Badge, id dark and subject light
- pop back to the parent level after toggling a tag
- resolve Switch view icons with the sidebar's getIcon resolver
- track the highlighted row by identity so async rebuilds can't jump it
2026-07-31 01:06:19 +05:30
Sydney Gomes
820f32a875 Merge pull request #3634 from aerodeval/fix/onboarding-questionnaire-polish
fix: onboarding questionnaire surface, button sizing and team-size default
2026-07-31 00:23:15 +05:30
Sydney
535db2216d feat: report an unanswered team size as prefer_not_to_say
Team size is the only optional answer, so skipping it dropped the key
entirely and the persona event came through incomplete. Default it at
submitPersona, the one point both telemetry and routing read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 19:03:44 +05:30
Sydney
5d8b42a381 fix: put the onboarding screen on the base surface
surface-gray-1 read as an off-white tint behind the questionnaire.
surface-base is pure white in light mode and still flips with the theme,
which bg-white would not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 19:03:17 +05:30
Sydney
83f25c9462 refactor: size the questionnaire Next button with the size prop
size="sm" already yields h-7 and the theme's button radius, so the !h-7
and !rounded-lg overrides were restating the default with !important.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 19:02:52 +05:30
Ritvik Sardana
d0441a3409 chore(command-palette): tighten comments
Every comment keeps only the constraint it protects; essays, product
citations and metric rationales go. ponytail: markers stay.
2026-07-30 18:21:54 +05:30
Ritvik Sardana
2295a1328a fix(auth): drop the unused in-SPA login resource
Nothing calls it: the router guard exits logged-out users to the
server-rendered login page, and returning is a full page load. Removing
it also closes the review concern that an SPA login could leave
module-scope userStorage keys bound to the pre-login identity — that
path now provably does not exist.
2026-07-30 18:00:03 +05:30
Ritvik Sardana
534a5772a9 feat(command-palette): saved reply promotion polish and right-arrow drill
- Promoted saved replies get their own "Most used" section at root; only
  the generic "Ticket" group stays headerless under the context chip
- The composer's saved-reply modal now feeds the same usage record, so
  promotion reflects how an agent actually works, not just palette picks
- ArrowRight drills into the highlighted row's children when the caret
  is at the input's end, mirroring Backspace-back; inert elsewhere
- Group containers drop their margin so rows keep one 36px rhythm across
  headerless group boundaries
2026-07-30 17:55:54 +05:30
RitvikSardana
0975880eba Merge pull request #3542 from RitvikSardana/fix/embed-private-inline-images
fix: drop img src after setting embed so inline images render in emails
2026-07-30 16:19:29 +05:30
Ritvik Sardana
c519ec2a1e fix(command-palette): let a command's own focus survive the close
Reka's close auto-focus restored the pre-open element on unmount, which
ran after a saved reply had already focused the composer and yanked focus
away. Prevent it and route through restoreFocus, which only restores when
the closing command left focus nowhere.
2026-07-30 15:56:49 +05:30
Ritvik Sardana
4348e36a75 fix(command-palette): drop the redundant already-set toast
The drill-down already ticks the current value, so picking it again
closes quietly instead of toasting.
2026-07-30 14:10:20 +05:30
Ritvik Sardana
8e7df85e57 fix(command-palette): polish flat row matching and search fallback styling
- Hidden flat rows (quick-set, saved replies) surface only on a
  substring-or-better match, so a scattered hit like "sett" no longer
  un-hides every option row above an exact match on Settings
- "(advanced search)" renders inline in muted ink via a titleSuffix field
2026-07-30 13:37:53 +05:30
Ritvik Sardana
91d35ff15f Merge branch 'develop' of https://github.com/frappe/helpdesk into feat/command-palette 2026-07-29 17:22:07 +05:30
Ritvik Sardana
800f7f5361 chore: code cleanup 2026-07-29 17:17:38 +05:30
Ritvik Sardana
340cd7a6df fix(command-palette): row and drill-down polish
- Fixed row height (h-9) so rows with shortcut hints match plain rows
- Availability rows show status color dots (reuses ticket status parseColor)
  and await the statuses fetch, fixing the empty drill-down on a cold store
- "Set availability" matches "my status", presence words and live status names
- Reset palette state on open instead of close, so the dismissed context chip
  no longer flashes back during the close animation
- Single-group drill-downs drop their header, which only repeated the breadcrumb
- Team and type rows lose their repeated static icons; agent rows show avatars
- "Recent" group renamed to "Recent tickets"
2026-07-29 17:08:07 +05:30
Ritvik Sardana
8e47ca01a2 fix(command-palette): derive filter keywords from live status and priority options 2026-07-29 16:30:23 +05:30
Ritvik Sardana
a3cc232da8 fix(command-palette): label the search fallback as advanced search 2026-07-29 13:24:41 +05:30
Ritvik Sardana
deb74eaf87 refactor(command-palette): footer hints use ShortcutKey 2026-07-29 13:12:02 +05:30
Ritvik Sardana
e0e3413209 fix(command-palette): tick the active view in switch view 2026-07-29 12:39:57 +05:30
Ritvik Sardana
4c8aad4ebb fix(command-palette): chip shows subject again, capped at 30% width 2026-07-29 02:29:51 +05:30
Ritvik Sardana
fdf98c1c21 fix(command-palette): move the context chip back above the input 2026-07-29 02:28:58 +05:30
Ritvik Sardana
813a717ce9 fix(command-palette): tighten chip spacing 2026-07-29 02:26:53 +05:30
Ritvik Sardana
2e3cd38034 fix(command-palette): chip shows only the ticket id 2026-07-29 02:22:06 +05:30
Ritvik Sardana
438a6f03b1 fix(command-palette): inline the context chip with the input 2026-07-29 02:20:50 +05:30
RitvikSardana
563d22a209 Merge pull request #3631 from RitvikSardana/refactor/feedback-in-sidebar
fix: move ticket's feedback to sidepanel
2026-07-29 02:05:24 +05:30
Ritvik Sardana
6947d41101 docs(command-palette): add a how-it-works walkthrough 2026-07-29 02:01:20 +05:30
Ritvik Sardana
2e1f38f4b4 fix: add feedback secton to right 2026-07-29 01:53:01 +05:30
Ritvik Sardana
553761a8a7 fix(command-palette): reply and comment rows use the composer's icons 2026-07-28 22:40:13 +05:30
Ritvik Sardana
97848b7018 refactor(command-palette): trim recents to three and note stack shape 2026-07-28 22:37:23 +05:30
Ritvik Sardana
e6a4121918 refactor(command-palette): route-to-commands lookup for root rows 2026-07-28 22:36:17 +05:30
Ritvik Sardana
13352db959 fix(command-palette): tick the current availability 2026-07-28 22:08:59 +05:30
Ritvik Sardana
cfaa913877 fix(command-palette): optimistic tag ticks, run guard, recents in context 2026-07-28 20:11:56 +05:30
Ritvik Sardana
c66928f4f0 docs(command-palette): cover keepOpen rows and fallback behaviour 2026-07-28 20:01:05 +05:30
Ritvik Sardana
b16d89862d fix(command-palette): tighten search fallback copy 2026-07-28 19:58:15 +05:30
Ritvik Sardana
a0741af53c fix(command-palette): fallback and create polish
- Create ticket "…" now actually pre-fills the subject on TicketNew
- no Create ticket fallback for #-queries, that's a ticket lookup
- drop the Create group header, "New ticket" already says it
2026-07-28 19:47:40 +05:30
Ritvik Sardana
30bca0279e fix(command-palette): chip hugs its content instead of stretching full width 2026-07-28 19:42:27 +05:30
Ritvik Sardana
1f7d82a0bd feat(command-palette): fade between levels
Drilling in or out, and removing the chip, now get the same brief fade
the chip animation already set the tone for. Typing never re-keys the
list, so filtering stays instant. Honors prefers-reduced-motion.
2026-07-28 19:37:58 +05:30
Ritvik Sardana
1b31f2ccc9 feat(command-palette): Linear-style tag toggling
The tag drill-down listed only unapplied tags and closed after adding
one. It now lists every tag with the applied ones ticked; Enter toggles
add/remove and the palette stays open, so several tags change in one
visit. keepOpen commands rebuild their level in place after performing.

Row renamed "Add tag" -> "Tags", since it removes now too.
2026-07-28 19:35:20 +05:30
Ritvik Sardana
65229b7d29 fix(command-palette): give Change type its own icon
It shared the tag glyph with Add tag; type rows now use the shapes icon.
2026-07-28 19:32:47 +05:30
Ritvik Sardana
0fbdbc5a86 fix(command-palette): drop the remove glyph from the context chip
esc's footer label already reads "Remove context" while the chip is up.
2026-07-28 19:32:02 +05:30
Ritvik Sardana
bfc500dfe7 fix(ticket): shorten copy toasts to match the palette
"Ticket #X copied to clipboard" and "Ticket URL copied to clipboard"
now read "… copied", the same copy the command palette uses.
2026-07-28 19:30:59 +05:30
Ritvik Sardana
86a51d1684 fix(command-palette): drop the Search-elsewhere heading and all-caps headers
The fallback rows explain themselves; group headers now render in
sentence case instead of uppercase.
2026-07-28 19:30:44 +05:30
Ritvik Sardana
0eac87ba25 fix(command-palette): make the context chip inert, removal is backspace-only
Clicking the chip removed the context, which is far too easy to hit for
an action that strips the ticket's own commands out of the palette.
2026-07-28 19:20:08 +05:30
Ritvik Sardana
85f08bf062 fix(command-palette): show the global root after the context chip is removed
Removing the chip on an empty query returned the scoped list — now empty —
instead of falling through to recents, navigation and settings.
2026-07-28 19:17:56 +05:30
Ritvik Sardana
837f6d07fd Merge remote-tracking branch 'upstream/develop' into feat/command-palette 2026-07-28 19:01:43 +05:30
RitvikSardana
3f16756d35 Merge pull request #3629 from RitvikSardana/perf/search-permission-scale
perf(search): keep permission filtering bounded at any ticket count
2026-07-28 18:55:17 +05:30
Ritvik Sardana
6295521022 refactor(perf): grant the search test user HD Customer
A role-less user has no doctype-level read on HD Ticket, so get_list
raises PermissionError on a clean CI site. Real searching users are
agents or HD Customers; the test now models the latter.
2026-07-28 17:04:32 +05:30
Ritvik Sardana
fb11a89635 refactor(perf): trim comments, replace map() with a comprehension
The map() call tripped frappe's no-functional-code semgrep rule.
2026-07-28 16:47:55 +05:30
Ritvik Sardana
4a8bdb61af refactor(perf): bind facet permission list via json_each, not 3N placeholders
get_filter_options pasted the accessible-ticket list into the query three
times (name / reference_name / reference_ticket each carry the ticket for
a different row type), binding 3N variables — so it hit SQLite's
bound-variable ceiling at a third of the ticket count the search query
does.

The list now binds as one JSON string per column: 3 variables total,
regardless of site size. Same rows match, the ceiling is gone.
2026-07-28 16:47:24 +05:30
Ritvik Sardana
292269e4a1 refactor(perf): bound the search permission prefilter, post-filter past the cap
frappe.get_list("HD Ticket", pluck="name") ran unbounded on every search
call, feeding a SQLite IN (...): linear cost per keystroke (108ms at 200k
tickets) and a hard OperationalError past the bound-variable ceiling,
which core's except sqlite3.Error turns into silently empty results.

get_search_filters now probes at most PREFILTER_LIMIT + 1 names. Under
the limit the exact prefilter binds as before; over it, the prefilter is
skipped and _drop_unpermitted permission-checks the returned results
(<=100) through one get_list, which applies every layer including User
Permissions. Rows without a resolvable ticket fail closed; Communication
rows resolve via reference_name.
2026-07-28 16:46:44 +05:30
Sydney
253623c263 fix: stop mail loops from bounces and autoresponders
Inbound mail was only screened with the X-Auto-Generated header, which
only helpdesk's own acks set. A real bounce or out-of-office therefore
reached ticket creation and could start three separate loops: the ack in
HD Ticket.after_insert replies to raised_by (the address that just
bounced), threading onto a portal ticket makes frappe CC the parent doc's
owner on every inbound mail, and an account with enable_auto_reply
answers mailer-daemon directly.

Detect machine-generated mail by the standard markers instead: RFC 3834
Auto-Submitted, the RFC 3464 multipart/report delivery-status type, and
the RFC 5321 null return-path. Matches are routed to handle_bad_emails so
they land in Unhandled Email rather than disappearing -- the fetch has
already marked them seen either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:31:09 +05:30
Ritvik Sardana
5b3b789726 docs(command-palette): mark §14 shipped on perf/search-permission-scale 2026-07-28 14:29:56 +05:30
Ritvik Sardana
8155afaa4e refactor(ticket): move feedback from timeline to sidebar
Feedback no longer renders as a timeline entry on desktop. It now shows as
its own collapsible section under Ticket SLA, with Rating / Feedback /
Comment rows matching the customer portal labels.

Mobile keeps feedback in its activity list.
2026-07-28 13:54:39 +05:30
Ritvik Sardana
095be4dd83 refactor(command-palette): order Add comment above Reply to ticket
Source order is the tie-breaker on an empty query, so this is the order
an agent sees when they open the palette on a ticket.

"Saved Replies" moves with the reply row rather than staying put — the
two belong together, and leaving it under "Add comment" would have put a
reply action below the comment it is not related to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:46:41 +05:30
Ritvik Sardana
85a0e7f9ce refactor(command-palette): name the saved-reply rows "Saved Replies"
Drops the verb entirely. The row opens a category, the same shape as the
existing `Settings` row, and it is what the product calls the feature in
the settings tab and the doctype — so there is no invented verb to keep
in sync. "Reply with saved reply" stuttered and "Insert" read as jargon.

Root rows lose the title prefix: the reply's own name is the title and
"Saved Reply" moves to the subtitle, muted and right-aligned the way
search rows already carry "Open · #0484". A prefix spent the first
characters on chrome before the word the agent had just typed.

Two things the checks caught while renaming, both of which would have
shipped silently:

- "reply" is not a substring of "replies", so the plural title was
  unreachable by the obvious singular query — the scorer found nothing at
  all. It is now carried in `keywords`.
- My stated reason for the subtitle was wrong. A title prefix only buries
  the category when the category hides its distinguishing word mid-title
  ("Insert saved reply"). Leading with "Saved" means both rows get a
  prefix match and `CONTEXT_WEIGHT` decides, so "Saved Reply: X" would
  have ranked fine too. The subtitle is a readability choice, not a
  ranking fix, and the check now says so.

The invariant worth keeping is the general one: a category title must not
bury its own distinguishing word mid-title, because prefix (1000) against
word-boundary (~950) is wider than 1.2 vs 1.15 of weight can close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:32:00 +05:30
Ritvik Sardana
1f8978e61e refactor(command-palette): rename saved-reply rows to "Insert"
"Reply with saved reply" stuttered. "Insert saved reply" also says what
actually happens — the reply lands in the composer and the agent still
has to send it, which was the whole point of not sending on select. It is
the category-standard verb too (Front, Help Scout).

Root rows become "Insert: Refund policy", matching the parent's verb.
That pairing is not cosmetic: scoring the alternatives showed the prefix
decides the hierarchy.

- "Saved reply: X" beats the parent on any "saved" query, because a
  leading-word match scores 1000 against the parent's ~950 word-boundary
  match, and FLAT_OPTION_WEIGHT 1.15 vs CONTEXT_WEIGHT 1.2 cannot close
  a 50-point gap. One saved reply would bury the drill-down.
- "Reply: X" under an "Insert" parent inverts it the same way on "reply".
- Sharing the parent's verb keeps the parent ahead on "insert", "saved"
  and "saved reply", while "refund" still reaches the reply directly.

Side benefit: typing "reply" used to surface three rows from the reply
family at once. Now "Reply to ticket" leads it alone.

`fuzzyScore.check.ts` locks the ordering, and asserts the rejected
"Saved reply: X" pairing really does invert it, so the reason the titles
are worded this way survives the next rename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:24:42 +05:30
Ritvik Sardana
aafbd91dc4 docs(command-palette): record the shipped items in the plan
Everything in "What can be done now" is done. The section is now a
record of what was built and what turned out differently, and the
"later" list is unchanged: §1 undo, §6 selection, §7 and §15 behind
Custom Side Panel, §13 SLA, §14 search cost.

Two dependencies are discharged: §9 landed before §5's root rows and
before any §7 work, so content-shaped words at root now rank above search
hits instead of replacing them. Custom Side Panel is the only hard
dependency left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:07:55 +05:30
Ritvik Sardana
5e6d05da53 docs(command-palette): trim the README from 380 lines to 95
It had become a liability rather than documentation. Two of its claims
were cited by reviewers *instead of* reading the code:

- that the palette is not rendered in the customer portal — it was not
  actually guarded until recently
- that `title_only` is a title-search fast path — it narrows the SELECT
  clause, not the MATCH, which is a materially different thing and the
  reason a body phrase can return "No results"

Both are now stated correctly, along with the pinned fallback that makes
the second survivable.

Keeps the `Command` shape, the ranking rules including scope-ranks-not-
gates, and the three non-obvious decisions: module-scope singleton state,
frappe-ui's `useShortcut` over the local one, and publishing list and
composer state module-scope rather than routing it through the URL.

Dropped: the branch narrative, the file-by-file line counts (stale the
moment anything changes), the environment workaround, and the hardcoded
`bench --site` commands. Those belong in a PR description. The open-items
list is superseded by `CP-improvements-plan.md`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:06:43 +05:30
Ritvik Sardana
0e80f7bc36 fix(translation): accept an array of replacements
~50 call sites pass replacements as one array — `__("{0} d", [days])`.
That reads as a single argument, so `{1}` onwards silently kept its
placeholder and `{0}` rendered the whole comma-joined array. Single
substitutions worked by accident, which is why it went unnoticed.

Accepted as an overload rather than rewritten at every call site, and
numbers are now stringified rather than assumed to be strings. Drops the
repo's `tsc` error count from 952 to 709.

Also clears the palette's own remaining type errors, all of the same
shape — an index or optional field used without a guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:06:01 +05:30
Ritvik Sardana
daea3045d8 feat(command-palette): polish pass
Ten small things, none of which change what the palette does:

- **Matched text is bold again.** `stripTags` was throwing away the
  `<mark>` runs the FTS index already computed, so search rows never
  showed *why* they matched. Rendered as text nodes, so no `v-html` and
  no sanitiser — ticket content cannot inject markup.
- **Results go stale, not blank.** `onQueryChange` cleared synchronously
  then debounced 200ms, so the list emptied on every keystroke and the
  panel resized under the cursor. Old rows now stay until new ones land;
  the token still invalidates anything in flight.
- **Loading is visible mid-list.** It only surfaced on an empty list, so
  a drill-down on a cold store showed 300ms of nothing. A 2px
  indeterminate bar sits over the input's border, so nothing shifts.
- **Group headers read as structure**, `text-xs uppercase` instead of
  `text-sm` in a different colour.
- **Per-level empty state.** A drilled-in level with nothing left showed
  the root's "No commands found", which is false.
- **Focus restore.** There is no `DialogTrigger`, so closing dropped a
  keyboard user on `<body>`. Restored only when the close left focus
  nowhere, since a command may claim it — the saved-reply rows hand focus
  to the composer, and that must win.
- **Home/End and Ctrl+N/P.**
- **640px**, up from 560, which truncated subjects around 40 characters.
- **"Keyboard shortcuts" row**, since the palette is the discovery
  surface for the shortcut system and `Mod+/` was undiscoverable from it.
  Its open flag moves module-scope alongside the other shared modals.
- Reduced-motion covers the new loading bar.

`titleRuns` is covered in `fuzzyScore.check.ts`: run splitting, adjacent
marks leaving no empty run, and other tags coming out inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:03:49 +05:30
Ritvik Sardana
6b0cd3142b feat(command-palette): saved replies, ordered by the agent's own usage
The gap that decided whether this is a shortcut or a workflow. Composing
replies is the bulk of an agent's day and the palette did not touch it —
`Reply to ticket` opened an empty box and you typed the whole message.

"Reply with saved reply" drills into the replies, renders the picked one
against the ticket and opens the composer pre-filled. Never
send-on-select: the agent has to see what goes to a customer.

Ordering is frequency, per agent, and it applies on an **empty query
only** — once you type, `scoreCommand` decides and usage survives as the
tie-breaker, because `groupCommands` falls back to source order at equal
score. Typing "refund" must return the refund reply, never the most-used
reply that happens to fuzzy-match it. `sort` being stable makes cold
start `modified desc` for an agent with no history.

The top replies also get root-level rows, earned rather than guessed: a
reply is promoted only once this agent has used it 5 times, top 3. An
agent with long-tail habits accumulates nothing and sees no root rows,
which is the right outcome — it degrades to nothing rather than to noise.
Titles are stored with the counts, so a root row costs no fetch and
cannot block the root build. Rows carry `hideWhenEmpty`, composed titles
("Reply: Refund policy", since a bare title reads as a ticket) and
`FLAT_OPTION_WEIGHT`, so they rank under the drill-down parent. Safe at
root only because scope now ranks instead of gating.

These stay a drill-down rather than going fully flat: cardinality is
unbounded and the names are pure content, so they would collide with
ticket search on nearly every useful word. Inside the level the query
filters replies only — a scoped search, which is the correct shape here.

`replyComposer.ts` publishes the mounted composer's insert action module
scope, the same way `listViewFilters.ts` does for the list: the palette
lives in AppSidebar and cannot inject from the ticket page. An
already-open composer is left open rather than toggled, which would
discard a draft.

`saved_reply_applied` now carries `source` on both surfaces.
Ranking rules live in `savedReplyRanking.ts`, import-free, and are
covered by `savedReplyRanking.check.ts` — threshold boundaries, sort
stability, no-mutation, and losing a row when a reply is deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:57:27 +05:30
Ritvik Sardana
fe88c1f418 fix(command-palette): scope recent tickets to the signed-in agent
`hd_recent_tickets` was a bare key and `logout()` cleared it, so a shift
handover on a shared support machine handed the previous agent's ticket
**subjects** — which routinely carry customer names — to whoever logged
in next.

`userStorage.ts` suffixes the key with the session user id. It reads the
cookie rather than the auth store because this runs at module load,
before pinia is installed, and the key has to be right on the first read.
Saved-reply usage counts will want the same helper.

Also removes the two now-unscoped keys from disk on load, since scoping
only prevents the next leak, not the one already stored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:50:22 +05:30
Ritvik Sardana
cc1a262757 refactor(command-palette): delete the recent-commands MRU
A deletion, not a fix. It was broken four ways in four lines, over a
~15-command corpus where an MRU has no job:

- every leaf run was tracked, ticket-search ids included
- the window was 3 slots, so three ticket opens emptied it permanently
- ids resolved only against the static list, so misses were dropped
- drill-down commands never entered it, excluding the five most-used
  actions

Recent **tickets** stay — an agent bounces between ~5 a session and it is
the only zero-typing action in the product. The ticket currently open no
longer lists itself as somewhere to go next.

Buys back three rows for the polish batch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:49:00 +05:30
Ritvik Sardana
1f6d319959 refactor(command-palette): derive the broadcast field label from meta
`FIELD_LABELS` was a hardcoded copy of `getField(fieldname).label`, so a
site that relabels Team to "Squad" would have had the palette announce
"Team" over the socket while its own UI said "Squad" — the exact drift
the upcoming Custom Side Panel work makes reachable.

`getMeta` is safe from module scope and cached per doctype, so this
shares the fetch `tiptap-extensions.ts` already makes.

Row titles stay literal on purpose: `getField` returns null until meta
lands, so deriving them would flash "Change agent_group" on a cold load.
That belongs with the rest of §15, where which fields exist and whether
they are editable get designed together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:47:23 +05:30
Ritvik Sardana
3ab9ce57a4 fix(command-palette): restore listbox roles and unbind keys from the input
Two accessibility defects, both regressions from dropping headless-ui
`Combobox` — which supplied for free what was never written back.

ARIA (§3): the list was plain `<div>`s, so a screen-reader user could
arrow ten times and hear nothing. Adds `role="combobox"` on the input
with `aria-controls` and `aria-activedescendant`, `role="listbox"` on the
list, `role="group"` per section, `role="option"` with `aria-selected`
per row, and a polite live region for the result count. Rows now carry
their flat index, which supplies the stable option id and lets
highlighting compare indexes instead of object identity — a "Recent" row
is a clone sharing its original's id.

Keyboard (§4): `@keydown` was bound to the `<input>`, but Reka's focus
trap lets Tab reach the context chip and the back button, where arrows
and Enter did nothing until the user found Shift+Tab. Bound to
`DialogContent` instead, with Enter on a focused button left alone so it
does not fire both the click and the highlighted command.

Helpdesk sells into procurement that gates on this, so it is commercial
as well as right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:46:10 +05:30
Ritvik Sardana
1630b3f4fa feat(command-palette): telemetry for opens, runs and assign source
There was not one telemetry call in the palette, so every ranking and
cut argument was taste against taste.

- `command_palette_opened`, carrying the route it was opened from
- `command_palette_command_run`, carrying command id, query length and
  drill-down depth: `runs ÷ opens` below 0.5 means agents open, miss and
  press Esc, and a long query or non-zero depth on a common action means
  the ranking is wrong
- `source` on `ticket_assigned` in both the palette and the popover, so
  the two land in one funnel and substitution is distinguishable from
  addition

Cmd+K now goes through `openPalette()`/`closePalette()` instead of
poking `isOpen` directly, or the open rate would have missed the path
most agents use.

Fixes the popover's existing call while there: it passed
`{ doctype }` where `{ data: { doctype } }` was expected, so it had been
reporting no properties at all. Plain `tsc` cannot resolve `.vue`, which
is why the shape error went unseen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:43:34 +05:30
Ritvik Sardana
5692e56287 fix(command-palette): scope must rank, not gate
Typing on a ticket page returned only that ticket's commands whenever
any of them matched, and global search never ran. `Set status: Open`
matches "open", so a ticket whose subject contains "open" was
unreachable without dismissing the context chip first.

Scoped rows already outrank global ones via `CONTEXT_WEIGHT`, so append
the global list below them instead of returning early. An empty query
still shows the ticket's commands alone — that is what the chip is for.

Also pin the fallback group whenever the query is long enough to search,
rather than only when nothing matched. `fuzzyScore` matches
subsequences, so almost any real word matched something and the escape
hatch stayed hidden exactly when it was needed — which is most visible
with `title_only: true`, where a phrase from an email body finds
nothing. Retitled from "No results" now that it sits below results.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:41:44 +05:30
Ritvik Sardana
b2c9d541a4 fix(command-palette): review fixes, flat option rows and ticket scope
Addresses the blockers from the design and PM reviews of the palette:

- assign is add-only and never drops an existing assignee; logs the
  activity and fires `capture` like the popover does
- "Assign to me" root row, hidden once the agent holds the ticket
- customer-portal gate lives with the state (`isPaletteAvailable`), not
  the mount site, since AppSidebar has two parents
- flat option rows: typing `urgent` sets the priority with no drill-down
- current value ticked; a no-op pick toasts instead of closing silently
- palette writes broadcast `notifyTicketUpdate` so co-viewers see them
- highlight follows its row across async result arrival
- filters merge through the list's own `applyFilters` rather than a
  one-way URL write (`listViewFilters.ts`)
- context chip scopes the palette to the open ticket
- `esc` moved to the footer, labelled by what it actually does

Runnable checks: `fuzzyScore.check.ts` (ranking, incl. flat-row vs
parent in both directions), `listViewFilters.check.ts` (merge,
same-field replace, clear, no-list fallback).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 12:39:12 +05:30
Ritvik Sardana
2ba939f586 refactor(command-palette): name components in full
CP.vue -> CommandPalette.vue, CPRow.vue -> CommandPaletteRow.vue.
The abbreviations were the only ones in the folder and read as noise
next to commands.ts / searchCommands.ts.
2026-07-28 01:48:15 +05:30
Ritvik Sardana
538e5224e1 feat(command-palette): contextual Cmd+K palette for the agent portal
Rebuilds the command palette around what the agent is currently looking
at, rather than a flat command list.

- Ticket context: status, priority, assignee, team, type, tags, reply,
  comment, merge, next/previous ticket, copy id/link. Drill-downs load
  their options on demand.
- Ticket list context: filter by status or priority, assigned to me,
  switch view, clear filters. Filters travel via the URL query so the
  palette stays decoupled from ListViewBuilder.
- Server-side FTS search over tickets, comments and communications,
  merged into the same ranked list as commands.
- Recents (MRU commands and visited tickets) on an empty query, plus a
  settings drill-down derived from the permission-filtered tabs list.

Supporting changes:

- ListViewBuilder: split view loading from filter application, so a
  filter pushed onto the URL no longer resets the user's sort, columns
  and rows. An unresolvable ?view now redirects while keeping filters.
- useTicketNavigation: functions hoisted to module scope so the palette
  can reuse them; the composable is now shortcut registration only.
- modalStates: merge modal state shared, so the palette can open it.
- layoutSettings: navigate commands derive from the sidebar's own nav
  list, so labels and icons cannot drift. Contacts icon unified.
- Removed the CommandPalette barrel export; the palette is a singleton
  that registers its own global shortcut.
2026-07-28 01:46:00 +05:30
RitvikSardana
b6f660ee26 Merge pull request #3620 from frappe/pot_develop_2026-07-26
chore: update POT file
2026-07-27 21:08:33 +05:30
RitvikSardana
4f7abdc58b Merge pull request #3625 from RitvikSardana/fix/link-filters-format
fix(customisation): convert link_filters to dict before search_link
2026-07-27 21:08:04 +05:30
Ritvik Sardana
6d48587320 refactor(home): render recent activity rows as router links
Rows were divs with a click handler, so they were not real anchors. Middle
click and cmd click did nothing and the destination never showed on hover.
router-link renders an <a href> and keeps in-app navigation.
2026-07-27 20:56:58 +05:30
Ritvik Sardana
85592330e2 fix(customisation): convert link_filters to dict before search_link
Customize Form stores link_filters as [[doctype, fieldname, operator,
value]], and parseField passed that list straight through as the filters
param of frappe.desk.search.search_link.

Most doctypes tolerate it because search_widget hands list filters to
frappe.get_list. Doctypes registered in standard_queries (User) skip that
path and receive filters untouched, and those queries only read dicts, so
user_query raises AttributeError on filters.get(). Any Link-to-User field
with link_filters set breaks the New Ticket page.

Convert to the dict form search_link expects, matching what the desk does
with get_filter_from_json.
2026-07-27 20:52:14 +05:30
RitvikSardana
e8114d5d3e Merge pull request #3624 from RitvikSardana/fix/recent-activity-fade
fix(home): keep the last recent activity row out of the bottom fade
2026-07-27 20:40:56 +05:30
RitvikSardana
fc5dc103b8 Merge pull request #3623 from RitvikSardana/dashboard-copy
fix(dashboard): show chart-specific empty states
2026-07-27 20:40:34 +05:30
Ritvik Sardana
9e787d4c31 fix(dashboard): show tag charts to managers only
Tag mix is org level insight. An agent's view is scoped to their own
tickets, where the tag breakdown reflects routing rather than anything
they can act on, so skip the section and its fetch for non managers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:53:29 +05:30
Ritvik Sardana
487f4d1c22 fix(home): keep the last recent activity row out of the bottom fade
The scroll fade is a fixed overlay, so the final row (and its hover state)
always sat under it. Pad the list by the fade height so content scrolls
clear of the gradient.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:44:23 +05:30
Ritvik Sardana
b6fc94d2e0 fix(dashboard): key empty states on a stable chart key
Chart titles are translated server side, so an English-keyed lookup misses
on non-English sites. Emit a stable key from the chart config builders and
match on it. Carry the chart title in the dashboard-wide empty state too,
so those cards render headers instead of placeholder bars.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:44:03 +05:30
Ritvik Sardana
327315068c fix(dashboard): show chart-specific empty states
Only the empty-state title was overridden per chart, so every empty chart
fell back to the generic "Dashboard charts will appear here" message.
Map the copy by chart title and render the real chart header instead of
skeleton bars on empty cards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:55:18 +05:30
RitvikSardana
d26c4cc450 Merge pull request #3621 from RitvikSardana/feat/tag-analytics
feat: First Ticket tag and tag charts on the dashboard
2026-07-27 17:39:57 +05:30
Ritvik Sardana
465e14530f fix: track hd_agent changes 2026-07-27 17:29:33 +05:30
Ritvik Sardana
27dbf50d80 refactor(tags): check for an earlier ticket instead of counting
`raised_by` is unindexed, so counting every ticket for a requester was a
full scan on each insert. An existence check stops at the first match.
2026-07-27 16:53:54 +05:30
Ritvik Sardana
1198b0e922 fix(style): change sla due color 2026-07-27 16:31:13 +05:30
Ritvik Sardana
d5354838e6 fix(tags): fill in a missing colour on an existing tag
A Tag master minted before colours existed, or by desk's tag sidebar,
keeps a null colour and renders gray forever. Treat a missing colour
the same as a claim and fill it in; a tag that already has one still
keeps it.
2026-07-27 16:22:30 +05:30
Ritvik Sardana
8d7aabb29e feat(dashboard): add Top Tags and Tag Trend charts
Two tag charts on the dashboard, reusing the existing date, team and
agent filters. Top Tags ranks the busiest tags on a horizontal bar so
long tag names get the long axis; Tag Trend stacks the top five tags
per day so a spike in one is visible. Tags live in Tag Link rather
than a scalar column, so these join instead of reusing the group-by
helpers the other master charts use.

Bar chart value axes now step by whole numbers. Every bar chart on
this dashboard counts tickets, and half a ticket is not a thing.
2026-07-27 16:15:22 +05:30
Ritvik Sardana
55afea6641 feat(tags): tag a requester's first ticket
Tag the first ticket ever raised by a requester with "First Ticket"
instead of setting a custom field, so it filters, reports and slices
like any other tag. The count runs in after_insert, where a count of
one means this is the first, and goes through apply_tag so the Tag
master is claimed for helpdesk and shows in the picker.
2026-07-27 16:15:03 +05:30
RitvikSardana
0aa9b3882c Merge pull request #3613 from RitvikSardana/feat/ticket-layout
feat: new ticket sidepanel layout
2026-07-27 03:14:05 +05:30
Ritvik Sardana
b6636c1b33 fix: add tags in Ticket Filters 2026-07-27 02:54:38 +05:30
Ritvik Sardana
dd198e2246 fix: framework/ui build 2026-07-27 02:25:12 +05:30
Ritvik Sardana
22b15e7cc1 fix(build): resolve @framework/ui imports from the desk root
@framework/ui is linked from source, so rollup resolves its imports
starting at apps/frappe/ui, and desk/node_modules is never on that
lookup chain. dompurify and reka-ui arrive there as frappe-ui deps, so
`bench build --app helpdesk` failed on CI where apps/frappe/ui has
nothing installed.

Deduping both forces them to resolve from the project root, the same
reason vue and frappe-ui are already listed.
2026-07-27 02:04:50 +05:30
Ritvik Sardana
34229ac4c3 refactor: tags component API 2026-07-27 01:41:41 +05:30
Ritvik Sardana
4308f38d53 feat(tags): batch ticket tag edits into one request
Tag editing now runs as a session: chips update optimistically while the
picker is open, and the whole edit lands as a single update_tags call when
it closes, logging one activity instead of one per tag.

The colour step moves into its own TagColorPicker component, and the tag
components are grouped under ticket-agent/tag/.
2026-07-26 18:25:12 +05:30
frappe-pr-bot
d2c5016988 chore: update POT file 2026-07-26 11:43:09 +00:00
Ritvik Sardana
950209e4c7 fix: remove dead code 2026-07-26 16:54:16 +05:30
Ritvik Sardana
2810cb4f3f Merge branch 'develop' of https://github.com/frappe/helpdesk into feat/ticket-layout 2026-07-26 14:43:35 +05:30
Ritvik Sardana
83a55e6d42 fix: remove chevron 2026-07-25 22:20:33 +05:30
Ritvik Sardana
4171740a65 fix: hide navigation buttons 2026-07-24 18:43:33 +05:30
Ritvik Sardana
eb27fb81c9 fix: polish sidebar and priority 2026-07-24 18:36:24 +05:30
Sydney
98c998b838 refactor: reorder and clarify ticket portal insert rules 2026-07-24 16:22:32 +05:30
Sydney Gomes
feb3dc53cb Merge pull request #3618 from aerodeval/fix/comment-attachment-upload-feedback
fix: attachment upload feedback + uploads while editing comments
2026-07-24 15:19:42 +05:30
Sydney
049935b60b fix: exit comment edit mode reliably on save/discard
Toggling `editable` on an existing comment crashed Vue's patch ("Cannot read properties of null (reading 'insertBefore')" in processCommentNode) because the v-if edit affordances sit next to the renderless Editor's ProseMirror-managed DOM. The aborted update left the Save/Discard buttons stuck on screen and the comment never left edit mode.

Key the Editor on edit/view mode so switching cleanly remounts it instead of patching across ProseMirror's DOM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:03:49 +05:30
Sydney
9d68631a74 fix: allow file uploads when editing a ticket comment
The edit-comment Editor had no upload-function, so pasted or dropped media fell back to an orphaned private file (no attached_to) that 403s for other viewers. Wire up uploadFunction bound to the ticket, matching the new-comment editor.

Closes #3617

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:07:21 +05:30
Sydney
8b7958bbb6 fix: show a spinner on the attachment button while uploading
The FileUploader attach button only disabled itself during an upload with no visible indicator, so users had no way to tell why the send button was disabled. Swap the paperclip for a LoadingIndicator while uploading in the comment, email, and compact editors.

Closes #3616

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:07:20 +05:30
Ritvik Sardana
b403afc692 fix: spacing in side panel 2026-07-24 12:59:28 +05:30
Sydney
383e395535 refactor: use get_query to enforce ticket read permissions 2026-07-24 10:27:36 +05:30
Ritvik Sardana
e1bb4efbda fix: polish SLA section 2026-07-24 02:26:46 +05:30
Ritvik Sardana
7cbdd88364 chore: copy cleanup 2026-07-23 18:37:05 +05:30
Ritvik Sardana
02c6d57e76 fix: restore add_priority_integer patch
This 2023 patch was dropped in fce245316 when integer_value was replaced
by level. Old patches must never be deleted from the ledger; the new
backfill_priority_level supersedes it by addition, not removal.
2026-07-23 18:30:37 +05:30
Ritvik Sardana
67d0601fef chore: untrack stray .claude-worktrees gitlink
An accidental 160000 gitlink to the embed-private-images worktree was
committed in 865fdb4c9. Remove it and ignore .claude-worktrees so it
can't be re-added.
2026-07-23 18:30:30 +05:30
Ritvik Sardana
a15be81f08 fix: add chevron in status field 2026-07-23 18:25:11 +05:30
Ritvik Sardana
b192881de0 fix: ux for max tags 2026-07-23 18:12:30 +05:30
Ritvik Sardana
509d81b012 fix: SLA popover spacing 2026-07-23 17:30:30 +05:30
Ritvik Sardana
9cfcc1f72a fix: tags UX 2026-07-23 17:20:48 +05:30
RitvikSardana
6018181a35 Merge pull request #3610 from RitvikSardana/fix/default-portal-settings-existing-sites
fix: set default portal settings for existing sites
2026-07-23 17:08:25 +05:30
Ritvik Sardana
fbeeb0a5c3 fix: set default portal settings for existing sites
Sets Portal Settings default_role to HD Customer and
default_portal_home to /helpdesk on existing sites, only when the
values are not already set. Previously this ran on fresh installs only.
2026-07-23 16:57:26 +05:30
Ritvik Sardana
7fdcbd70d0 fix: SLA time style 2026-07-23 16:13:21 +05:30
Ritvik Sardana
aad724d1a8 chore: code cleanup 2026-07-23 14:00:03 +05:30
Ritvik Sardana
ebf49e189b fix: spacing in sections 2026-07-23 13:43:27 +05:30
Ritvik Sardana
d63a356da5 chore: update labels 2026-07-23 13:25:33 +05:30
Ritvik Sardana
274271f80a fix: polish side panel 2026-07-23 13:16:01 +05:30
Sydney
4c03293c97 refactor: derive ticket read fields from permissions 2026-07-23 10:08:54 +05:30
Sydney
89afe5614b fix: hide internal fields in ticket creation response 2026-07-23 10:08:26 +05:30
Ritvik Sardana
c61bb6df99 feat: add colors in Tags 2026-07-23 00:29:03 +05:30
RitvikSardana
146bfe33d0 Merge pull request #3608 from RitvikSardana/fix/attachment-remove
fix: unable to remove attachment
2026-07-22 22:54:40 +05:30
RitvikSardana
313e4620a4 Merge pull request #3607 from RitvikSardana/feat/home-recent-activity
feat: Recent Activity card on Home dashboard
2026-07-22 21:56:16 +05:30
Ritvik Sardana
0cc6f5d23a fix(ux): ticket value update 2026-07-22 21:25:21 +05:30
Ritvik Sardana
28a2385104 fix: line height 2026-07-22 21:00:18 +05:30
Ritvik Sardana
5038dd93a1 refactor: shared AttachmentList with lucide icon and video preview
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:50:17 +05:30
Ritvik Sardana
316a1b6292 fix: unable to remove attachment 2026-07-22 20:14:59 +05:30
Ritvik Sardana
06fc790387 fix: flaky tests in hd_agent 2026-07-22 20:01:37 +05:30
Ritvik Sardana
381073c764 refactor: API function 2026-07-22 19:29:57 +05:30
Ritvik Sardana
ae5702abf0 fix: dedupe recent activity per ticket before limiting
Group each source's events by ticket in SQL (MAX(creation)) so a single
busy ticket cannot consume the row limit and starve other eligible
tickets. Backfill the updated-action text for winning tickets, filter
inaccessible tickets before the cap, and add a regression test.
2026-07-22 18:51:07 +05:30
Ritvik Sardana
0415c75e9d fix: better list of events 2026-07-22 18:37:52 +05:30
Ritvik Sardana
2c3a354009 feat: tags in sidepanel 2026-07-22 17:52:47 +05:30
Ritvik Sardana
28465bb1a6 fix: better empty state 2026-07-22 17:49:58 +05:30
Ritvik Sardana
fd6e34b0c4 feat: recent activity 2026-07-22 17:33:07 +05:30
Ritvik Sardana
6dd3a6af57 feat: show priority level icon in agent ticket field
Render the HD Ticket Priority level icon before the selected value and
each option in the agent side panel priority Link (via the combobox
item-prefix slot, reused for the control). Keep the customer portal
priority as plain text for now.
2026-07-22 13:05:44 +05:30
Ritvik Sardana
78167274b1 fix: make patch better 2026-07-22 12:55:46 +05:30
Ritvik Sardana
7f6c42b853 chore: code cleanup 2026-07-22 12:55:46 +05:30
Ritvik Sardana
fce245316a feat: display ticket priority as icon and level
Replace the arbitrary integer_value on HD Ticket Priority with a purposeful
`level` Select field, and render priorities as an icon + name across the
agent ticket list, home pending tickets, customer portal, and SLA settings.

- Add `level` (Urgent/High/Medium/Low), remove integer_value
- Backfill patch: builtins map by name, custom priorities inherit the level
  of the nearest builtin above them (Medium when a site has no builtins)
- New TicketPriority.vue component and ticketPriority store
- Drop the now-unused priority-range colour pipeline in agent_home
2026-07-22 12:55:46 +05:30
RitvikSardana
77981e1f01 Merge pull request #3604 from RitvikSardana/feat/priority-level-icons
feat: display ticket priority as icon and level
2026-07-22 12:50:14 +05:30
Ritvik Sardana
700c48816c fix: assignee component width 2026-07-22 12:49:38 +05:30
Ritvik Sardana
04ac2c476f fix: make patch better 2026-07-22 12:13:27 +05:30
Ritvik Sardana
a48e9e5a49 chore: code cleanup 2026-07-22 11:47:06 +05:30
Ritvik Sardana
2d124b8f36 feat: display ticket priority as icon and level
Replace the arbitrary integer_value on HD Ticket Priority with a purposeful
`level` Select field, and render priorities as an icon + name across the
agent ticket list, home pending tickets, customer portal, and SLA settings.

- Add `level` (Urgent/High/Medium/Low), remove integer_value
- Backfill patch: builtins map by name, custom priorities inherit the level
  of the nearest builtin above them (Medium when a site has no builtins)
- New TicketPriority.vue component and ticketPriority store
- Drop the now-unused priority-range colour pipeline in agent_home
2026-07-22 02:07:20 +05:30
Ritvik Sardana
a16c83a51c fix: assignTo focus state and animation 2026-07-22 00:11:09 +05:30
Ritvik Sardana
20f65744a4 fix: use link field from framework 2026-07-21 18:54:28 +05:30
Ritvik Sardana
53284913fa fix: better SLA layout 2026-07-21 17:09:50 +05:30
Ritvik Sardana
865fdb4c9a fix: reposition SLA layout 2026-07-21 12:48:50 +05:30
Sydney Gomes
8ba5f214fb Merge pull request #3569 from aerodeval/feat/mobile-team-switcher
feat(mobile): team switcher in agent ticket header + mobile fixes
2026-07-21 02:27:52 +05:30
Sydney
cd63d475ad fix(desk): dark-mode styling for collapsed quote toggle in EmailEditor
The "..." collapse toggle used a hardcoded light background (#e8eaed) with
no dark variant, rendering as a bright white pill in dark mode. Add the
dark rules already used by the read-only EmailContent view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 02:11:22 +05:30
RitvikSardana
f3e4d311d3 Merge pull request #3599 from RitvikSardana/editor/paste
fix: editor link and edit comment
2026-07-20 13:00:30 +05:30
Sydney
23597cec0a refactor: clarify permlevel exemptions in portal insert rules 2026-07-20 12:59:46 +05:30
Ritvik Sardana
641ad86f1d fix(patch): editor link and edit comment 2026-07-20 12:51:45 +05:30
Sydney
d2e7ee7232 fix(mobile): ticket header row px-3 to sit on the same gutter as tabs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:41:34 +05:30
Sydney
8b359ac378 fix(mobile): align tab lists with the header hamburger icon
The hamburger icon sits 12px from the screen edge (ms-1 + button
px-2) while tab lists used 20px (px-5), so the tab row started
visibly right of the header icon. Tab lists are now px-3 on mobile
across all tabbed views: agent ticket and customer portal ticket
(mobile-only tab bars, unconditional) and Contact/Customer pages
(max-sm variant so desktop keeps the default px-5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:39:11 +05:30
Sydney
b2f01d90c9 refactor: move activity deep-links from URL hash to ?highlight param
The hash was doing two jobs on ticket pages: useActiveTabManager
stores the active tab in it, and notification/search links used it to
point at one activity. The prefix allowlist only guarded the reader;
the writer side still interleaved: the post-highlight cleanup stripped
whatever hash existed 3s later (snapping desktop tabs picked during
the highlight window back to tab 0) and dropped ?view=, and on mobile
a deep-link hash was destroyed by the tab manager before its target
could ever mount.

Deep-link targets now travel as ?highlight=<element id> while the
hash carries only tab state. useActiveTabManager needs no changes --
it already preserves route.query in every navigation, so the two
writers own disjoint fields. Generators emit #activity (renders all
activity types on both form factors) plus the highlight param, making
mobile notification/search deep-links functional for the first time.
Cleanup removes only the consumed param, keeping ?view= and the tab
the user is on.

Supersedes the deepLinkPrefixes allowlist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:20:51 +05:30
Sydney
068a41d8e6 refactor: allowlist deep-link prefixes instead of tab-name denylist
Deep-link ids are always prefixed (comment-, communication-), so match
on those instead of maintaining a list of tab names that must track
every tab rename or addition. Also prefixes MobileNotifications'
mention hash the way desktop Notifications.vue already does — the bare
comment name could never match the comment-<name> element ids.

Addresses greptile review on #3569.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:54:00 +05:30
Sydney
70051fe5f2 fix(mobile): align ticket header padding with tab list (px-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:32:55 +05:30
Sydney
af9aa8df76 fix: truncate long single-assignee name in AssignTo trigger
A single assignee's full name rendered without ellipsis because every
flex ancestor kept min-width:auto; long names overflowed the trigger.
min-w-0 down the chain (Button label span, trigger row, MultipleAvatar)
lets the existing truncate class clip with an ellipsis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:30:10 +05:30
Sydney
b5592d6f91 fix(mobile): agent availability as native dropdown submenu
AvailabilityMenuMobile nested a right-placed Popover inside the
profile Dropdown; on narrow screens there is no room to the right so
the status panel landed on top of the menu items below it (theme
toggle). Replaced with a native Dropdown submenu option (same pattern
as the Apps entry) so reka-ui keeps it positioned inside the viewport,
and deleted the now-unused component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:25:10 +05:30
Sydney
ea219b8c5b fix(mobile): match team switcher styling with assignee control
Renders the team Link through its target slot as the same outline
button AssignTo uses, and splits the header row evenly between the
two controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:44:12 +05:30
Sydney
df1787a833 fix(customer-portal): consistent text-p-sm in mobile details
Labels and values in the details tab now all use text-p-sm; the SLA
label width matches the other labels at 150px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:22:45 +05:30
Sydney
9417f76547 fix(mobile): opaque sidebar background
frappe-ui Sidebar is transparent in dark mode by design; as a fixed
overlay on mobile the page content showed through. Same fix as
frappe/suite#108.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:22:45 +05:30
Sydney
b8b8b85c8b fix(mobile): don't treat active-tab hash as activity deep-link
useActiveTabManager stores the active tab in the URL hash, while
notification/search links use the hash to deep-link to one activity.
scrollToHash() treated any hash as a deep-link and stripped it from
the URL after highlighting, so opening the Activity tab on mobile
snapped back to Details a few seconds later (the lucide sprite's
<symbol id="activity"> even gave it an element to "scroll" to).
Tab-name hashes are now excluded from deep-link handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:22:45 +05:30
Sydney
39af981753 fix: skip get_apps fetch on customer portal
Customers lack permission for frappe.apps.get_apps and the call 403s.
The sidebar remounts on portal switch, so the flag re-evaluates for
agents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:22:45 +05:30
Sydney
02810a821f feat(mobile): team switcher in agent ticket header
Adds an HD Team dropdown next to AssignTo in the mobile ticket
header so agents can reassign a ticket's team without opening the
details tab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:22:45 +05:30
RitvikSardana
9c742fc313 Merge pull request #3568 from RitvikSardana/ui/sidebar
fix: SidebarItem overflow
2026-07-18 04:02:02 +05:30
Ritvik Sardana
984e5aa953 fix: rerendering of mention popover 2026-07-18 03:39:18 +05:30
Ritvik Sardana
1264551c8a fix: SidebarItem overflow 2026-07-18 03:26:13 +05:30
RitvikSardana
41561b1dc1 Merge pull request #3565 from RitvikSardana/ux/tickets-customer-contact
fix: make headers sticky in TicketsTab
2026-07-18 00:10:53 +05:30
Ritvik Sardana
db2971a5d9 chore: code cleanup 2026-07-17 19:20:51 +05:30
Ritvik Sardana
134142453c fix: make headers sticky in TicketsTab 2026-07-17 19:07:09 +05:30
Sydney Gomes
29c4dc2f2b Merge pull request #3538 from aerodeval/feat/onboarding-persona-questionnaire
feat: onboarding persona questionnaire
2026-07-17 18:59:36 +05:30
Sydney
8e3bbea28b feat: route the explore goal to the seeded welcome ticket
Falls back to the tickets list when the welcome ticket is gone or the
lookup fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:34:08 +05:30
Sydney
fbda167795 fix: cap questionnaire textarea resize
Vertical-only, between one row block and roughly twice the default height.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:34:05 +05:30
Sydney
eb9089a3ec refactor: read telemetry state from boot and simplify the persona guard
Ship the pulse client config as a boot value (the same channel as lang/dir)
so telemetryEnabled() is synchronous and the guard never hits the network.
Collapse personaInterrupt onto canViewPersona, move the Persona-route check
inside it, and report persona_captured truthfully for all users — the guard
alone owns eligibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:33:54 +05:30
Sydney
cf1c863525 fix: keep the app title in branded invitation subjects
The subject now reads "You've been invited to join <brand> on <app title>"
so recipients still learn which product invited them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:33:44 +05:30
Sydney Gomes
49902e915b Merge pull request #3563 from aerodeval/fix/outlook-mso-comments
fix: strip html comments from agent reply email content
2026-07-17 15:43:41 +05:30
Sydney
2b83c4bb42 fix: translate create button label on tickets page 2026-07-17 15:23:40 +05:30
RitvikSardana
0afefdefec Merge pull request #3562 from RitvikSardana/fix/customer-primary-contact
fix: reuse existing contact as customer primary contact & accurate ticket counts
2026-07-17 14:47:44 +05:30
Sydney
513bf1c312 fix: strip html comments from agent reply email content 2026-07-17 14:20:47 +05:30
Ritvik Sardana
9fe0925f92 chore: match prettier indentation, drop unused doc prop, note tablist height coupling 2026-07-17 13:32:04 +05:30
Ritvik Sardana
b2cd9f35ce fix: ticket count in customer & contact page 2026-07-17 13:18:41 +05:30
Sydney
d9018ff7bb fix: keep brand name out of other apps' invitation emails
HelpdeskUserInvitation is registered via override_doctype_class, which
is site-wide — every User Invitation got the HD brand name as its email
title once onboarding set one, so on a multi-app site an ERPNext invite
would arrive titled with the helpdesk brand.

Defer to the framework title (per-app via the app_title hook) unless
the invitation's app_name is helpdesk. Covered both ways by tests:
helpdesk invitations get the brand, frappe invitations keep their own
title with a brand set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:09:34 +05:30
Sydney
2567f75a55 refactor: standardize onboarding routing
Access control moves onto the route: /onboarding gets a beforeEnter
gate (canViewPersona), and the global guard keeps only the
once-per-session interrupt (personaInterrupt). persona.ts sheds its
homegrown { done, to } protocol for plain vue-router idioms.

Layout moves into the route tree: portal pages nest as children of a
PortalRoot parent that picks the agent or customer shell from the
session, while /onboarding stays top-level and renders bare. That
deletes meta.fullScreen and App.vue's layout branching — the root
template is a single router-view, and the no-flash behavior becomes
structural: an unresolved route renders nothing because there is
nothing else to render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:21:26 +05:30
Sydney
c500bc53ee fix: translate and reword the questionnaire subtitle
The subtitle was the only user-facing string in the component not wrapped
in __(), so it stayed English in every locale while its siblings (Next,
Finish, Skip for now, every question title) translated.

It also renders under each question rather than just the first, so "A few
quick questions to tailor Helpdesk to how you work" narrated a flow the
reader was already several screens into — and promised tailoring under
questions that configure nothing. State the reason instead, so it holds up
on any question.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:25:06 +05:30
Sydney
7c9dc58791 feat: ask how they heard about helpdesk as chips
The referral question was a required free-text box, which is hard to
aggregate. Offer the channels people actually arrive through, Frappe
ecosystem first, and keep "Other" so the free-text nuance isn't lost.

Single-select: attribution is one answer, and it keeps the Pulse property
a scalar rather than an array.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:11:51 +05:30
Sydney
8e5cee8d06 fix: align questionnaire chip states with the select's tokens
The chips jumped from surface-gray-4 when selected to surface-gray-5 on
hover, while unselected chips had no bg hover at all — frappe-ui's outline
variant only moves the border.

Reuse the Select's own recipe (shared/selection/utils.ts) so selection
feedback is consistent across the form: alpha-gray-2 on hover, gray-3 when
selected, gray-4 when both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:11:41 +05:30
Sydney
c0cca6f238 fix: don't render the portal shell before the route resolves
App.vue picks its layout from route.meta.fullScreen, but rendered before
the first navigation resolved. At START_LOCATION meta is empty, so v-else
made PortalRoot the default and the sidebar painted for the length of the
guard's round trips before a fullScreen route (onboarding) replaced it.

Render neither branch until route.name exists. Fixing it here rather than
deferring the mount keeps theme, favicon and config init at their current
timing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:11:31 +05:30
Ritvik Sardana
534bc291bc fix: if new contact invited via customer show in toast 2026-07-16 18:30:01 +05:30
RitvikSardana
b2faee1248 Merge pull request #3555 from frappe/view-icons
fix: replace emojis with icons for custom views
2026-07-16 18:15:36 +05:30
Ritvik Sardana
36eb9fd89a fix: use clock-alert icon for SLA Alerts view
triangle-alert reads as a generic error and hourglass is already used by
the Pending Tickets view. clock-alert conveys time-based urgency, which
matches SLA breach tracking. Existing sites pick it up via the
rename_and_update_std_views patch.
2026-07-16 17:21:31 +05:30
Ritvik Sardana
4767ca00e2 fix: restore customer portal permission banner in sidebar
The sidebar refactor to AppSidebar dropped CustomerPortalPermissionBanner
along with its showPermissionNoticeBanner computed. Restore it in the
footer slot with the original visibility logic (agent portal only,
manager/admin, HD Settings notice flag).
2026-07-16 17:21:30 +05:30
Sydney
2e450082be feat: ask for "Other" detail on next instead of disabling it
Selecting "Other" reveals a free-text box. Rather than silently disabling
Next, keep it enabled and ask on click: proceed() blocks, focuses the box
and surfaces an error via frappe-ui's Textarea error prop.

The error clears on navigation and on toggling "Other" off, so re-opening
the box never shows an error the user hasn't earned.

Also make referral_source required, and carry the asterisk on the title
for questions whose title is the prompt and so have no label to hang it
on. "Please specify" served as both placeholder and error; replace it
with per-question placeholder copy and an actionable error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:11:04 +05:30
Ritvik Sardana
755ac8de7a fix: dont invite primary contact if already exists 2026-07-16 15:52:07 +05:30
Sydney
8099e23dfb fix: only gate next on text questions marked required
canProceed treated every text question as mandatory, so referral_source
blocked Next until answered even though it carries no required flag and
renders no asterisk. required was consulted for the asterisk only, never
during validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 15:35:40 +05:30
Sydney
deb2c7f921 test: cover persona capture and invitation email title
Adds tests for mark_persona_captured (brand name written to HD Settings
and Website Settings, site_name default for the welcome email subject,
persona_captured flag, blank brand left untouched) and for the
HelpdeskUserInvitation title override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:42:15 +05:30
Ritvik Sardana
a296498102 fix: SavedReply filter 2026-07-16 14:20:11 +05:30
Ritvik Sardana
8828d53fd7 fix: notification panel wrt sidebar 2026-07-16 13:47:03 +05:30
Sydney Gomes
1b3ccb41c0 Merge pull request #3554 from ews-pgasser/chore/german-translations
added missing msgstr for translation
2026-07-16 13:16:53 +05:30
Philipp Gasser
eb802e01a6 chore: added missing msgstr for translation 2026-07-16 09:32:18 +02:00
Ritvik Sardana
698e6d274a fix: styles 2026-07-16 12:39:46 +05:30
Sydney
d5b65d2910 feat: disable next until the current question is answered 2026-07-16 12:11:41 +05:30
Sydney
dd7bb45af8 fix: hide internal ticket fields from customers 2026-07-16 11:33:42 +05:30
Ritvik Sardana
aaa6cdf574 fix: update sidebar code with new API 2026-07-16 02:44:36 +05:30
RitvikSardana
91163b657a Merge pull request #3531 from ews-pgasser/chore/german-translations 2026-07-16 02:32:49 +05:30
Sydney
be07eb606b fix: only expose creation-form fields from templates 2026-07-16 02:05:57 +05:30
Sydney
d03db34406 fix: stamp response times on portal replies 2026-07-16 02:05:27 +05:30
Ritvik Sardana
827d865b9b fix: remove memoji package 2026-07-16 01:25:51 +05:30
Ritvik Sardana
8fa03b6478 fix(views): use icons instead of emojis 2026-07-16 01:25:11 +05:30
Sydney
b4cb599c27 fix: await onboarding navigation chain and un-fade on failure 2026-07-15 20:02:38 +05:30
Sydney
b939428949 refactor: isolate persona routing and drop guard import cycle 2026-07-15 19:47:14 +05:30
ews-pgasser
afed0f86ea chore: added newly introduced german translations by pot-file 2026-07-15 16:00:29 +02:00
Sydney
fa1430f1a8 feat: use brand name in invitation email subject 2026-07-15 19:30:17 +05:30
Sydney
b57525fabe feat: set site_name from onboarding org name for welcome email 2026-07-15 19:21:58 +05:30
Sydney
8dd9905606 perf: read persona flag from auth boot to drop guard round-trip 2026-07-15 19:09:28 +05:30
Philipp Gasser
976c195a71 Merge branch 'develop' into chore/german-translations 2026-07-15 15:15:51 +02:00
Sydney
cdc1e286a4 feat: set site-wide app name from onboarding org name 2026-07-15 18:23:35 +05:30
Sydney
5dd984e21f feat: add 'how did you hear' question to onboarding 2026-07-15 18:23:09 +05:30
Sydney
39f94615c6 feat: set brand name from onboarding org name 2026-07-15 17:57:52 +05:30
Sydney
c5f44440b3 feat: route to selected goal and refine onboarding questionnaire 2026-07-15 17:40:28 +05:30
RitvikSardana
737533d3db Merge pull request #3540 from kaulith/fix/agent-reply-no-prior-communication
fix: agent reply fails on tickets with no prior communication
2026-07-15 13:31:43 +05:30
Sydney
90ca4ef236 chore: rename onboarding event to onboarding_persona_hd 2026-07-15 12:48:35 +05:30
Sydney
3fb08b35ab refactor: persist persona flag via onboarding endpoint 2026-07-15 12:42:34 +05:30
Sydney
13212ec6e5 feat: add organization size to onboarding questionnaire 2026-07-15 12:41:51 +05:30
Sydney Gomes
f04e19a001 Merge pull request #3547 from aerodeval/fix/sla-default-blank-condition-catchall
fix: ticket SLA policy application
2026-07-15 12:11:50 +05:30
Sydney
d73f46742a test: blank SLA condition must not match ahead of the default SLA
Covers both halves of the catch-all fix: a non-default SLA with a
blank ("" or NULL) condition never matches a ticket, and demoting the
seeded "Default" SLA by promoting a new default doesn't leave the
demoted one winning every ticket.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:58:46 +05:30
Sydney
5fa9d980f3 Merge upstream/develop into onboarding branch 2026-07-15 01:25:25 +05:30
RitvikSardana
06e3a4eb4c Merge pull request #3539 from frappe/pot_develop_2026-07-12
chore: update POT file
2026-07-15 00:59:41 +05:30
RitvikSardana
d632f6e95f Merge branch 'develop' into pot_develop_2026-07-12 2026-07-14 22:31:04 +05:30
RitvikSardana
78d83a9f24 Merge pull request #3522 from frappe/pot_develop_2026-07-05
chore: update POT file
2026-07-14 22:30:33 +05:30
RitvikSardana
584a1182c0 Merge pull request #3517 from frappe/l10n_develop
chore: sync translations from crowdin
2026-07-14 21:28:21 +05:30
RitvikSardana
ddf5acc9ef Merge pull request #3549 from frappe/upgrade-fui
fix: bump frappe UI to beta-24
2026-07-14 21:18:48 +05:30
Ritvik Sardana
49f14fdd98 fix: allow spaces in SavedReply 2026-07-14 17:49:45 +05:30
Ritvik Sardana
80fb83a431 fix: filter operator popover 2026-07-14 16:33:10 +05:30
Ritvik Sardana
bd2102ac20 fix: media upload in articles 2026-07-14 15:09:57 +05:30
Ritvik Sardana
ecb0a564fe fix: spacing in attachment icon 2026-07-14 14:55:34 +05:30
Sydney Gomes
9da4452311 Merge pull request #3548 from aerodeval/chore/raise-frappe-upper-bound
chore: raise upper bound limit for Frappe dependency
2026-07-14 14:39:36 +05:30
Ritvik Sardana
5c8eb2603f fix: patch beta24 suggestion-renderer 2026-07-14 14:39:16 +05:30
Sydney
6ea221f67e chore: raise upper bound limit for Frappe dependency 2026-07-14 14:26:18 +05:30
Ritvik Sardana
9b77397563 fix: commonify editor component 2026-07-14 13:36:56 +05:30
Sydney
5f63b65017 fix: default SLA checkbox double-toggles and never actually flips 2026-07-14 13:11:01 +05:30
Ritvik Sardana
1f8b5e583f chore: change frappe dependency version 2026-07-14 12:45:08 +05:30
Ritvik Sardana
c6bb72ca85 fix: migrate editor 2026-07-14 12:25:38 +05:30
Sydney
00d7bb5c84 fix: don't let a blank SLA condition act as an implicit catch-all
An enabled, non-default SLA with no condition was matching every
ticket ahead of the actual default SLA, since a blank condition
short-circuited to "always match" in the loop. This is most visible
when the seeded "Default" SLA is demoted (a different SLA is flagged
default_sla) but keeps its blank condition — it then silently wins
for every ticket regardless of which SLA is now marked default.

Only SLAs with a real condition can be matched this way now; a blank
condition no longer qualifies as a candidate, leaving default_sla as
the only legitimate catch-all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 11:12:49 +05:30
Sydney
0f33cfb647 fix: keep customer selectable at ticket creation 2026-07-14 10:43:36 +05:30
Sydney
5013fb703a feat: add company name question to onboarding 2026-07-14 10:27:12 +05:30
Ritvik Sardana
92fc5a65ee fix: label styles 2026-07-13 20:21:08 +05:30
Ritvik Sardana
3b31cb444b fix: update old colors 2026-07-13 18:32:49 +05:30
Sydney
fa266c315e fix: add back button for questions 2026-07-13 18:17:48 +05:30
Sydney Gomes
99d685a27a Merge branch 'develop' into fix/ticket-field-level-perms 2026-07-13 17:57:15 +05:30
Ritvik Sardana
79ebba3319 chore: upgrade frappe-ui to 1.0.0-beta.24
- Bump frappe-ui beta.3 -> beta.24 and @tiptap/* -> ^3.26.0
- Run espresso v2 token codemod across desk/src (surface/ink/outline
  renames, type-scale shift, and text-size+font-weight class merges)
- Dedupe tiptap/prosemirror in vite.config so the editor resolves a
  single prosemirror instance (fixes "Adding different instances of a
  keyed plugin" RangeError that blanked all rich-text editors)
- Fix TicketSLA Badge crash by returning a valid theme ("gray") during
  ticket load; beta.24 Badge rejects unknown/empty themes
- Re-apply follow-ups from the reverted bump PR: MobileSidebar i18n,
  hide TicketCustomerSidebar actions when Closed, TicketFeedback
  StarRating -> frappe-ui Rating, TicketCustomer editor optional
  chaining, vite-helpers editor-style.css submodule alias

Known issue: editor @mention and / slash-command popups do not open on
beta.24 (suggestion item list is empty at first render, so the popup
element never mounts); tracked for a follow-up fix.
2026-07-13 17:52:44 +05:30
RitvikSardana
482ebf07cf Merge pull request #3541 from RitvikSardana/customer-portal-permission-banner
fix: Customer portal permission banner
2026-07-13 16:33:42 +05:30
Ritvik Sardana
c8fc81655b fix: dont let contact field change by end user 2026-07-13 16:23:44 +05:30
RitvikSardana
65e0500094 Merge pull request #3543 from RitvikSardana/ux-alignment
fix: recent tickets alignment
2026-07-13 16:04:54 +05:30
Ritvik Sardana
0635405c64 fix: copy of dialog 2026-07-13 16:00:13 +05:30
Ritvik Sardana
9475349028 fix: bring back guest api call for config 2026-07-13 15:55:59 +05:30
Ritvik Sardana
c26cf84d15 fix: recent tickets alignment 2026-07-13 15:50:15 +05:30
Sydney
6e444c2129 test: field level permission coverage for HD Ticket 2026-07-13 15:39:13 +05:30
Sydney
c184868ee4 fix: migrate field permissions on existing sites 2026-07-13 15:39:12 +05:30
Sydney
764468333a fix: raise hidden template custom fields to permlevel 1 2026-07-13 15:39:10 +05:30
Sydney
6040ddebd3 fix: restrict agent-only ticket fields to permlevel 1 2026-07-13 15:39:08 +05:30
Sydney
ffb9cd9ed4 fix: rework onboarding questionnaire ui 2026-07-13 15:23:35 +05:30
Ritvik Sardana
a96b6ff8cc fix: add owner and creation field in bulk creation 2026-07-13 14:28:35 +05:30
Ritvik Sardana
10e6f73dc3 fix(UI): linked customers in contact page 2026-07-13 13:46:14 +05:30
Ritvik Sardana
4606250025 fix: remove guest access from get_config 2026-07-13 13:27:36 +05:30
Ritvik Sardana
0b37b43932 fix: null check in field options 2026-07-13 13:04:49 +05:30
Sydney Gomes
8f6042d342 Merge pull request #3530 from aerodeval/fix/thunderbird-formatting
fix: preserve formatting for thunderbird received emails
2026-07-13 13:02:15 +05:30
Ritvik Sardana
28eb577ee9 perf: promote contacts to managers with set-based queries
Replace the per-customer document loop with a single child-table
update and a bulk Has Role insert, evicting stale role caches since
bulk_insert skips the ORM. Flip the notice flag via set_single_value
and publish the settings-updated event explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:47:28 +05:30
Sydney
dbac204a35 fix: add pre-wrap to maintain spacing formatting 2026-07-13 12:15:39 +05:30
Ritvik Sardana
be0b6f7e48 refactor: use $socket from global store in config store
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:07:41 +05:30
Ritvik Sardana
b108f47c7b fix: update perms on banner 2026-07-13 10:42:00 +05:30
Ritvik Sardana
a28ad43a50 fix: restrict non-agents to their own contact on tickets 2026-07-13 10:25:42 +05:30
Ritvik Sardana
1891b404c0 feat: scope ticket customer field to contact's customers on customer portal 2026-07-13 10:25:33 +05:30
Kaushal Shriwas
18b59372aa fix: agent reply fails on tickets with no prior communication 2026-07-12 18:48:28 +05:30
frappe-pr-bot
b18851a0ff chore: update POT file 2026-07-12 11:35:30 +00:00
Sydney
eb6fcd5229 fix: refine onboarding persona questionnaire 2026-07-12 16:33:20 +05:30
Ritvik Sardana
a8ca6f442f fix: drop img src after setting embed so inline images render in emails
Pasted images are uploaded as private files, and parse_content kept the
original src alongside the embed attribute. The framework's cid
replacement then produced a duplicate-src img tag, so email clients
could resolve the login-gated /private/files URL instead of the inline
cid attachment. Swap src for embed (site files only, external URLs and
data URIs untouched) so the inline attachment always wins.
2026-07-12 12:53:39 +05:30
Ritvik Sardana
c81c2dfe19 chore: emphasize customer managers term in notice dialog 2026-07-12 01:59:19 +05:30
Ritvik Sardana
82765be52d refactor: guard notice endpoints with agent_manager_only decorator 2026-07-12 01:45:52 +05:30
Ritvik Sardana
5666f55117 chore: state previous portal behaviour and shorten checkbox label 2026-07-12 01:44:19 +05:30
Ritvik Sardana
0557912621 chore: simplify notice dialog copy and state what managers can do 2026-07-12 01:35:09 +05:30
MochaMind
1cb7861af3 chore: Bosnian translations 2026-07-11 15:48:23 +05:30
Sydney
1733b75837 feat: add onboarding persona questionnaire 2026-07-11 00:54:49 +05:30
Sydney
101fc39e0f feat: add Questionnaire stepper component 2026-07-11 00:54:48 +05:30
Sydney
2cf2cfac45 feat: add persona_captured flag to HD Settings 2026-07-11 00:54:39 +05:30
MochaMind
6606eee222 chore: Bosnian translations 2026-07-10 15:19:33 +05:30
Sydney Gomes
2ac1caa944 Merge pull request #3533 from frappe/mergify/aerodeval/I89bf640cfda25c6518dd8a913dec524b9b80bd52
Update workflow automation rules
2026-07-10 12:33:42 +05:30
Sydney Gomes
47d5c70d02 Update workflow automation rules
Signed-off-by: Sydney Gomes <null>
2026-07-10 12:11:11 +05:30
ews-pgasser
2cbe4355fc chore: added german translations 2026-07-09 18:19:51 +02:00
Sydney
c087f6fe10 fix: add consistent variable naming 2026-07-09 17:46:50 +05:30
Sydney
18ca7518c3 fix: add support for thunderbird mails in replied content 2026-07-09 17:36:01 +05:30
Sydney
0939bf8ff7 fix: preserve line-height for thunderbird mails 2026-07-09 16:55:30 +05:30
Sydney Gomes
77a0119309 Merge pull request #3529 from aerodeval/fix/display-time-elapsed-labels
fix: display proper labels for time elapsed
2026-07-09 15:15:48 +05:30
Ritvik Sardana
5f7a2e671c fix: sidebar banner layout 2026-07-09 14:25:32 +05:30
Sydney
bd22cf47df fix: preserve formatting for thunderbird received emails 2026-07-09 13:25:10 +05:30
Sydney
64d4499627 fix: display proper labels for time elapsed 2026-07-09 12:33:45 +05:30
Sydney Gomes
4d250f6ee8 Merge pull request #3506 from aerodeval/fix-ticket-merge
fix: redirect responses to correct merged ticket
2026-07-08 18:55:07 +05:30
Sydney
4951c74712 fix: correct invalid Check field default in HD Ticket
raised_outside_working_hours had default "False", which Frappe rejects
for Check fields (must be "0"/"1"). This broke any save_customization()
on HD Ticket, failing the field dependency form-script tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:41:53 +05:30
Sydney
6610c58c7c fix: handle replies when merge target can't be resolved
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:57:21 +05:30
Ritvik Sardana
1e16325daa feat: link permission update documentation in notice dialog 2026-07-07 22:09:34 +05:30
Ritvik Sardana
8492e6be56 feat: one-time notice for changed customer portal permissions
Agent managers see a sidebar banner explaining the new customer portal
permission model introduced by the customer contact management refactor.
The dialog offers to restore the previous behaviour by promoting every
existing customer contact to customer manager, so contacts can again see
all tickets of their customer.

The notice is enabled by a patch only on sites that have non-manager
customer members (i.e. sites migrated from the old contact model).
2026-07-07 21:22:11 +05:30
MochaMind
43a2cab245 chore: Persian translations 2026-07-06 14:22:35 +05:30
frappe-pr-bot
c80ce12f93 chore: update POT file 2026-07-05 11:10:55 +00:00
MochaMind
19182c83f3 chore: Uzbek translations 2026-07-03 04:05:26 +05:30
MochaMind
9cb5dceb38 chore: Bosnian translations 2026-07-01 03:58:36 +05:30
MochaMind
f9291a8e1f chore: Croatian translations 2026-07-01 03:58:34 +05:30
MochaMind
1160f36fe4 chore: Esperanto translations 2026-06-30 03:28:46 +05:30
MochaMind
13ce5b46fe chore: Serbian (Latin) translations 2026-06-30 03:28:44 +05:30
MochaMind
5516a4e19a chore: Norwegian Bokmal translations 2026-06-30 03:28:41 +05:30
MochaMind
216dfb9caa chore: Uzbek translations 2026-06-30 03:28:39 +05:30
MochaMind
186728d584 chore: Bosnian translations 2026-06-30 03:28:36 +05:30
MochaMind
4b70246543 chore: Burmese translations 2026-06-30 03:28:34 +05:30
MochaMind
f8652ac2ef chore: Hindi translations 2026-06-30 03:28:31 +05:30
MochaMind
369905d689 chore: Croatian translations 2026-06-30 03:28:29 +05:30
MochaMind
bbf6c95f42 chore: Thai translations 2026-06-30 03:28:27 +05:30
MochaMind
8636289f79 chore: Persian translations 2026-06-30 03:28:24 +05:30
MochaMind
c50efca62d chore: Indonesian translations 2026-06-30 03:28:22 +05:30
MochaMind
9c813a8fb6 chore: Portuguese, Brazilian translations 2026-06-30 03:28:18 +05:30
MochaMind
d421ae08f3 chore: Vietnamese translations 2026-06-30 03:28:16 +05:30
MochaMind
f0000f429a chore: Chinese Simplified translations 2026-06-30 03:28:13 +05:30
MochaMind
1f0e0ca725 chore: Turkish translations 2026-06-30 03:28:11 +05:30
MochaMind
7124a5a95b chore: Swedish translations 2026-06-30 03:28:09 +05:30
MochaMind
1d2d9d6e30 chore: Serbian (Cyrillic) translations 2026-06-30 03:28:06 +05:30
MochaMind
2d82b9dde9 chore: Slovenian translations 2026-06-30 03:28:04 +05:30
MochaMind
a8a14f8e32 chore: Russian translations 2026-06-30 03:28:01 +05:30
MochaMind
6ad6125546 chore: Portuguese translations 2026-06-30 03:27:59 +05:30
MochaMind
7e89bfa22e chore: Polish translations 2026-06-30 03:27:56 +05:30
MochaMind
3f849eba16 chore: Dutch translations 2026-06-30 03:27:54 +05:30
MochaMind
fb72bb2b96 chore: Korean translations 2026-06-30 03:27:52 +05:30
MochaMind
dd4ebe3400 chore: Italian translations 2026-06-30 03:27:49 +05:30
MochaMind
a50d82b3c9 chore: Hungarian translations 2026-06-30 03:27:47 +05:30
MochaMind
393433fb45 chore: German translations 2026-06-30 03:27:44 +05:30
MochaMind
1443a294dc chore: Danish translations 2026-06-30 03:27:42 +05:30
MochaMind
bb5e3b0dc2 chore: Czech translations 2026-06-30 03:27:39 +05:30
MochaMind
f270a9331d chore: Bulgarian translations 2026-06-30 03:27:37 +05:30
MochaMind
16eaa3abc3 chore: Arabic translations 2026-06-30 03:27:34 +05:30
MochaMind
9fa30934b1 chore: Spanish translations 2026-06-30 03:27:32 +05:30
MochaMind
5d1faf68ac chore: French translations 2026-06-30 03:27:29 +05:30
RitvikSardana
2b2d20e06e Merge pull request #3516 from RitvikSardana/editor-upload-fn
fix: pass uploadFunction prop
2026-06-30 03:16:48 +05:30
Ritvik Sardana
8fbf5c2b41 fix: null guard check 2026-06-30 01:04:45 +05:30
Ritvik Sardana
44cfd1d5cb fix: null guard check 2026-06-30 01:00:26 +05:30
Ritvik Sardana
1b26e17dc9 fix: pass uploadFunction prop 2026-06-30 00:54:34 +05:30
RitvikSardana
e093d73077 Merge pull request #3515 from RitvikSardana/route-by-customer
fix: add auto set customer setting
2026-06-29 18:47:06 +05:30
RitvikSardana
a82eabfe9d Merge pull request #3514 from RitvikSardana/fix/lv-design
fix: listview column resize & phone column
2026-06-29 18:41:40 +05:30
Ritvik Sardana
06b488221e fix: auto set customer setting 2026-06-29 18:32:42 +05:30
Ritvik Sardana
c4b07a935d fix: listview column resize, phone column & max capping 2026-06-29 18:02:33 +05:30
RitvikSardana
9d7e75c02a Merge pull request #3513 from RitvikSardana/fix/restore-doctype-permissions
fix: restore doctype permissions reverted in #3512
2026-06-29 15:27:05 +05:30
Ritvik Sardana
a5d4c45cda fix: query HD Ticket instead of Issue in support hour distribution
Restores the report query change from #3502 (reverted in #3512),
matching the report's ref_doctype change to HD Ticket.
2026-06-29 15:13:49 +05:30
Ritvik Sardana
c5d09133e5 fix: use get_list for permission-aware article category queries
Restores the knowledge base API change from #3502 (reverted in #3512):
get_list enforces user permissions where get_all bypasses them.
2026-06-29 15:13:27 +05:30
Ritvik Sardana
209bda2bac fix: restore doctype permissions reverted in #3512
Re-applies the DocType permission changes that were bundled into #3502
and lost when it was reverted (#3512):

- HD Ticket: replace 'All' with 'HD Customer' and 'HD Customer Manager'
- HD View: replace 'Guest' with 'HD Customer' and 'HD Customer Manager'
- HD Article / HD Article Category / HD Ticket Type: add 'HD Customer Manager'
- HD Ticket Template: add 'Agent Manager'

Also restores the Support Hour Distribution report export.
2026-06-29 15:10:00 +05:30
RitvikSardana
4a072f4664 Merge pull request #3512 from frappe/revert-3502-fix/bump-frappe-ui
Revert "fix: bump frappe UI"
2026-06-29 15:00:00 +05:30
RitvikSardana
3c4b6b84a9 Revert "fix: bump frappe UI" 2026-06-29 13:30:41 +05:30
Sydney
4819dfca35 fix: correctly route replies to merged tickets to the merge target 2026-06-29 00:01:27 +05:30
Sydney
076cdc759f test: add test cases for routing to correct communication 2026-06-28 22:59:02 +05:30
Sydney
644ce7926c fix: redirect replies on merged tickets to the merge target 2026-06-28 22:58:08 +05:30
RitvikSardana
5df20e73df Merge pull request #3502 from RitvikSardana/fix/bump-frappe-ui
fix: bump frappe UI
2026-06-28 18:22:17 +05:30
RitvikSardana
8c255460ec Merge pull request #3504 from frappe/pot_develop_2026-06-28
chore: update POT file
2026-06-28 17:31:14 +05:30
RitvikSardana
6d79a4a5b6 Merge pull request #3497 from frappe/l10n_develop
chore: sync translations from crowdin
2026-06-28 17:30:57 +05:30
frappe-pr-bot
ece9852a78 chore: update POT file 2026-06-28 11:13:20 +00:00
Ritvik Sardana
56d24f0faa chore: code cleanup 2026-06-28 14:18:25 +05:30
Ritvik Sardana
c9fdd600c1 fix: better list view 2026-06-28 14:02:26 +05:30
MochaMind
378522e7db chore: Uzbek translations 2026-06-28 03:01:47 +05:30
Ritvik Sardana
3622dfe4ac fix: dev mode 2026-06-27 23:35:33 +05:30
RitvikSardana
483d08c895 Merge pull request #3503 from RitvikSardana/fix/customer-ticket
fix: set customer only for new tickets
2026-06-27 20:57:58 +05:30
Ritvik Sardana
44571648bf fix: update logic 2026-06-27 20:44:53 +05:30
Ritvik Sardana
4474bfaa41 fix: cap analytic changes to 100% 2026-06-27 20:32:32 +05:30
Ritvik Sardana
12bc0109d2 fix: set customer only for new tickets 2026-06-27 20:29:19 +05:30
Ritvik Sardana
bdfe683918 Merge branch 'fix/bump-frappe-ui' of https://github.com/RitvikSardana/helpdesk into fix/bump-frappe-ui 2026-06-27 20:06:33 +05:30
Ritvik Sardana
93397d6e4e chore: remove dead code 2026-06-27 20:06:06 +05:30
RitvikSardana
8fe42b4762 Merge branch 'develop' into fix/bump-frappe-ui 2026-06-27 19:45:51 +05:30
RitvikSardana
fedc622288 Merge pull request #3501 from RitvikSardana/fix-scroll
fix: remove overscroll-behavior
2026-06-27 19:44:56 +05:30
Ritvik Sardana
6f44b64d1f fix: add view perm for new roles 2026-06-27 19:43:10 +05:30
Ritvik Sardana
b0308c22be fix: update frappe-ui version 2026-06-27 19:29:59 +05:30
MochaMind
bea5399cb1 chore: Swedish translations 2026-06-27 03:03:07 +05:30
Ritvik Sardana
b9a6b2091d feat: add Read and Select permissions for Agent role on HD Ticket Status 2026-06-26 18:20:02 +05:30
Ritvik Sardana
717a967484 fix: remove overscroll-behavior: none to fix scroll chaining issue 2026-06-26 18:17:07 +05:30
Sydney Gomes
7a0e3a80bb Merge pull request #3500 from aerodeval/fix-agent-availability
refactor: fetching agent availability
2026-06-26 12:51:33 +05:30
Sydney
9d1cbecc24 fix: use onError instead of catch for promise rejection in setAvailability 2026-06-26 12:37:01 +05:30
Sydney
01ad287670 fix: drop redundant per ticket fetch for current agent in AssignTo 2026-06-26 11:57:39 +05:30
Sydney
4b38d0c1f9 refactor: update frontend to read availability from store 2026-06-26 11:44:35 +05:30
Sydney
d2ea0ecd8d refactor: seed agent availability from session 2026-06-26 11:43:00 +05:30
MochaMind
3f2c19afab chore: Swedish translations 2026-06-26 02:16:26 +05:30
MochaMind
41f6ba8e42 chore: German translations 2026-06-26 02:16:23 +05:30
RitvikSardana
882465c6ea Merge pull request #3499 from RitvikSardana/fix/customer-dark-mode
fix: dark mode issues
2026-06-26 01:38:13 +05:30
Ritvik Sardana
6d2c88466f fix: return submit promise in remove contact dialog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 00:56:08 +05:30
RitvikSardana
b8794be86e Merge pull request #3488 from RitvikSardana/fix/dashboard-and-recent-tickets
fix: dedupe recent tickets, feedback rating by resolution date, staledashboard stats
2026-06-25 23:52:30 +05:30
Ritvik Sardana
cce4fc77b5 fix: stale tickets 2026-06-25 23:36:55 +05:30
Ritvik Sardana
2d99d87fca fix: badge color 2026-06-25 23:24:01 +05:30
Ritvik Sardana
d47ac23c8f fix: dark mode issues 2026-06-25 23:21:36 +05:30
RitvikSardana
9fa9b9c910 Merge pull request #3045 from RitvikSardana/refactor/customer-contact
feat!: customer contact management
2026-06-25 21:11:16 +05:30
Ritvik Sardana
10a910e6ff fix: validate primary contact 2026-06-25 13:26:06 +05:30
MochaMind
1c3f8d9a0a chore: Uzbek translations 2026-06-25 02:10:00 +05:30
MochaMind
b3eb622e51 chore: Bulgarian translations 2026-06-25 02:09:58 +05:30
MochaMind
76d735c02d chore: Polish translations 2026-06-25 02:09:40 +05:30
MochaMind
1f7298b38c chore: Hungarian translations 2026-06-25 02:09:35 +05:30
MochaMind
7dc62637fd chore: Arabic translations 2026-06-25 02:09:29 +05:30
MochaMind
ca5a98257f chore: French translations 2026-06-25 02:09:26 +05:30
MochaMind
4256969c5c chore: Bosnian translations 2026-06-24 01:55:05 +05:30
MochaMind
2690cab677 chore: Croatian translations 2026-06-24 01:55:02 +05:30
MochaMind
8ffd2ca107 chore: Swedish translations 2026-06-24 01:54:59 +05:30
MochaMind
0a7e8b5d74 chore: German translations 2026-06-24 01:54:55 +05:30
Ritvik Sardana
5eacadd4e2 fix: link a contact to customer while editing the contact 2026-06-23 20:47:12 +05:30
Ritvik Sardana
072779ada3 fix(styles): customer contact 2026-06-23 18:32:49 +05:30
Ritvik Sardana
213e75b06e fix: add empty state in contact feedback 2026-06-23 10:37:00 +05:30
MochaMind
79502dc7cd chore: Esperanto translations 2026-06-23 01:57:33 +05:30
MochaMind
f21335bcb2 chore: Serbian (Latin) translations 2026-06-23 01:57:30 +05:30
MochaMind
57a5d5621c chore: Norwegian Bokmal translations 2026-06-23 01:57:27 +05:30
MochaMind
cd7f1c23ff chore: Bosnian translations 2026-06-23 01:57:24 +05:30
MochaMind
959553e478 chore: Burmese translations 2026-06-23 01:57:21 +05:30
MochaMind
fd3ece06df chore: Hindi translations 2026-06-23 01:57:18 +05:30
MochaMind
a37cf0c9d9 chore: Croatian translations 2026-06-23 01:57:16 +05:30
MochaMind
aaf67c8497 chore: Thai translations 2026-06-23 01:57:13 +05:30
MochaMind
14f606aea7 chore: Persian translations 2026-06-23 01:57:10 +05:30
MochaMind
3a6c8b357e chore: Indonesian translations 2026-06-23 01:57:08 +05:30
MochaMind
223a7f31e3 chore: Portuguese, Brazilian translations 2026-06-23 01:57:05 +05:30
MochaMind
6dde923bac chore: Vietnamese translations 2026-06-23 01:57:02 +05:30
MochaMind
b7e500bcfc chore: Chinese Simplified translations 2026-06-23 01:56:59 +05:30
MochaMind
8645c240c8 chore: Turkish translations 2026-06-23 01:56:56 +05:30
MochaMind
2e8038f341 chore: Swedish translations 2026-06-23 01:56:53 +05:30
MochaMind
34e2639a19 chore: Serbian (Cyrillic) translations 2026-06-23 01:56:50 +05:30
MochaMind
47277c4794 chore: Slovenian translations 2026-06-23 01:56:48 +05:30
MochaMind
cb6f7dd5e7 chore: Russian translations 2026-06-23 01:56:45 +05:30
MochaMind
cf8581b56e chore: Portuguese translations 2026-06-23 01:56:42 +05:30
MochaMind
a2e36cd11b chore: Polish translations 2026-06-23 01:56:40 +05:30
MochaMind
1a3a084868 chore: Dutch translations 2026-06-23 01:56:37 +05:30
MochaMind
acd73e79c9 chore: Korean translations 2026-06-23 01:56:34 +05:30
MochaMind
d4af3c8695 chore: Italian translations 2026-06-23 01:56:31 +05:30
MochaMind
92258e51e4 chore: Hungarian translations 2026-06-23 01:56:29 +05:30
MochaMind
785e15386b chore: German translations 2026-06-23 01:56:26 +05:30
MochaMind
8040e1162a chore: Danish translations 2026-06-23 01:56:23 +05:30
MochaMind
26c26ea902 chore: Czech translations 2026-06-23 01:56:21 +05:30
MochaMind
667181452c chore: Arabic translations 2026-06-23 01:56:18 +05:30
MochaMind
8e720a9641 chore: Spanish translations 2026-06-23 01:56:15 +05:30
MochaMind
3a3c310437 chore: French translations 2026-06-23 01:56:13 +05:30
Ritvik Sardana
62c0cec4d2 fix(styles): customer/contact 2026-06-22 14:35:57 +05:30
RitvikSardana
5648ffc225 Merge pull request #3490 from frappe/pot_develop_2026-06-21
chore: update POT file
2026-06-22 12:36:03 +05:30
RitvikSardana
41f87f5279 Merge pull request #3478 from frappe/l10n_develop
chore: sync translations from crowdin
2026-06-22 12:34:31 +05:30
Ritvik Sardana
7b27b04677 fix(i18n): wrap untranslated strings in customer/contact UI
Wrap user-facing labels, button text and a toast message in __() so
they are picked up for translation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:32:19 +05:30
Ritvik Sardana
6329101c4c fix: improve copy and add agent manager perm to User Invitation 2026-06-22 02:18:57 +05:30
MochaMind
6187001eef chore: Bosnian translations 2026-06-22 01:58:18 +05:30
MochaMind
25f6b212e5 chore: Croatian translations 2026-06-22 01:58:14 +05:30
MochaMind
61ba2d37de chore: Persian translations 2026-06-22 01:58:11 +05:30
MochaMind
920fa0d928 chore: Portuguese translations 2026-06-22 01:58:02 +05:30
MochaMind
a3ac0b47ef chore: Italian translations 2026-06-22 01:57:58 +05:30
Ritvik Sardana
e748d55818 fix: replace image styles 2026-06-21 22:25:05 +05:30
Ritvik Sardana
9b13a57ad7 refactor(customer): contact management code 2026-06-21 21:36:24 +05:30
Ritvik Sardana
156f09bf4d fix: add role data in new roles 2026-06-21 20:46:24 +05:30
Ritvik Sardana
21bcd8b6b1 refactor: contact composable file 2026-06-21 19:36:29 +05:30
Ritvik Sardana
ae9c61f55a fix: make customer_contact patch better 2026-06-21 18:46:37 +05:30
Ritvik Sardana
c54adb2f8c fix: remove dead code 2026-06-21 18:44:55 +05:30
frappe-pr-bot
a9d5d68a17 chore: update POT file 2026-06-21 11:29:53 +00:00
Ritvik Sardana
1cb03e6a0a fix: remove dead code 2026-06-21 16:48:32 +05:30
Ritvik Sardana
063172cb2e fix: feedback access agents 2026-06-21 15:30:13 +05:30
Ritvik Sardana
f660cad1bb fix(customer): keep manager role when contact manages another customer 2026-06-21 15:12:36 +05:30
Ritvik Sardana
acd1053e48 fix: grill-me suggestions 2026-06-21 14:03:09 +05:30
MochaMind
4df56477f0 chore: Swedish translations 2026-06-21 02:00:37 +05:30
Ritvik Sardana
8bf1e7bca8 fix(analytics): neutral arrow if no changes 2026-06-20 18:04:36 +05:30
Ritvik Sardana
0618c671d8 fix: align feedback trend chart with number-card on resolution_date
get_feedback_trend_data plotted feedback by creation while
get_avg_feedback_score averages by resolution_date, so the trend and the
number card drew from different ticket populations. Key the trend off
resolution_date too so the graph explains the number-card figure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:40:06 +05:30
Ritvik Sardana
2ed7cf62fb fix: dedup recent tickets, feedback rating by resolution date, stale dashboard stats
- get_recent_tickets: exclude current + org tickets from the raised_by query
  so a ticket matching both customer and raised_by no longer appears twice.
- Dashboard avg feedback rating: filter on resolution_date instead of
  creation, so every ticket resolved in the period is counted regardless of
  when it was raised.
- Dashboard.vue: drop the static frappe-ui cache keys on the stat resources;
  the shared cached instance stayed bound to a previous mount's filters and
  sent stale params after navigating away and back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:28:40 +05:30
Ritvik Sardana
81ece4d572 fix: edit customer name 2026-06-20 17:15:44 +05:30
Ritvik Sardana
ff4fa6e54d fix(perms): hd_customer 2026-06-20 15:09:38 +05:30
MochaMind
e7f2bafe49 chore: Swedish translations 2026-06-20 01:47:52 +05:30
Ritvik Sardana
7e5f6766d2 fix(styles): customer/contact analytics 2026-06-19 13:27:12 +05:30
Ritvik Sardana
18141f4fc4 fix: add contact role update confirmation 2026-06-19 12:49:35 +05:30
Ritvik Sardana
f9f850d250 fix: customer selection flow while creating ticket 2026-06-19 12:19:46 +05:30
Ritvik Sardana
1eaa7c15e6 chore: copy cleanup 2026-06-19 03:52:34 +05:30
Ritvik Sardana
a693a8b33f fix: add patch to backfill customer_type in existing customers 2026-06-19 02:33:58 +05:30
Ritvik Sardana
0298b9aaac fix: delete customer contact logic 2026-06-19 02:21:24 +05:30
MochaMind
35c777ac49 chore: Bosnian translations 2026-06-19 01:22:36 +05:30
MochaMind
21cfccb16e chore: Croatian translations 2026-06-19 01:22:34 +05:30
Ritvik Sardana
9188bd9770 fix: image uploader in contact customer 2026-06-18 19:03:08 +05:30
Ritvik Sardana
95425fc1f0 fix: code cleanup in contact 2026-06-18 17:37:40 +05:30
Ritvik Sardana
5b5e04477a fix: show linked customers in contact 2026-06-18 17:30:59 +05:30
Ritvik Sardana
d876608b0a fix: new customer dialog styles 2026-06-18 16:56:15 +05:30
Ritvik Sardana
df8386d9b7 fix: label styling 2026-06-18 16:17:35 +05:30
Ritvik Sardana
6bd8ed03fe fix: update modals according to frappe-ui update 2026-06-18 11:51:56 +05:30
MochaMind
a2ad9a9448 chore: Bosnian translations 2026-06-18 00:52:15 +05:30
Ritvik Sardana
2950677756 refactor: hd_customer doctype 2026-06-17 21:11:59 +05:30
Ritvik Sardana
44b8acdab3 fix: update list view of customer doctype 2026-06-17 20:32:13 +05:30
Ritvik Sardana
45af71ad9a fix: add primary contact in new customer dialog 2026-06-17 20:12:47 +05:30
Sydney Gomes
13c3f2ef4d Merge pull request #3486 from aerodeval/fix-deploy-issue-type
fix: deploy type issue
2026-06-17 15:33:03 +05:30
Sydney
f20e957f4b fix: deploy type issue 2026-06-17 15:20:25 +05:30
Sydney Gomes
aba29b26b5 Merge pull request #3481 from aerodeval/fix-ticket-cutoff
fix: ticket unexpectedly auto-closed due to inactivity
2026-06-17 03:12:08 +05:30
Sydney
f106e04c04 fix: linter issues 2026-06-17 03:00:48 +05:30
Sydney
0b191398e3 fix: remove failing lint file: 2026-06-17 02:58:22 +05:30
Sydney
d216bbd0a3 fix: update all communications for the ticket 2026-06-16 18:38:36 +05:30
Sydney
6d0ed2ea83 fix: compute auto-close inactivity cutoff in system timezone 2026-06-16 17:52:26 +05:30
MochaMind
ac8f4b041c chore: Esperanto translations 2026-06-16 00:21:31 +05:30
MochaMind
9f08f171a1 chore: Serbian (Latin) translations 2026-06-16 00:21:29 +05:30
MochaMind
356a53b816 chore: Norwegian Bokmal translations 2026-06-16 00:21:26 +05:30
MochaMind
67a8d95c34 chore: Bosnian translations 2026-06-16 00:21:24 +05:30
MochaMind
7c5c1439df chore: Burmese translations 2026-06-16 00:21:22 +05:30
MochaMind
8719d7d67c chore: Hindi translations 2026-06-16 00:21:19 +05:30
MochaMind
a62c796d14 chore: Croatian translations 2026-06-16 00:21:16 +05:30
MochaMind
ae9b6ae999 chore: Thai translations 2026-06-16 00:21:14 +05:30
MochaMind
1c9916bed0 chore: Persian translations 2026-06-16 00:21:11 +05:30
MochaMind
691d2f8c00 chore: Indonesian translations 2026-06-16 00:21:08 +05:30
MochaMind
8b4b199f89 chore: Portuguese, Brazilian translations 2026-06-16 00:21:06 +05:30
MochaMind
6292eb9a43 chore: Vietnamese translations 2026-06-16 00:21:03 +05:30
MochaMind
499a7aaebb chore: Chinese Simplified translations 2026-06-16 00:21:00 +05:30
MochaMind
8742d4b87e chore: Turkish translations 2026-06-16 00:20:58 +05:30
MochaMind
200346f101 chore: Swedish translations 2026-06-16 00:20:55 +05:30
MochaMind
d101e659f4 chore: Serbian (Cyrillic) translations 2026-06-16 00:20:53 +05:30
MochaMind
2c66ac9eff chore: Slovenian translations 2026-06-16 00:20:50 +05:30
MochaMind
903ee9fad3 chore: Russian translations 2026-06-16 00:20:47 +05:30
MochaMind
ba46e54248 chore: Portuguese translations 2026-06-16 00:20:44 +05:30
MochaMind
1fb6e69345 chore: Polish translations 2026-06-16 00:20:42 +05:30
MochaMind
23e8443342 chore: Dutch translations 2026-06-16 00:20:39 +05:30
MochaMind
d158336851 chore: Korean translations 2026-06-16 00:20:36 +05:30
MochaMind
447d8ce540 chore: Italian translations 2026-06-16 00:20:33 +05:30
MochaMind
7d6633975c chore: Hungarian translations 2026-06-16 00:20:30 +05:30
MochaMind
2ab9ea47af chore: German translations 2026-06-16 00:20:27 +05:30
MochaMind
cf04339d30 chore: Danish translations 2026-06-16 00:20:25 +05:30
MochaMind
8647b7247e chore: Czech translations 2026-06-16 00:20:22 +05:30
MochaMind
1101402b97 chore: Arabic translations 2026-06-16 00:20:19 +05:30
MochaMind
c4c134b858 chore: Spanish translations 2026-06-16 00:20:17 +05:30
MochaMind
3101a55d48 chore: French translations 2026-06-16 00:20:14 +05:30
Ritvik Sardana
b55e7474c3 refactor: make HD Customer primary contact optional
Stop auto-defaulting primary_contact to the first contact on save. Since
the primary contact is a manager, defaulting it would silently promote an
arbitrary contact to manager. Primary is now optional and only validated
when set; designating a primary still makes that contact a manager.

Update tests to cover the new behaviour.
2026-06-15 16:43:59 +05:30
RitvikSardana
57db7d773d Merge pull request #3474 from frappe/l10n_develop
chore: sync translations from crowdin
2026-06-15 13:25:05 +05:30
RitvikSardana
3ddf53fa24 Merge pull request #3473 from frappe/pot_develop_2026-06-14
chore: update POT file
2026-06-15 13:06:10 +05:30
RitvikSardana
0aefa8a990 Merge pull request #3475 from RitvikSardana/filter-issues
fix: assigned_on filter
2026-06-15 12:05:53 +05:30
Ritvik Sardana
7c0d464b19 fix: filter assigned_on 2026-06-15 12:03:32 +05:30
MochaMind
048e090978 chore: Esperanto translations 2026-06-14 22:46:48 +05:30
MochaMind
14313d1512 chore: Serbian (Latin) translations 2026-06-14 22:46:45 +05:30
MochaMind
0f367d3ee8 chore: Norwegian Bokmal translations 2026-06-14 22:46:43 +05:30
MochaMind
2032f0e6ce chore: Bosnian translations 2026-06-14 22:46:41 +05:30
MochaMind
c20df2883a chore: Burmese translations 2026-06-14 22:46:39 +05:30
MochaMind
a8fe77ed04 chore: Hindi translations 2026-06-14 22:46:37 +05:30
MochaMind
996f3cafd3 chore: Croatian translations 2026-06-14 22:46:35 +05:30
MochaMind
988c9dff77 chore: Thai translations 2026-06-14 22:46:32 +05:30
MochaMind
3b3fda017e chore: Persian translations 2026-06-14 22:46:30 +05:30
MochaMind
69be149305 chore: Indonesian translations 2026-06-14 22:46:28 +05:30
MochaMind
da5f35cff8 chore: Portuguese, Brazilian translations 2026-06-14 22:46:26 +05:30
MochaMind
d696354a08 chore: Vietnamese translations 2026-06-14 22:46:24 +05:30
MochaMind
8518cdee98 chore: Chinese Simplified translations 2026-06-14 22:46:21 +05:30
MochaMind
2f3ece2b72 chore: Turkish translations 2026-06-14 22:46:19 +05:30
MochaMind
06334aa8d3 chore: Swedish translations 2026-06-14 22:46:17 +05:30
MochaMind
c960ec66ee chore: Serbian (Cyrillic) translations 2026-06-14 22:46:15 +05:30
MochaMind
ad85f77a19 chore: Slovenian translations 2026-06-14 22:46:12 +05:30
MochaMind
5ac09b533a chore: Russian translations 2026-06-14 22:46:10 +05:30
MochaMind
0fe243c076 chore: Portuguese translations 2026-06-14 22:46:08 +05:30
MochaMind
77321ebe91 chore: Polish translations 2026-06-14 22:46:05 +05:30
MochaMind
5921597d4a chore: Dutch translations 2026-06-14 22:46:03 +05:30
MochaMind
57d76f6739 chore: Korean translations 2026-06-14 22:46:01 +05:30
MochaMind
bd29aa9f6a chore: Italian translations 2026-06-14 22:45:59 +05:30
MochaMind
ab56e09cb7 chore: Hungarian translations 2026-06-14 22:45:57 +05:30
MochaMind
faf3a1e9f6 chore: German translations 2026-06-14 22:45:54 +05:30
MochaMind
6c16a8e0f5 chore: Danish translations 2026-06-14 22:45:52 +05:30
MochaMind
c455caf515 chore: Czech translations 2026-06-14 22:45:50 +05:30
MochaMind
312e7cfdd4 chore: Arabic translations 2026-06-14 22:45:48 +05:30
MochaMind
e753544ddf chore: Spanish translations 2026-06-14 22:45:46 +05:30
MochaMind
5d45612436 chore: French translations 2026-06-14 22:45:43 +05:30
Ritvik Sardana
988d0eddf1 refactor(invitation: unified invitation flow for existing and new contacts 2026-06-14 20:25:22 +05:30
RitvikSardana
050f952ee2 Merge pull request #3449 from RitvikSardana/animate-filter
fix: add animation in filter component
2026-06-14 17:41:10 +05:30
Ritvik Sardana
97a4d66caa fix: initial focus state 2026-06-14 17:19:28 +05:30
Ritvik Sardana
ed255f4de3 feat: improve filter UX and rename filter module
- transactional replace: keep the existing condition until a new one is
  applied, swapping in place instead of deleting on click
- auto-return to overview after a discrete value pick (equals, is,
  timespan, single date/range)
- operator cycling shortcut moved to Alt/Opt + Up/Down (directional,
  no browser conflict) and works in list mode
- keyboard navigation for the 'Is' (Set/Not Set) list
- loading state for link-field search
- accessibility: listbox/option roles, aria-labels, focus-visible row
  actions, truncation tooltips
- rename filterCore.ts -> filter.ts, useFilterCore -> useFilter
2026-06-14 17:00:56 +05:30
frappe-pr-bot
c0af24a43d chore: update POT file 2026-06-14 11:18:42 +00:00
Ritvik Sardana
c742146f82 fix: add replace filter functionality 2026-06-14 16:17:40 +05:30
Sydney Gomes
276abf5024 Merge pull request #3472 from aerodeval/fix-custom-template-fields
fix: badges having borders & width truncation for texts
2026-06-12 01:33:15 +05:30
Sydney
38341f6483 fix: badges having borders & width truncation for texts 2026-06-12 01:29:21 +05:30
Sydney Gomes
95f10986d1 Merge pull request #3448 from aerodeval/fix-ui-issues
fix: UI bugs
2026-06-12 00:57:19 +05:30
Sydney Gomes
fe163921c7 Merge branch 'develop' into fix-ui-issues 2026-06-12 00:42:33 +05:30
Sydney
342b0566f9 fix: increase width for labels 2026-06-12 00:39:09 +05:30
Sydney
5dee0bf042 fix: add borders to customer fields 2026-06-12 00:36:54 +05:30
Sydney
3c2b1477cd fix: delete ticket error 2026-06-12 00:17:48 +05:30
Sydney
e3b9d8d01a Revert "fix: sidebar fc banner spacing"
This reverts commit e2b5619cc6.
2026-06-12 00:02:09 +05:30
RitvikSardana
cd5e49ef71 Merge pull request #3412 from frappe/pot_develop_2026-05-31
chore: update POT file
2026-06-11 23:27:27 +05:30
RitvikSardana
5f62da6aea Merge pull request #3410 from frappe/l10n_develop
chore: sync translations from crowdin
2026-06-11 23:25:27 +05:30
Sydney
a1ed7f5623 fix: spacing and alligment for tabs 2026-06-11 13:25:40 +05:30
Ritvik Sardana
cd06d9a550 fix: add animation in filter component 2026-06-11 12:22:27 +05:30
Sydney
6c3d8e7bc6 fix: improve customer portal layouting 2026-06-11 11:49:15 +05:30
Sydney
2c8c6b7266 fix: add back toggle theme to customer portal 2026-06-11 11:01:18 +05:30
RitvikSardana
ed7fe3b884 Merge pull request #3447 from RitvikSardana/refactor/filter
refactor: filter component UX
2026-06-11 02:42:00 +05:30
Ritvik Sardana
e0f5c81eac chore: code cleanup 2026-06-11 02:27:27 +05:30
Ritvik Sardana
22059e455f chore: rename component 2026-06-11 02:23:04 +05:30
Ritvik Sardana
460fd2bc24 Merge branch 'develop' of https://github.com/frappe/helpdesk into refactor/filter 2026-06-11 02:20:27 +05:30
Ritvik Sardana
f079593bf0 refactor: filter component UX 2026-06-11 02:16:13 +05:30
RitvikSardana
46b11fb618 Merge pull request #3446 from RitvikSardana/fix/field-dep
fix(field-dependency): show selected values first, refresh options
2026-06-11 01:16:06 +05:30
Sydney
cb7490ccb2 fix: broken switches in field dependency and assignment rules 2026-06-10 18:05:17 +05:30
Sydney
e93aaad32b fix: assign to agent indicators fix 2026-06-10 16:39:24 +05:30
Sydney
e2b5619cc6 fix: sidebar fc banner spacing 2026-06-10 16:38:28 +05:30
Sydney
9160028ef7 fix: collapsed response 3 dots to be added 2026-06-10 16:37:58 +05:30
Ritvik Sardana
1140533f1b fix(field-dependency): show selected values first, refresh options, fix toggle
- Order child values that were selected on load to the top, frozen to the
  saved selections so the list does not reorder while toggling
- Clear the cached field options when the editor mounts so newly created
  link/select values (e.g. a new ticket type) appear instead of stale ones
- Update the list-view enabled toggle to reflect state locally so disabling
  actually persists (the controlled Switch was re-sending the same value)
2026-06-10 13:14:05 +05:30
Ritvik Sardana
9b8fc7e376 fix: better UX for editing contact 2026-06-10 12:43:53 +05:30
MochaMind
8e46d58491 chore: Hungarian translations 2026-06-09 20:13:58 +05:30
Sydney Gomes
4e1150211d Merge pull request #3439 from aerodeval/fix-agent-resource-call
fix: avoid calling agent availability for customer
2026-06-09 14:00:16 +05:30
Sydney
b01cde3cad fix: avoid calling agent availability for customer 2026-06-09 13:47:07 +05:30
RitvikSardana
d4fc6c4ad6 Merge pull request #3438 from RitvikSardana/lv/filter-placeholder
fix: placeholder in filters
2026-06-09 13:15:59 +05:30
Ritvik Sardana
e3d4cede79 fix: placeholder in filters 2026-06-09 13:14:15 +05:30
RitvikSardana
2d34952cd2 Merge pull request #3437 from RitvikSardana/lv/failed-sla
fix(Ticket ListView): don't show failed resolution when the ticket it not resolved
2026-06-09 13:03:02 +05:30
Ritvik Sardana
fc417dc5e8 fix(overflow): first field in quick filters 2026-06-09 12:52:43 +05:30
Ritvik Sardana
baf2d88e38 fix: dont show failed resolution when the ticket it not resolved 2026-06-09 12:52:13 +05:30
RitvikSardana
db85ad6819 Merge pull request #3436 from RitvikSardana/focus-saved-reply
fix(multiselect): click pill to copy its value
2026-06-09 12:23:20 +05:30
Ritvik Sardana
40a5cfefa1 feat(multiselect): click pill to copy its value
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:55:05 +05:30
Ritvik Sardana
42cdaa80ea fix(saved-replies): keep email box open when dismissing modal
Clicking the saved-replies modal backdrop also collapsed the reply box:
the Dialog portals to body and its backdrop (.dialog-overlay) lacks
role="dialog", so CommunicationArea's onClickOutside treated it as an
outside click. Ignore .dialog-overlay so dialog-portal clicks are skipped.

Also guards editorRef access and autofocuses the search input on open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:35:57 +05:30
Sydney Gomes
5ce25aade7 Merge pull request #3435 from aerodeval/fix-local-agents
fix: allow agents added locally to reflect their status
2026-06-09 02:14:53 +05:30
Sydney
171aa2a571 fix: allow agents added locally to reflect their status 2026-06-09 02:00:41 +05:30
RitvikSardana
47708e7e7e Merge pull request #3434 from RitvikSardana/fix/recent-tix
fix: recents tickets condition
2026-06-09 00:38:06 +05:30
Ritvik Sardana
5e2aa06b4a fix: recents tickets condition 2026-06-09 00:25:22 +05:30
Sydney Gomes
9c72a5291e Merge pull request #3431 from aerodeval/agent-status-ux
fix: improve agent status UX
2026-06-08 14:04:36 +05:30
Sydney
e3b9e4047f fix: dont show toast when self assigning a ticket 2026-06-08 13:56:38 +05:30
Sydney
6ba980ec49 fix: improve ux copy of tooltip 2026-06-08 13:28:49 +05:30
Sydney
60b4a0975e fix: improve ux of the HD agent status doctype 2026-06-08 13:28:15 +05:30
Sydney Gomes
8e94b78133 Merge pull request #3427 from aerodeval/refactor-assignment-notification
refactor: point ticket assignment notifications to agent portal
2026-06-07 15:06:01 +05:30
Sydney Gomes
017ddf49f1 Merge pull request #3372 from aerodeval/feat-agent-status
feat: agent availability status
2026-06-07 00:56:58 +05:30
Sydney
48cc2068b6 fix: display availability select to only agents with HD agent 2026-06-07 00:38:39 +05:30
Sydney
3d113d29f2 fix: string for availability status 2026-06-06 17:26:23 +05:30
Sydney
81b85f259f fix: failing test cases 2026-06-06 17:15:45 +05:30
Sydney
5a7bfe0d4e fix: bulk update agent status in patch 2026-06-06 16:55:20 +05:30
Sydney
379d5a25d3 fix: chage agent status order to status_order and its weights 2026-06-06 16:42:56 +05:30
Sydney
14b39007d3 fix: have atleast one active category available & enabled 2026-06-06 16:21:00 +05:30
Sydney
11de600874 fix: move make_status to test_utils 2026-06-06 16:09:13 +05:30
Sydney Gomes
ae1a25dd2c Merge pull request #3428 from aerodeval/empty-state-fix
fix: empty state fix for cross browser compatibility
2026-06-05 23:18:55 +05:30
Sydney
3f4abcd49e fix: empty state fix for cross browser compatibility 2026-06-05 23:07:03 +05:30
Sydney
194913f303 refactor: extend notification log to route to HD portal on assignment 2026-06-05 20:46:50 +05:30
MochaMind
f13a64d23e chore: Swedish translations 2026-06-05 19:10:43 +05:30
Sydney
d4a22633eb fix: delay toast of assignment succesful for away agents 2026-06-05 12:11:47 +05:30
Sydney Gomes
c5281a66ad Merge branch 'develop' into feat-agent-status 2026-06-05 11:32:51 +05:30
RitvikSardana
559d77c5ba Merge pull request #3416 from frappe/erpnext-doc-events
refactor(ERPNext Integration): use doc_events instead of override_doctype_class
2026-06-05 09:32:04 +05:30
Ritvik Sardana
e6236388f3 fix: better copy 2026-06-05 09:14:27 +05:30
Ritvik Sardana
f06db95103 fix: add loading state 2026-06-05 09:08:11 +05:30
RitvikSardana
dbf59f78e6 Merge pull request #3422 from RitvikSardana/upgrade-frappe-ui
refactor: Upgrade frappe UI
2026-06-05 08:59:57 +05:30
Sydney
7c1851c150 fix: allow hd agent manager to create and edit status 2026-06-05 01:43:26 +05:30
Sydney
10441bdaca fix: add setup and teardown for testcases 2026-06-04 23:03:57 +05:30
Sydney
3cc3b2307c fix: use frappe intergration testcase 2026-06-04 20:10:07 +05:30
Sydney
477f850b36 fix: user frappe intergration testcase 2026-06-04 20:09:53 +05:30
Sydney
1c5ffb9a72 fix: merge conflicts 2026-06-04 19:57:16 +05:30
Sydney Gomes
b1324999f0 Merge branch 'develop' into feat-agent-status 2026-06-04 19:35:48 +05:30
Sydney
de6ed69a32 fix: improve mobile experience and code cleanup 2026-06-04 19:34:16 +05:30
Sydney
7bbc3129cc refactor: use socket communicaiton to update agnet status 2026-06-04 19:33:22 +05:30
Sydney
599f8d11e4 fix: add get_agent_name in utils 2026-06-04 19:22:34 +05:30
Ritvik Sardana
cf0306db47 fix(tickets): open status/assignee dropdowns via shortcut on frappe-ui v1
frappe-ui v1's Dropdown (reka-ui) renders the slotted trigger Button with
`as-child` and portals the menu, so the old `nextElementSibling.click()`
hit the adjacent toolbar button (or nothing) instead of the dropdown.
Click the trigger element itself, which now IS the dropdown trigger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:45:17 +05:30
Ritvik Sardana
839232774e chore: improve copy 2026-06-04 18:39:47 +05:30
Ritvik Sardana
8c84d631b1 fix: persist user preference in ticket side panel 2026-06-04 18:13:36 +05:30
Ritvik Sardana
5805af9e15 fix(tickets): guard getStatusColor against undefined status
getStatus() returns undefined while the HD Ticket Status list resource
is still loading (and for any unknown/disabled status). The recent-tickets
section renders off a separate resource that can resolve first, so the
status badges destructured `color` off undefined and threw on every
ticket detail load. Default to {} so the existing colorMap fallback applies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:59:23 +05:30
Ritvik Sardana
7a0c3f9f86 fix: customer&contact dialog 2026-06-04 17:23:15 +05:30
Sydney
14c1b1a6b0 fix: imrove tooltip copy 2026-06-04 16:34:01 +05:30
Ritvik Sardana
d02c88c6d1 fix: update status indicator styling 2026-06-04 14:30:15 +05:30
Ritvik Sardana
7d004beea6 fix(tickets): trust agreement_status for resolution badge
The resolution-by list cell recomputed SLA state client-side and showed
"NaNm" when resolution_by was null (e.g. older tickets with a
resolution_date but no SLA deadline). Use the server-computed
agreement_status for Fulfilled/Failed/Paused and only show a live
countdown for an in-progress ticket with a future deadline; mark
overdue-unresolved as Failed.

Also show remaining hours alongside days in shortDuration (e.g.
"4 days 4h").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:28:59 +05:30
Sydney
9ec4bbe5f5 fix: allow default status to be set 2026-06-04 14:17:00 +05:30
Ritvik Sardana
65a813becb fix: source dayjs from frappe-ui in remaining files
These files imported the raw "dayjs" package and relied on plugins
(relativeTime, localizedFormat, utc, timezone) that the deleted
dayjs.ts wrapper extended on the shared singleton. Point them at
frappe-ui's pre-extended dayjs so .fromNow()/.format()/.tz() work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:05:25 +05:30
Ritvik Sardana
cadcc36ae6 refactor: use dayjs from frappe-ui, remove local dayjs wrapper
Drop desk/src/dayjs.ts and source dayjs from frappe-ui instead. The
shortDuration helper moves to utils, and the custom .long() prototype
extension is replaced with .format("LLLL") at its call sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:01:19 +05:30
Sydney
374749adae fix: improve tooltip copy 2026-06-04 13:48:54 +05:30
Ritvik Sardana
a9dff6c13c fix(ui): render lucide and emoji view icons in views dropdown
ViewBreadcrumbs fed string icons straight to FeatherIcon, which renders a
circle placeholder for lucide-* names. Use the local Icon component, which
routes emoji, lucide-* masks, feather names and components correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:41:08 +05:30
Ritvik Sardana
7b54852a6e fix(ui): use system date/time format and adapt to v1 range tuple model
- add getDateFormat/getTimeFormat utils backed by the site's boot data
  (window.date_format / window.time_format) and use them in dateFormat,
  getFormattedDate, and all single-date pickers instead of hardcoded
  DD-MM-YYYY
- frappe-ui v1 DateRangePicker models a [from, to] tuple and emits [] on
  clear; Dashboard, AvgTimeMetrics and RecentFeedback kept the legacy
  'from,to' string for state and API params, so custom ranges broke.
  Convert at the picker boundary (computed/split-join) and guard clears
  with range?.length

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:22:02 +05:30
Ritvik Sardana
b3620cc7ec fix(ui): restore Columns/Sort button labels under frappe-ui v1
v1 Button renders slots.default() ?? label, and a bare <template v-if>
registers the default slot even when false, so the label never rendered.
Use the #icon slot for the icon-only (hideLabel) mode and a conditional
#prefix otherwise, leaving the default slot genuinely absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:11:03 +05:30
Ritvik Sardana
42d8f369fd fix(ui): render lucide icon strings in custom option renderers and v1 Select slots
- add renderOptionIcon util: lucide-* strings as CSS-mask spans, legacy
  feather names via FeatherIcon, components as-is; use it in
  TemplateOption and Agents RoleOption (FeatherIcon renders a circle
  placeholder for unknown names like lucide-trash-2)
- buildPercentageChange returns lucide names; CardBase renders the mask span
- Icon.vue handles lucide-* strings
- SavedReplyView: Select #label slot is the form-label override in v1
  (receives { required }, not { option }) and crashed the view; use
  #item-prefix, which v1 also reuses for the selected value in the trigger

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:06:18 +05:30
Sydney
6c996e9f71 fix: update agent status & add tests 2026-06-04 12:51:43 +05:30
Sydney
2be0694fe0 fix: reallign with new hd status category 2026-06-04 12:15:26 +05:30
Ritvik Sardana
40330c3a11 chore: bump frappe-ui submodule to v1.0.0-beta.3
The vite dev server aliases frappe-ui to this submodule when its
node_modules exist (vite-helpers.ts), so the checkout must match the npm
dependency or the v1-migrated code runs against the v0 source in dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:10:37 +05:30
Sydney
341591faf2 feat: add availability menu to profile and bring prreferences section to frontend 2026-06-04 11:50:37 +05:30
Ritvik Sardana
6a95bd30ec fix(ui): declare modelValue on ConfirmDialog to restore v-model control
In v0 the parent's v-model fell through onto the inner Dialog and won over
the wrapper's internal state. v1's Dialog treats the canonical open prop as
authoritative, so the internal showDialog default (true) kept the dialog
permanently visible. Declare an optional modelValue and resolve visibility
through a computed, preserving both the v-model mode and the v-if +
show()/hide() mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:48:36 +05:30
Ritvik Sardana
a801a3985e refactor(ui): replace deprecated Input with TextInput
The legacy Input component warns in frappe-ui v1. Swap to TextInput:
- @input value handlers -> @update:model-value (TextInput attrs fall
  through to the native input, so @input would receive an Event)
- icon-left -> #prefix slot with the lucide icon component
- debounce -> bound number prop; drop the removed inputClass prop
- drop the global Input registration in main.js

Switch/Rating/Divider needed no changes (plain v-model usage only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:38:46 +05:30
Ritvik Sardana
3938ea4dd0 refactor(ui): migrate date pickers to frappe-ui v1 API
- placement -> side/align on DateRangePicker
- formatter function prop (removed in v1) -> format string, preserving the
  DD-MM-YYYY display; drop the no-longer-supported debounce prop
- deprecated @change -> @update:model-value
- AddHolidayModal: reactive :value -> :model-value with the documented
  YYYY-MM-DD model format (v1 treats value as uncontrolled-initial only)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:30:02 +05:30
Ritvik Sardana
fef0c72802 refactor(ui): migrate feather icon strings to lucide
Prefix all feather icon-name strings with lucide- across icon/icon-left/
icon-right props and icon: option fields, per frappe-ui v1. lucide-static
ships every feather name as an alias, so the mapping is a 1:1 prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:20:10 +05:30
MochaMind
3aef20c244 chore: Hungarian translations 2026-06-03 18:14:40 +05:30
Ritvik Sardana
812b758c35 refactor(ui): migrate Dialog usages to frappe-ui v1 API
Flatten the deprecated options blob to top-level props, switch v-model to
v-model:open, rename #body-content->#default and #body-title->#title, and
convert full-override #body/#body-main slots to #default + the bare prop.
disableOutsideClickToClose becomes :dismissible (inverted).

TextEditor usage is intentionally left on the v0 API (deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:14:00 +05:30
Ritvik Sardana
099c78d6cc chore(deps): upgrade frappe-ui to 1.0.0-beta.3
Bump frappe-ui from 0.1.278 (v0) to the v1 beta. Add explicit @tiptap v3
packages used directly by tiptap-extensions.ts so they no longer rely on
hoisting from frappe-ui.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:12:00 +05:30
Ritvik Sardana
573ade93dc fix: improve integration layout 2026-06-03 17:26:32 +05:30
Ritvik Sardana
78266f45a6 fix: feedback layout component 2026-06-03 17:21:08 +05:30
Sydney
75e4bbebec fix: use agent status from HD Agent Status 2026-06-03 15:46:31 +05:30
Ritvik Sardana
ac44791470 fix: move ERPNext integration components 2026-06-03 10:08:29 +05:30
MochaMind
a2dc3d34ac chore: Chinese Simplified translations 2026-06-02 17:49:27 +05:30
Ritvik Sardana
8741685e2b fix(PhoneControl): parse existing values in a better way 2026-06-02 11:14:28 +05:30
RitvikSardana
c778f533b2 Merge pull request #3417 from RitvikSardana/security-new-ticket
fix: add session user as raised_by while creating from the API
2026-06-02 11:09:38 +05:30
Ritvik Sardana
61c90a1ea2 fix: add session user as raised_by while creating from the API 2026-06-02 11:08:40 +05:30
Sydney
5b2800b6a4 fix: connect HD Agent with HD Agent status 2026-06-02 01:21:53 +05:30
Sydney
c0cca4c07a feat: create HD Ticket Agent Status 2026-06-02 01:17:48 +05:30
Ritvik Sardana
1d72408609 Merge branch 'develop' of https://github.com/frappe/helpdesk into erpnext-doc-events 2026-06-02 01:09:35 +05:30
Ritvik Sardana
96e502e472 fix: better UX for desk button 2026-06-02 01:05:31 +05:30
Ritvik Sardana
86c3ac52b7 refactor: make field syncing extensible 2026-06-01 23:38:20 +05:30
Ritvik Sardana
86dc72ed4e refactor(UI): enable integration settings 2026-06-01 22:42:11 +05:30
Sydney Gomes
ace1fd7c17 Merge pull request #3415 from aerodeval/fix-rtl-regression
fix: UI bugs caused due to RTL
2026-06-01 20:01:48 +05:30
Sydney
4621b799cd fix: add setting modal fixes 2026-06-01 19:38:26 +05:30
Sydney
bfc79662d1 fix: rtl regression 2026-06-01 19:02:13 +05:30
MochaMind
4451389e9e chore: Hungarian translations 2026-06-01 16:59:05 +05:30
Sydney Gomes
025927fb48 Merge pull request #3174 from aerodeval/feat/rtl-support
feat: rtl support in helpdesk
2026-06-01 16:31:42 +05:30
Sydney
febb8675c5 fix: improve ticket merge copy 2026-06-01 15:53:56 +05:30
RitvikSardana
61b9d02a8b Merge pull request #3414 from RitvikSardana/erpnext-not-installed
fix: check if ERPNext is installed
2026-06-01 15:48:34 +05:30
Sydney
f17ec115b6 fix: ui bugs in rtl 2026-06-01 15:39:50 +05:30
Sydney
3a30e3d4a5 refactor: add a back button in settings layout base and removed reused code 2026-06-01 15:20:01 +05:30
Ritvik Sardana
d60ec14bcb fix: check if erp_next is installed 2026-06-01 14:23:14 +05:30
Sydney
9a8044cf54 fix: revert back token changes 2026-06-01 12:32:18 +05:30
Ritvik Sardana
eae877c1c3 refactor: use doc_events instead of override_doctype 2026-06-01 12:19:59 +05:30
RitvikSardana
492538908a Merge pull request #3393 from saadchaudharry/fix/3335-filter-agents-on-initial-load
refactor: get_users function to optimize role fetching and user retrieval
2026-06-01 11:56:48 +05:30
Sydney
b55c34e2fc fix: bugs and inconsistencies missed out 2026-05-31 22:50:10 +05:30
MochaMind
37f174cddd chore: Korean translations 2026-05-31 16:44:11 +05:30
MochaMind
bc99a267e3 chore: Czech translations 2026-05-31 16:44:08 +05:30
frappe-pr-bot
49eb0f257f chore: update POT file 2026-05-31 11:07:09 +00:00
Sydney
0b248ef283 fix: add remaining rtl fixes to newly made changes 2026-05-31 14:24:21 +05:30
Sydney
b1b7d01617 Merge remote-tracking branch 'upstream/develop' into feat/rtl-support
# Conflicts:
#	desk/src/components/MultipleAvatar.vue
#	desk/src/components/SavedRepliesSelectorModal.vue
#	desk/src/components/Settings/Assignment Rules/AssignmentRulesListView.vue
#	desk/src/components/Settings/EmailAccountList.vue
#	desk/src/components/Settings/Holiday/HLCalender.vue
#	desk/src/components/Settings/Holiday/HolidayList.vue
#	desk/src/components/Settings/SavedReplies/SavedRepliesList.vue
#	desk/src/components/Settings/SavedReplies/components/PreviewDialog.vue
#	desk/src/components/Settings/Sla/SlaPolicyList.vue
#	desk/src/components/TicketField.vue
#	desk/src/components/TypingIndicator.vue
#	desk/src/components/command-palette/CP.vue
#	desk/src/components/command-palette/CPGroup.vue
#	desk/src/components/command-palette/CPGroupResult.vue
#	desk/src/components/desk/global/AddNewAgentsDialog.vue
#	desk/src/components/frappe-ui/Link.vue
#	desk/src/components/layouts/MobileSidebar.vue
#	desk/src/components/layouts/SettingsLayoutBase.vue
#	desk/src/components/layouts/Sidebar.vue
#	desk/src/components/notifications/Notifications.vue
#	desk/src/components/ticket/TicketMergeModal.vue
#	desk/src/components/view-controls/Filter.vue
#	desk/src/components/view-controls/SortBy.vue
#	desk/src/pages/MobileNotifications.vue
#	desk/src/pages/knowledge-base/Article.vue
#	desk/src/pages/knowledge-base/KnowledgeBaseCustomer.vue
#	desk/src/pages/ticket/TicketConversation.vue
#	desk/src/pages/ticket/TicketNew.vue
2026-05-31 13:41:37 +05:30
Sydney
fd77fbef52 fix: allow switching from ltr to rtl for rtl based languages 2026-05-31 13:32:06 +05:30
Sydney
2e2b5160f6 fix: sweep desk app for rtl-safe logical tailwind classes 2026-05-31 13:32:06 +05:30
Sydney
f11933d6b2 fix: add translation support to list view filter components 2026-05-31 13:30:42 +05:30
Sydney
37ad6fabe4 fix: address spacing issues caused due to rtl and improve translation coverage 2026-05-31 13:30:42 +05:30
Sydney
1f5e8de22a fix: add translation support and polish ticket view for rtl 2026-05-31 13:30:42 +05:30
Sydney
c7c06afaee fix: add support to rtl for home page sections 2026-05-31 13:27:34 +05:30
Sydney
3a8de2ce18 chore: setup rtl in index.html and add rtl plugin 2026-05-31 13:25:39 +05:30
MochaMind
c8d0d91185 chore: Hungarian translations 2026-05-30 16:31:11 +05:30
saadchaudharry
ed567fa6fe refactor: get_users function to optimize role fetching and user retrieval 2026-05-30 15:40:28 +05:30
Ritvik Sardana
eeddf0e67b chore: code cleanup 2026-05-29 23:03:45 +05:30
Ritvik Sardana
52fd69bafe refactor: primary contact setup in HD Customer 2026-05-29 18:09:21 +05:30
Ritvik Sardana
c45d5108c6 fix: contact row primary 2026-05-29 17:13:28 +05:30
Ritvik Sardana
c2dbac937a refactor: dont override contact doctype 2026-05-29 17:04:31 +05:30
Ritvik Sardana
ec7d066f21 fix: shape size of avatar 2026-05-29 15:36:03 +05:30
Sydney Gomes
a53162be7d Merge pull request #3389 from Hemil-Sangani/fix/helpdesk-portal-export-me-filter
fix: resolve @me filters during export in Helpdesk Portal
2026-05-29 15:34:37 +05:30
Sydney
240b361744 fix: resolve @me in assigned to filter during export 2026-05-29 14:37:08 +05:30
Ritvik Sardana
fd1323a48e refactor: PhoneControl code 2026-05-29 13:49:33 +05:30
Ritvik Sardana
a7696d23fd refactor: hd_ticket perms code 2026-05-29 12:56:11 +05:30
RitvikSardana
2924283358 Merge pull request #3404 from RitvikSardana/fix/erpnext-integration
fix: better button labels
2026-05-29 12:12:37 +05:30
Ritvik Sardana
5651411fd0 fix: better button labels 2026-05-29 12:11:26 +05:30
Ritvik Sardana
949ee00cba fix: hover state 2026-05-28 22:37:25 +05:30
RitvikSardana
f6024dcdde Merge pull request #3401 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-28 21:14:34 +05:30
Ritvik Sardana
993d0b5595 fix: hover state in TicketsTab 2026-05-28 21:13:45 +05:30
Ritvik Sardana
2468a95d17 fix: use PhoneControl in contact field 2026-05-28 20:58:24 +05:30
Ritvik Sardana
ff65eadf63 fix: add PhonControl 2026-05-28 20:46:11 +05:30
Ritvik Sardana
04b131a993 fix: phone number adding bug 2026-05-28 18:58:03 +05:30
Ritvik Sardana
57ecb37f1b fix: show skeleton while analytics is loading in customer/contact page 2026-05-28 18:24:57 +05:30
Ritvik Sardana
118f9e254c fix: add invite_as_user checkbox in contact creation 2026-05-28 17:58:43 +05:30
MochaMind
523aab61f9 chore: Burmese translations 2026-05-28 16:25:36 +05:30
MochaMind
0612bb7685 chore: Thai translations 2026-05-28 16:25:34 +05:30
MochaMind
d9dbfafe62 chore: Slovenian translations 2026-05-28 16:25:24 +05:30
MochaMind
b0260a4efb chore: Hungarian translations 2026-05-28 16:25:18 +05:30
MochaMind
e5bff7196a chore: German translations 2026-05-28 16:25:15 +05:30
MochaMind
0388d09e53 chore: Danish translations 2026-05-28 16:25:13 +05:30
MochaMind
236f867b2c chore: Spanish translations 2026-05-28 16:25:08 +05:30
MochaMind
a81145b72b chore: French translations 2026-05-28 16:25:06 +05:30
Ritvik Sardana
6e9217bad2 fix: max width 2026-05-28 15:54:29 +05:30
Sydney Gomes
67ff79c24d Merge branch 'develop' into feat-agent-status 2026-05-28 14:35:34 +05:30
Sydney
065026b2b6 Revert "feat: add open tickets and avg first response in agents tab"
This reverts commit 85edb100f2.
2026-05-28 14:33:41 +05:30
RitvikSardana
c1c1940f12 Merge pull request #3398 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-28 14:10:02 +05:30
RitvikSardana
15551fd302 Merge pull request #3394 from RitvikSardana/feat/bulk-reply
feat: bulk reply
2026-05-28 10:46:28 +05:30
Sydney
85edb100f2 feat: add open tickets and avg first response in agents tab 2026-05-28 02:53:47 +05:30
Ritvik Sardana
cbf8324eba fix: attachment logic 2026-05-28 01:30:59 +05:30
Ritvik Sardana
bae7d65b73 chore: remove md file 2026-05-28 01:09:29 +05:30
Ritvik Sardana
aa90218ec8 fix: test case 2026-05-28 00:54:51 +05:30
Ritvik Sardana
8b68e28ac4 test: bulk reply 2026-05-28 00:16:55 +05:30
Ritvik Sardana
47e543629f refactor: bulk reply modal 2026-05-27 23:44:58 +05:30
Ritvik Sardana
4994e03ef5 chore: code cleanup 2026-05-27 18:54:43 +05:30
Ritvik Sardana
0a5f73c87e fix: merge conflict issues 2026-05-27 17:56:43 +05:30
Ritvik Sardana
fc44803337 Merge branch 'develop' of https://github.com/frappe/helpdesk into refactor/customer-contact 2026-05-27 17:01:53 +05:30
MochaMind
9704a69797 chore: Korean translations 2026-05-27 16:02:29 +05:30
MochaMind
10760560b2 chore: Esperanto translations 2026-05-27 16:02:26 +05:30
MochaMind
ad34fd7656 chore: Russian translations 2026-05-27 16:02:24 +05:30
MochaMind
b0000976f6 chore: Serbian (Latin) translations 2026-05-27 16:02:21 +05:30
MochaMind
c57f736e71 chore: Norwegian Bokmal translations 2026-05-27 16:02:18 +05:30
MochaMind
a6226b1562 chore: Bosnian translations 2026-05-27 16:02:15 +05:30
MochaMind
c0fa6b48dd chore: Burmese translations 2026-05-27 16:02:12 +05:30
MochaMind
5a9c113087 chore: Croatian translations 2026-05-27 16:02:09 +05:30
MochaMind
e8b65d056f chore: Thai translations 2026-05-27 16:02:06 +05:30
MochaMind
37cd9fd7ae chore: Persian translations 2026-05-27 16:02:02 +05:30
MochaMind
100ac35cc4 chore: Indonesian translations 2026-05-27 16:01:59 +05:30
MochaMind
049e1c554b chore: Portuguese, Brazilian translations 2026-05-27 16:01:57 +05:30
MochaMind
92a3832d5c chore: Vietnamese translations 2026-05-27 16:01:54 +05:30
MochaMind
7373292684 chore: Chinese Simplified translations 2026-05-27 16:01:51 +05:30
MochaMind
8468faf501 chore: Turkish translations 2026-05-27 16:01:48 +05:30
MochaMind
cbb3536df5 chore: Swedish translations 2026-05-27 16:01:45 +05:30
MochaMind
eb7d392b8b chore: Serbian (Cyrillic) translations 2026-05-27 16:01:42 +05:30
MochaMind
139e3d019c chore: Slovenian translations 2026-05-27 16:01:39 +05:30
MochaMind
8d520703d6 chore: Portuguese translations 2026-05-27 16:01:36 +05:30
MochaMind
1a5a94785d chore: Polish translations 2026-05-27 16:01:33 +05:30
MochaMind
859fb683d6 chore: Dutch translations 2026-05-27 16:01:30 +05:30
MochaMind
723afc0c67 chore: Italian translations 2026-05-27 16:01:28 +05:30
MochaMind
2047e83fd4 chore: Hungarian translations 2026-05-27 16:01:25 +05:30
MochaMind
367f2415c5 chore: German translations 2026-05-27 16:01:22 +05:30
MochaMind
ed871686af chore: Danish translations 2026-05-27 16:01:19 +05:30
MochaMind
fd462c8b43 chore: Czech translations 2026-05-27 16:01:16 +05:30
MochaMind
8a76b6745f chore: Arabic translations 2026-05-27 16:01:13 +05:30
MochaMind
d6e89d748d chore: Spanish translations 2026-05-27 16:01:10 +05:30
MochaMind
bf2a0b16f4 chore: French translations 2026-05-27 16:01:07 +05:30
Ritvik Sardana
eb8d647c64 fix: save reply in local storage 2026-05-27 14:03:48 +05:30
Ritvik Sardana
1785703cf4 fix: resend invite when invitation expired 2026-05-27 12:04:41 +05:30
Ritvik Sardana
8d07acde61 fix: layout shift in contact info 2026-05-27 11:23:51 +05:30
Ritvik Sardana
151c0f1bb1 fix: add invite as user functionality in contact 2026-05-27 11:01:28 +05:30
Ritvik Sardana
425441d169 fix: better contact list view 2026-05-27 02:31:21 +05:30
Ritvik Sardana
2f8b81bf0b refactor: dirty logic 2026-05-27 02:13:29 +05:30
Ritvik Sardana
0396ab628d fix: image upload bug + phone primary not being set 2026-05-27 02:03:11 +05:30
Sydney
9d970c5144 feat: calculate and display time from agent status change 2026-05-27 02:00:21 +05:30
Ritvik Sardana
29ee69f83e fix: show total ticket count in contact 2026-05-27 00:05:37 +05:30
Ritvik Sardana
d7c5c1e32e fix: better logging message 2026-05-26 20:54:05 +05:30
RitvikSardana
814b4c0f20 Merge pull request #3361 from RitvikSardana/erpnext-hd-integration
feat: sync ERPNext customer with HD customer
2026-05-26 20:47:54 +05:30
RitvikSardana
c90bac04b4 Merge pull request #3386 from frappe/pot_develop_2026-05-24
chore: update POT file
2026-05-26 20:47:12 +05:30
Ritvik Sardana
2d0552fa91 fix: better defaults 2026-05-26 20:45:50 +05:30
RitvikSardana
8090f327fb Merge pull request #3371 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-26 20:30:49 +05:30
Ritvik Sardana
5efaf89e4d fix: bulk reply icon 2026-05-26 20:26:31 +05:30
Ritvik Sardana
f6947f115d chore: code cleanup 2026-05-26 19:23:17 +05:30
Ritvik Sardana
000ad30f03 refactor: code structure 2026-05-26 18:57:36 +05:30
Ritvik Sardana
9b7b779d08 fix: add attachment feature 2026-05-26 16:24:42 +05:30
Sydney
4d394a0414 feat: add functionality for busy agents 2026-05-26 14:43:34 +05:30
Sydney
779d4a0662 test: add testcases for agent status 2026-05-26 14:28:21 +05:30
Ritvik Sardana
a1cb277151 fix: better copy 2026-05-26 13:45:51 +05:30
Ritvik Sardana
c5c687fa73 chore: code cleanup 2026-05-26 13:18:44 +05:30
Sydney
b9fe65d96f fix: generalize toast warnings for assign to modal 2026-05-26 13:00:34 +05:30
Sydney
83ec1baab7 fix: change status name and display status in profile 2026-05-26 12:58:59 +05:30
Sydney
0c6d1ba23b fix: use dropdown instead of select for agent status 2026-05-26 12:48:45 +05:30
Ritvik Sardana
5229c333b7 fix: dont throw error for agent role 2026-05-26 12:39:36 +05:30
Ritvik Sardana
8de2ad89c0 chore: code cleanup 2026-05-26 12:13:42 +05:30
Ritvik Sardana
48c4721488 fix: handle bulk delete properly 2026-05-26 11:20:37 +05:30
Ritvik Sardana
6ce4499536 fix: enqueue job ID 2026-05-25 22:48:55 +05:30
RitvikSardana
6dc1c63999 Merge pull request #3366 from HarishChandran3304/fix/dark-mode-regressions
fix: dark mode regressions
2026-05-25 21:16:00 +05:30
Ritvik Sardana
0ffca6862a refactor: add class to contain common methods 2026-05-25 21:11:59 +05:30
Ritvik Sardana
93c2d17fe1 chore: code cleanup 2026-05-25 21:11:40 +05:30
Ritvik Sardana
536232e10d fix: dont force delete 2026-05-25 17:36:57 +05:30
HemilSangani
1f4b36ff60 refactor: improve @me filter export handling comments 2026-05-25 15:06:07 +05:30
HemilSangani
d8942c308f fix: resolve @me filters during export in Helpdesk Portal 2026-05-25 15:02:03 +05:30
Sydney
630704fb4f feat: add agent status indicators to frontend 2026-05-25 13:43:22 +05:30
Sydney
e2dd43be18 fix: add agent availabilty menu and color map for options 2026-05-25 13:41:20 +05:30
Harish Chandran
0fb37f2742 fix: make KB new article title input theme-aware 2026-05-25 12:54:11 +05:30
Ritvik Sardana
890d9aa903 fix: better in_sync logic 2026-05-25 12:47:08 +05:30
Ritvik Sardana
cc2f8c3755 fix: make UserPerms mirroring more verbose 2026-05-25 01:26:42 +05:30
Ritvik Sardana
b9f6b200c1 fix: handle updating sync on user perm or share update 2026-05-24 16:52:41 +05:30
frappe-pr-bot
947f6a5524 chore: update POT file 2026-05-24 10:57:36 +00:00
Ritvik Sardana
cf5660595d fix: handle renaming 2026-05-24 13:44:17 +05:30
Ritvik Sardana
919dea3090 fix: import path and file name 2026-05-24 02:21:44 +05:30
MochaMind
cdbb0b29fe chore: Swedish translations 2026-05-23 15:11:19 +05:30
Ritvik Sardana
bf44131e35 fix: add on_trash events for DocShare & User Permissions 2026-05-22 18:19:29 +05:30
Ritvik Sardana
3a8f31e5eb fix: sync DocShare 2026-05-22 18:18:27 +05:30
Ritvik Sardana
a506b5cf7b chore: change import paths 2026-05-22 17:39:59 +05:30
Ritvik Sardana
e582a68b68 chore: code cleanup 2026-05-22 16:49:01 +05:30
Ritvik Sardana
4a38da39e8 fix: sync user perms 2026-05-22 16:15:12 +05:30
Ritvik Sardana
016ac63148 feat: bulk reply in ticket list view 2026-05-21 18:01:02 +05:30
Ritvik Sardana
68812f153b fix: show manager the erpnext setting in UI 2026-05-21 16:27:03 +05:30
Ritvik Sardana
f69ad1f383 fix: better folder structure and erpnext settings 2026-05-21 16:12:28 +05:30
Ritvik Sardana
9387e52e27 fix: better UX for enabling integration from Settings 2026-05-21 15:47:59 +05:30
MochaMind
60d7dcf47a chore: Swedish translations 2026-05-21 15:14:47 +05:30
MochaMind
68f04c3c34 chore: French translations 2026-05-21 15:14:42 +05:30
Ritvik Sardana
f3d660cbb9 fix: create hd_customer custom field in before_tests 2026-05-21 14:07:31 +05:30
Ritvik Sardana
352b1f45db chore: code cleanup 2026-05-21 13:06:46 +05:30
Ritvik Sardana
e042c72bb9 fix: make hd_customer field hidden 2026-05-21 12:39:55 +05:30
Ritvik Sardana
ec1f86d1cc fix: add perms in HD Settings 2026-05-21 12:28:46 +05:30
Ritvik Sardana
7c3db92544 fix(ci): install apps on test-site 2026-05-21 12:28:24 +05:30
Ritvik Sardana
a0b5f39112 test: skip HD Customer sync tests when ERPNext is not installed 2026-05-21 10:50:50 +05:30
Sydney Gomes
fc54a0f42a Merge pull request #3378 from aerodeval/fix-merge-ticket-conditions
fix: add missing conditions in ticket merge modal
2026-05-21 03:00:52 +05:30
Sydney
146a389a7a fix: add missing conditions in ticket merge modal 2026-05-21 02:49:30 +05:30
Sydney Gomes
36a945e603 Merge pull request #3376 from aerodeval/fix-loading-state-regression
fix: loader appearing on top in SLa view
2026-05-21 02:24:16 +05:30
Sydney
ad8ff52580 fix: improper logic used to display loading state 2026-05-21 02:17:20 +05:30
Sydney
b0c846c67c fix: loader appearing on top in SLa view 2026-05-21 02:13:29 +05:30
RitvikSardana
b572cb56cc Merge branch 'develop' into erpnext-hd-integration 2026-05-20 20:52:17 +05:30
Ritvik Sardana
fd240682c8 fix: dont always show sync button in erpnext 2026-05-20 20:42:00 +05:30
Ritvik Sardana
835e0818f4 chore: code cleanup 2026-05-20 19:57:01 +05:30
Ritvik Sardana
60efc65b52 fix: use custom class instead of doc events hook 2026-05-20 19:52:45 +05:30
MochaMind
d1a8b12458 chore: French translations 2026-05-20 14:51:53 +05:30
Sydney
39adf83470 fix: generalize filter for assignment rule in override 2026-05-20 12:15:13 +05:30
Ritvik Sardana
54060409e8 chore: code cleanup 2026-05-19 18:02:19 +05:30
Sydney
266adf8842 fix: override assignment rule for filtering out away agents 2026-05-19 15:07:29 +05:30
Sydney
a9268b8621 feat: handle manual assignment for away agents 2026-05-19 15:05:17 +05:30
Sydney
c7e1861fc8 fix: add agent availability 2026-05-19 15:01:57 +05:30
MochaMind
b60463736f chore: Esperanto translations 2026-05-19 13:57:32 +05:30
MochaMind
ae6039da5b chore: Russian translations 2026-05-19 13:57:30 +05:30
MochaMind
b173c84ff3 chore: Serbian (Latin) translations 2026-05-19 13:57:27 +05:30
MochaMind
5912022f4b chore: Norwegian Bokmal translations 2026-05-19 13:57:25 +05:30
MochaMind
0f366341d3 chore: Bosnian translations 2026-05-19 13:57:23 +05:30
MochaMind
9a80854c95 chore: Burmese translations 2026-05-19 13:57:21 +05:30
MochaMind
a49c7378a9 chore: Croatian translations 2026-05-19 13:57:19 +05:30
MochaMind
c7ac7db049 chore: Thai translations 2026-05-19 13:57:17 +05:30
MochaMind
c49fcd0e5d chore: Persian translations 2026-05-19 13:57:14 +05:30
MochaMind
ff634daa7e chore: Indonesian translations 2026-05-19 13:57:12 +05:30
MochaMind
784d7ab53c chore: Portuguese, Brazilian translations 2026-05-19 13:57:10 +05:30
MochaMind
f67c39ea94 chore: Vietnamese translations 2026-05-19 13:57:08 +05:30
MochaMind
aa5399df04 chore: Chinese Simplified translations 2026-05-19 13:57:05 +05:30
MochaMind
81928e2117 chore: Turkish translations 2026-05-19 13:57:03 +05:30
MochaMind
8a936cb486 chore: Swedish translations 2026-05-19 13:57:01 +05:30
MochaMind
eae44520cc chore: Serbian (Cyrillic) translations 2026-05-19 13:56:59 +05:30
MochaMind
64c964002d chore: Slovenian translations 2026-05-19 13:56:56 +05:30
MochaMind
4cec7f83ae chore: Portuguese translations 2026-05-19 13:56:54 +05:30
MochaMind
25940eea86 chore: Polish translations 2026-05-19 13:56:51 +05:30
MochaMind
fd5773a10c chore: Dutch translations 2026-05-19 13:56:49 +05:30
MochaMind
368dc1a3a6 chore: Italian translations 2026-05-19 13:56:47 +05:30
MochaMind
7c5a093f47 chore: Hungarian translations 2026-05-19 13:56:45 +05:30
MochaMind
6dd10ca425 chore: German translations 2026-05-19 13:56:43 +05:30
MochaMind
3c45873ad3 chore: Danish translations 2026-05-19 13:56:40 +05:30
MochaMind
dbdec8cd3c chore: Czech translations 2026-05-19 13:56:38 +05:30
MochaMind
29f63a1a8d chore: Arabic translations 2026-05-19 13:56:36 +05:30
MochaMind
e4ff6aaeb1 chore: Spanish translations 2026-05-19 13:56:34 +05:30
MochaMind
95a231f878 chore: French translations 2026-05-19 13:56:32 +05:30
Sydney Gomes
5776cbd03d Merge pull request #3370 from aerodeval/fix-empty-state
fix: add pointer-events-none to empty state overlay
2026-05-19 12:08:12 +05:30
Sydney
bfa81e08d2 fix: remove unused z-index 2026-05-19 11:57:23 +05:30
Sydney
4b27fc1ce7 fix: disable pointer-events on empty state overlay to unblock header actions 2026-05-19 11:54:00 +05:30
Ritvik Sardana
6c6a4d347e fix: connect API with feedback chart data 2026-05-19 11:03:39 +05:30
Ritvik Sardana
21062888d6 fix: add loading state 2026-05-19 00:52:15 +05:30
Ritvik Sardana
9256730649 refactor: add feedback composable for contact 2026-05-19 00:31:31 +05:30
Ritvik Sardana
2bc4f60643 feat: feedback tab in Contact Page 2026-05-18 19:16:23 +05:30
RitvikSardana
760e6de95f Merge pull request #3367 from RitvikSardana/sla-hold
fix: correct label for SLA when ticket is on hold
2026-05-18 16:05:29 +05:30
Ritvik Sardana
a9dfd34d0c fix: corrent label for SLA when ticket is on hold 2026-05-18 16:03:45 +05:30
Harish Chandran
962546b56a fix: use theme-stable mobile sidebar backdrop 2026-05-18 15:13:47 +05:30
Harish Chandran
b2124c076c fix: make <hr> dividers visible in Settings list views 2026-05-18 15:11:22 +05:30
RitvikSardana
12dd55f9db Merge pull request #3363 from frappe/pot_develop_2026-05-17
chore: update POT file
2026-05-18 14:59:33 +05:30
Sydney Gomes
c0d5b2cfb8 Merge pull request #3353 from aerodeval/fix-ui-code-refactor
refactor(frontend): reduce code duplication across components and some ui fixes
2026-05-17 17:38:26 +05:30
Sydney
ec40d5a3e3 fix: flicker when tabs are changed 2026-05-17 17:33:09 +05:30
frappe-pr-bot
4982ef6ce1 chore: update POT file 2026-05-17 10:54:00 +00:00
Sydney
197e8c5334 fix: ui issues with centering of loading states and loading indicator 2026-05-17 14:41:45 +05:30
Ritvik Sardana
9bac8ae0c8 fix: dont show sync button if erpnext not installed 2026-05-17 01:49:22 +05:30
Ritvik Sardana
cee84c8506 fix: add perm guard in whitelisted methods 2026-05-17 01:25:31 +05:30
Ritvik Sardana
94862dc3fa fix: install CI 2026-05-17 01:15:55 +05:30
Ritvik Sardana
deed869a88 fix: add erpnext to install.sh 2026-05-17 01:10:46 +05:30
Ritvik Sardana
71b664adeb fix: add patch for adding erpnext customers to HD 2026-05-17 01:08:01 +05:30
Ritvik Sardana
8a31a0a1fc test: write test cases for hd_customer erpnext_customer sync 2026-05-17 00:34:21 +05:30
Ritvik Sardana
addaf967e7 fix: only show system manager the ERPNext Integration setting 2026-05-16 19:53:22 +05:30
Ritvik Sardana
7f70e4907f fix: add ERPNext Integration setting in Settings Modal 2026-05-16 19:37:24 +05:30
Sydney Gomes
d799ab2382 Merge branch 'develop' into fix-ui-code-refactor 2026-05-16 17:56:10 +05:30
Sydney
684bf0d80c fix: make dropdown lost mobile friendly 2026-05-16 17:42:45 +05:30
Sydney
171f62e07b fix: view breadcrumbs dropdown fix 2026-05-16 17:14:07 +05:30
Ritvik Sardana
ee540d64ec feat: sync erpnext customer with hd customer 2026-05-16 03:33:23 +05:30
Ritvik Sardana
b1250c737f feat: erpnext hd customer master sync 2026-05-16 00:11:38 +05:30
RitvikSardana
f7d0541bf9 Merge pull request #3359 from RitvikSardana/sidebar-fields
fix: border in ticket fields
2026-05-15 15:39:12 +05:30
Ritvik Sardana
2c3548b54c chore: code cleanup 2026-05-15 14:43:11 +05:30
Ritvik Sardana
eba1d2624e fix: border in ticket fields 2026-05-15 14:40:16 +05:30
RitvikSardana
b59d714f32 Merge pull request #3276 from dankgarlic1/feat/ticket-naming-expression
feat: change ticket ID format to string-based naming series
2026-05-15 13:28:59 +05:30
Ritvik Sardana
e8daf87e08 chore: code cleanup 2026-05-15 13:05:21 +05:30
Ritvik Sardana
cb4d90a1cd fix: default ticket id width in ListView 2026-05-15 13:04:39 +05:30
Sydney
431e95d450 fix: linter issues 2026-05-15 13:02:21 +05:30
Sydney Gomes
c632c03a4d Merge branch 'develop' into fix-ui-code-refactor 2026-05-15 12:52:35 +05:30
Sydney
0af8b49ab2 fix: dark mode ui issues 2026-05-15 12:50:13 +05:30
Sydney
0e0d460e2e fix: breadcrumbs fix for mobile and UI issues 2026-05-15 12:21:14 +05:30
RitvikSardana
074f3aeaa8 Merge pull request #3246 from HarishChandran3304/feat/dark-mode
feat: dark mode
2026-05-15 10:43:06 +05:30
Sydney
bf879318f9 fix: breadcrumbs and empty states centered 2026-05-14 15:52:44 +05:30
Harshit Raizada
ef72e3b071 chore: fixed patch file name in patch.txt 2026-05-14 12:45:38 +05:30
Harshit Raizada
d0f5bec3e0 chore: added the patch name in patch file 2026-05-14 12:44:46 +05:30
Sydney
2ed16f1232 fix: center empty states all over 2026-05-14 12:02:17 +05:30
RitvikSardana
225aa616de Merge pull request #3354 from RitvikSardana/fix/overdue-resolution
fix: overdue resolution time in SLA
2026-05-14 11:59:48 +05:30
Ritvik Sardana
264c1f3c78 chore: align SLA status 2026-05-14 11:53:04 +05:30
Ritvik Sardana
4c44d4aa59 fix: overdue resolution time in SLA 2026-05-14 11:50:42 +05:30
Harshit Raizada
e54c9d2c31 chore: sync patches.txt with develop 2026-05-14 11:01:39 +05:30
Harshit Raizada
728cb53ff4 chore: remove redundant parentheses 2026-05-14 10:58:33 +05:30
Harshit Raizada
c2ba5dfddc chore: remove legacy number types from TS interfaces for tickets 2026-05-14 10:58:33 +05:30
Harshit Raizada
079327f3d1 chore: remove legacy str | int type hints for ticket docs 2026-05-14 10:58:32 +05:30
Harshit Raizada
f1d823a52c chore: fix black formatting 2026-05-14 10:58:32 +05:30
Harshit Raizada
09b260e263 feat: change ticket ID format to string-based naming series 2026-05-14 10:58:25 +05:30
Sydney
704d887f35 fix: improve edit name in profile component 2026-05-14 02:33:53 +05:30
Sydney
1982d5d83a fix: use reusable components for EmptyState and Badge transtions 2026-05-14 02:32:05 +05:30
Harish Chandran
e9c5d2fb6b fix: use theme-aware tokens for custom ticket fields 2026-05-13 15:13:32 +05:30
Harish Chandran
ae2d2da2c1 chore: reorder user dropdown to move Toggle theme above Settings 2026-05-13 14:41:25 +05:30
Harish Chandran
0b436c70c3 fix: use theme-aware color in SkeletonLoader Dashboard page empty-state gradient 2026-05-13 14:37:35 +05:30
Sydney Gomes
f9668a5407 Merge pull request #3266 from aerodeval/fix-allow-sending-with-bcc/cc
fix: allow sending with bcc/cc
2026-05-13 13:41:14 +05:30
Sydney
777619925a fix: improve code quality 2026-05-13 13:24:54 +05:30
RitvikSardana
a72f5b37f4 Merge pull request #3342 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-13 11:45:05 +05:30
Sydney Gomes
c9c4a749a8 Merge branch 'develop' into fix-allow-sending-with-bcc/cc 2026-05-13 11:44:43 +05:30
Sydney Gomes
c996542a0c Merge pull request #3348 from aerodeval/test-from-email-coverage
test: add test cases for from-email switching
2026-05-13 11:42:05 +05:30
Sydney
54c32cc5be test: add test cases for from-email switching 2026-05-13 01:51:52 +05:30
Ritvik Sardana
0e58ef12b3 feat: enhance contact invitation flow and UI updates 2026-05-12 23:57:16 +05:30
RitvikSardana
8af10567af Merge pull request #3297 from RitvikSardana/fix/sla-header
fix: sla header in ticket view
2026-05-12 23:50:04 +05:30
Ritvik Sardana
4af5fac05e fix: dont update resolution when status_category remains the same of 'Resolved' 2026-05-12 22:42:59 +05:30
Ritvik Sardana
d802e76038 fix: sla formatting in UI 2026-05-12 22:42:59 +05:30
RitvikSardana
e0ddb619fd Merge branch 'develop' into fix/sla-header 2026-05-12 21:00:55 +05:30
Ritvik Sardana
881a0c484f chore: code cleanup 2026-05-12 20:51:56 +05:30
Harish Chandran
b3028961c3 fix: avoid no-unused-expressions lint error in chart computed blocks 2026-05-12 18:18:22 +05:30
Ritvik Sardana
a1f305edb4 fix: add patch for backfiling the fields for tickets raised in last 180 days 2026-05-12 17:27:14 +05:30
Ritvik Sardana
e7c57a1c04 fix: add test cases for response and resolution failed_by 2026-05-12 17:23:23 +05:30
Sydney
33195dcf02 fix: improve test cases 2026-05-12 17:01:12 +05:30
Harish Chandran
b100e025b3 feat: add dark-mode shadow overrides 2026-05-12 13:59:47 +05:30
Harish Chandran
8d6a3a92f9 fix: add semantic border color to Autocomplete footer divider 2026-05-12 13:11:40 +05:30
Harish Chandran
b76a759a68 fix: use surface-modal for Autocomplete popover background 2026-05-12 13:07:31 +05:30
RitvikSardana
c053047814 Merge pull request #3344 from RitvikSardana/fix/article-search
fix: broken article search
2026-05-12 12:48:20 +05:30
Ritvik Sardana
f6842abe93 chore: remove unwanted code 2026-05-12 12:26:23 +05:30
MochaMind
b5385a6b3c chore: Bosnian translations 2026-05-12 11:21:38 +05:30
MochaMind
06f04f757a chore: Croatian translations 2026-05-12 11:21:37 +05:30
MochaMind
bc7338bee6 chore: Swedish translations 2026-05-12 11:21:30 +05:30
Harish Chandran
64139d6fc3 fix: use theme-aware color in EmptyState overlay gradient 2026-05-12 04:20:00 +05:30
Harish Chandran
8217cc7455 Merge branch 'develop' into feat/dark-mode 2026-05-12 04:13:50 +05:30
Sydney Gomes
daadbb6915 Merge pull request #3329 from aerodeval/fix/ui-bugs
fix: UI bugs
2026-05-12 01:29:08 +05:30
Sydney
c0cb774f73 fix: ticket view ui fixes 2026-05-12 01:24:04 +05:30
Sydney
6e3a910555 fix: improve code quality in hd_ticket 2026-05-12 01:03:15 +05:30
Sydney
11382aa429 fix: ui bugs in ticket view page and settings 2026-05-12 01:02:47 +05:30
Sydney
8956558d27 fix: select box sizing in invite agents 2026-05-12 01:02:00 +05:30
RitvikSardana
beab18dc86 Merge pull request #3338 from niraj2477/redis-search
refactor: Remove HD Ticket indexing from redis based search
2026-05-12 00:57:49 +05:30
RitvikSardana
5fed27537d Merge pull request #3288 from RitvikSardana/weighted-users
fix: add weighted_users support in hd team
2026-05-11 23:26:18 +05:30
Ritvik Sardana
a319b47b8c chore: code cleanup 2026-05-11 23:19:40 +05:30
Ritvik Sardana
5a47bbd31d chore: update agents.md 2026-05-11 23:07:47 +05:30
Ritvik Sardana
ab34c77b42 chore: update ts_config 2026-05-11 23:06:47 +05:30
Ritvik Sardana
5f693fee2f fix: use same rename dialog in team edit and list view 2026-05-11 20:38:14 +05:30
Niraj Gautam
5f6e1d7b5c chore: Fix lint issue 2026-05-11 20:07:19 +05:30
Ritvik Sardana
fb9df9865e refactor: team edit view 2026-05-11 19:50:39 +05:30
Niraj Gautam
777466a744 fix: Remove HD Ticket indexing from redis based search 2026-05-11 19:48:28 +05:30
Ritvik Sardana
b124594c9c chore: code cleanup 2026-05-11 19:40:32 +05:30
Ritvik Sardana
215475d080 fix: add enable check while creating new team in UI 2026-05-11 19:37:18 +05:30
Ritvik Sardana
d39d56f67d refactor: HD Team doctype codebase 2026-05-11 19:28:29 +05:30
Ritvik Sardana
4d34520232 fix: test utils for creating team 2026-05-11 18:34:46 +05:30
Ritvik Sardana
2f1bccd869 chore: code cleanup 2026-05-11 18:26:05 +05:30
Ritvik Sardana
5947ae3f04 fix: handle app installation 2026-05-11 18:22:31 +05:30
Ritvik Sardana
3b206b9b3e chore: code cleanup 2026-05-11 18:08:02 +05:30
Sydney
8036ccc8f4 fix: multiple toasts in invite agent flow and ui fix 2026-05-11 18:05:58 +05:30
Ritvik Sardana
71956484ff fix: update link_filters in HDTicket 2026-05-11 17:56:48 +05:30
Ritvik Sardana
8b41331457 fix: add team disable in UI 2026-05-11 17:49:49 +05:30
Sydney
9702df6d32 fix: use combobox instead of Autocomplete and fix quick filters 2026-05-11 17:01:07 +05:30
Ritvik Sardana
a9dfa9a1b4 fix: sync disable state of HD Team with Assignment Rule 2026-05-11 14:39:52 +05:30
RitvikSardana
5c1a71274d Merge pull request #3331 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-11 14:19:52 +05:30
Sydney
df6643fc73 fix: ensure email signature is applied on editor open 2026-05-11 11:38:28 +05:30
MochaMind
f702fa2d14 chore: Esperanto translations 2026-05-11 11:23:22 +05:30
MochaMind
be5a4dbdda chore: Russian translations 2026-05-11 11:23:21 +05:30
MochaMind
633c712678 chore: Serbian (Latin) translations 2026-05-11 11:23:19 +05:30
MochaMind
9f4cefd92a chore: Norwegian Bokmal translations 2026-05-11 11:23:17 +05:30
MochaMind
b6c22d78a7 chore: Bosnian translations 2026-05-11 11:23:16 +05:30
MochaMind
d3c0b6d288 chore: Burmese translations 2026-05-11 11:23:14 +05:30
MochaMind
29f634e48f chore: Croatian translations 2026-05-11 11:23:13 +05:30
MochaMind
5582e9f326 chore: Thai translations 2026-05-11 11:23:11 +05:30
MochaMind
1d2dfcf20f chore: Persian translations 2026-05-11 11:23:09 +05:30
MochaMind
f84f9db434 chore: Indonesian translations 2026-05-11 11:23:08 +05:30
MochaMind
a1cecb3f2b chore: Portuguese, Brazilian translations 2026-05-11 11:23:06 +05:30
MochaMind
5ec41b1500 chore: Vietnamese translations 2026-05-11 11:23:04 +05:30
MochaMind
8faabcf831 chore: Chinese Simplified translations 2026-05-11 11:23:03 +05:30
MochaMind
09a72dbcc5 chore: Turkish translations 2026-05-11 11:23:01 +05:30
MochaMind
0b6f2c828f chore: Swedish translations 2026-05-11 11:23:00 +05:30
MochaMind
d923677bd6 chore: Serbian (Cyrillic) translations 2026-05-11 11:22:58 +05:30
MochaMind
0edeaed550 chore: Slovenian translations 2026-05-11 11:22:57 +05:30
MochaMind
29f17bd05b chore: Portuguese translations 2026-05-11 11:22:55 +05:30
MochaMind
68e16f8825 chore: Polish translations 2026-05-11 11:22:54 +05:30
MochaMind
5e9ca4413f chore: Dutch translations 2026-05-11 11:22:52 +05:30
MochaMind
c804633e37 chore: Italian translations 2026-05-11 11:22:50 +05:30
MochaMind
e7fea47bef chore: Hungarian translations 2026-05-11 11:22:49 +05:30
MochaMind
0ba8df95ca chore: German translations 2026-05-11 11:22:47 +05:30
MochaMind
73839ab2e6 chore: Danish translations 2026-05-11 11:22:46 +05:30
MochaMind
ffbb3bcf01 chore: Czech translations 2026-05-11 11:22:44 +05:30
MochaMind
26eaa73192 chore: Arabic translations 2026-05-11 11:22:42 +05:30
MochaMind
54cd0b6000 chore: Spanish translations 2026-05-11 11:22:41 +05:30
MochaMind
8dda232d76 chore: French translations 2026-05-11 11:22:39 +05:30
RitvikSardana
834e9fc184 Merge pull request #3328 from frappe/pot_develop_2026-05-10
chore: update POT file
2026-05-11 11:12:16 +05:30
Sydney
be8a417c17 fix: ui issues in frontend 2026-05-10 19:57:59 +05:30
Sydney
7a175e4fab fix: general settings update toggle without save 2026-05-10 18:36:44 +05:30
Sydney
75f7b69613 fix: general ui fixes 2026-05-10 18:22:05 +05:30
Sydney
bf20db015c fix: frappe-ui dropdown changes and combobox width fixes 2026-05-10 18:21:05 +05:30
Sydney
c142a79d65 fix: breadcrumbs listview dropdown appearing on the end 2026-05-10 18:19:34 +05:30
frappe-pr-bot
b909a6f6fc chore: update POT file 2026-05-10 10:49:34 +00:00
Sydney Gomes
bb66ea69f7 Merge pull request #3327 from aerodeval/fix/onboarding-setup
fix: make onboarding steps to depend on ticket creation
2026-05-08 15:03:21 +05:30
Sydney
ae73d2d342 fix: onboarding steps to depend on ticket creation 2026-05-08 14:51:09 +05:30
Sydney Gomes
55e89be0c6 Merge pull request #3324 from aerodeval/dashboard-filters
fix: dashboard filters fix
2026-05-08 03:58:43 +05:30
Sydney
be7eb8f370 fix: dashboard filters fix 2026-05-08 03:46:46 +05:30
RitvikSardana
c2b6dce941 Merge pull request #3292 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-08 02:38:15 +05:30
RitvikSardana
e5a94e969d Merge pull request #3316 from RitvikSardana/fix/node-ci
chore(ci): bump node version in relase ci
2026-05-08 02:36:34 +05:30
Ritvik Sardana
850c054797 chore(ci): bump node version in relase ci 2026-05-08 02:35:33 +05:30
Ritvik Sardana
d583d140c7 fix: add disabled field in hd_team 2026-05-07 23:54:08 +05:30
Ritvik Sardana
fece6ce14f fix: introduce failed_by field for response and resolution field 2026-05-07 23:53:26 +05:30
Sydney Gomes
f73ee4158b Merge pull request #3295 from gajjug004/fix/dashboard-period-filter
fix: parse period filter
2026-05-07 16:17:06 +05:30
gajjug004
a73870f22e fix: parse period filter 2026-05-07 15:17:45 +05:30
Ritvik Sardana
49d9da6891 fix: show proper SLA timing in Ticket View 2026-05-07 13:56:15 +05:30
MochaMind
168968f1b9 chore: Persian translations 2026-05-07 10:49:21 +05:30
Ritvik Sardana
c5e062a25f fix: remove two way sync 2026-05-07 02:19:18 +05:30
Sydney Gomes
8a93324e83 Merge pull request #3141 from pratikb64/fix/agent-landing-page-bugs
fix: Fix standard view filter, UI bugs add My Replies card
2026-05-07 01:19:09 +05:30
Sydney
af99843189 fix: replicate filters from dashboard in agent_home 2026-05-07 00:57:36 +05:30
Sydney
ffbf594d1b fix: handle 0 percent change 2026-05-06 23:35:39 +05:30
Sydney
ba9986905b fix: add missing filters & add filter patches for old Views 2026-05-06 19:32:57 +05:30
RitvikSardana
04745e17c8 Merge pull request #3289 from RitvikSardana/telephony-dep2
chore: telephony version
2026-05-06 18:44:27 +05:30
Ritvik Sardana
356170dea7 chore: telephony version 2026-05-06 18:38:03 +05:30
Sydney Gomes
54f31c1f0c Merge branch 'develop' into fix/agent-landing-page-bugs 2026-05-06 16:13:36 +05:30
Sydney
76ccb72d95 fix: support for last 7 days and improve filters 2026-05-06 16:10:14 +05:30
Sydney
41fbd69956 fix: use emptyState component in homepage 2026-05-06 15:58:53 +05:30
Sydney
df9e03bf40 fix: dashboard page enhancements 2026-05-06 15:56:28 +05:30
Sydney
98e1f8f3ed fix: reuse empty states form Emptystate 2026-05-06 15:55:21 +05:30
RitvikSardana
22ce427855 Merge pull request #3285 from frappe/revert-3282-telephony-dep
Revert "chore: add telephony dependency"
2026-05-06 14:17:26 +05:30
RitvikSardana
f56b112cec Revert "chore: add telephony dependency" 2026-05-06 14:16:32 +05:30
RitvikSardana
1693bdf559 Merge pull request #3282 from RitvikSardana/telephony-dep
chore: add telephony dependency
2026-05-06 11:39:33 +05:30
Ritvik Sardana
05415f77ef chore: update version 2026-05-06 11:38:16 +05:30
Ritvik Sardana
e0d1031f17 chore: add telephony dependency 2026-05-06 11:24:55 +05:30
RitvikSardana
18b0aa4a13 Merge pull request #3277 from RitvikSardana/sla-status-logic
fix: SLA Status Logic
2026-05-05 20:11:49 +05:30
RitvikSardana
d2244b4db4 Merge pull request #3275 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-05 16:35:43 +05:30
Ritvik Sardana
2c9329368c fix: add job for auto updating SLA status 2026-05-05 16:13:36 +05:30
Sydney
2ef9e4abd1 fix: broken dropdowns in home page 2026-05-05 14:33:10 +05:30
Harish Chandran
96c60310f1 Merge remote-tracking branch 'upstream/develop' into feat/dark-mode 2026-05-05 14:01:59 +05:30
Harish Chandran
20602eca52 fix: convert hardcoded blues to semantic versions 2026-05-05 13:55:34 +05:30
Harish Chandran
f78dd81ccc fix: remove redundant classes 2026-05-05 13:54:07 +05:30
Sydney
4eb157bf7e fix: remove hardcoded dates from filters 2026-05-05 12:43:02 +05:30
Ritvik Sardana
5bf232771d fix: more debounce in contact search 2026-05-05 12:20:40 +05:30
Ritvik Sardana
36ac5a1332 fix: Contact avatar in ticket view 2026-05-05 00:37:00 +05:30
Sydney Gomes
57e9742f59 Merge pull request #3278 from aerodeval/fix-profile-settings
fix: prevent request from being sent if name is unchanged
2026-05-05 00:25:05 +05:30
Sydney
e9d973d496 fix: dirty state of save button when name is changed 2026-05-05 00:14:05 +05:30
Ritvik Sardana
703d7f0f2b fix: allow event bubbling in EmailContent 2026-05-05 00:04:45 +05:30
Ritvik Sardana
a433538aa3 fix: agreement_status logic 2026-05-05 00:04:20 +05:30
Sydney
46450cf922 fix: prevent request from being sent if name is unchanged 2026-05-04 23:49:12 +05:30
Sydney
298b465d9a fix: remove throwing exception when to, cc or bcc is empty and failing testcase 2026-05-04 14:56:54 +05:30
Sydney
b1e2bf0e6c fix: add test cases and avoid sending mail without any field in backend 2026-05-04 12:13:21 +05:30
MochaMind
f756f0fbd2 chore: Esperanto translations 2026-05-04 10:45:12 +05:30
MochaMind
e1093e39e2 chore: Russian translations 2026-05-04 10:45:10 +05:30
MochaMind
e55533a2f4 chore: Serbian (Latin) translations 2026-05-04 10:45:09 +05:30
MochaMind
a015642c9e chore: Norwegian Bokmal translations 2026-05-04 10:45:07 +05:30
MochaMind
7b872266f7 chore: Bosnian translations 2026-05-04 10:45:05 +05:30
MochaMind
34d400e8d7 chore: Burmese translations 2026-05-04 10:45:03 +05:30
MochaMind
912097c157 chore: Croatian translations 2026-05-04 10:45:01 +05:30
MochaMind
13e63be3ce chore: Thai translations 2026-05-04 10:44:59 +05:30
MochaMind
e6aecafb8b chore: Persian translations 2026-05-04 10:44:58 +05:30
MochaMind
8d506f283b chore: Indonesian translations 2026-05-04 10:44:55 +05:30
MochaMind
9bb524882d chore: Portuguese, Brazilian translations 2026-05-04 10:44:54 +05:30
MochaMind
2c86d2ebb4 chore: Vietnamese translations 2026-05-04 10:44:52 +05:30
MochaMind
283094dcfb chore: Chinese Simplified translations 2026-05-04 10:44:50 +05:30
MochaMind
ec5aa09e54 chore: Turkish translations 2026-05-04 10:44:49 +05:30
MochaMind
d57e18111d chore: Swedish translations 2026-05-04 10:44:47 +05:30
MochaMind
8c837b36e4 chore: Serbian (Cyrillic) translations 2026-05-04 10:44:45 +05:30
MochaMind
67dd674b69 chore: Slovenian translations 2026-05-04 10:44:44 +05:30
MochaMind
6b3f0f8b39 chore: Portuguese translations 2026-05-04 10:44:42 +05:30
MochaMind
727b3e598d chore: Polish translations 2026-05-04 10:44:40 +05:30
MochaMind
46cf803665 chore: Dutch translations 2026-05-04 10:44:39 +05:30
MochaMind
e7b6858113 chore: Italian translations 2026-05-04 10:44:37 +05:30
MochaMind
9f5041bd68 chore: Hungarian translations 2026-05-04 10:44:35 +05:30
MochaMind
ed3b2eab9c chore: German translations 2026-05-04 10:44:34 +05:30
MochaMind
dd9d512b9b chore: Danish translations 2026-05-04 10:44:32 +05:30
MochaMind
16f645dc9d chore: Czech translations 2026-05-04 10:44:30 +05:30
MochaMind
bbee7e0e0a chore: Arabic translations 2026-05-04 10:44:29 +05:30
MochaMind
2b648194e0 chore: Spanish translations 2026-05-04 10:44:27 +05:30
MochaMind
01f1b2bc6a chore: French translations 2026-05-04 10:44:25 +05:30
RitvikSardana
4a9c428f3c Merge pull request #3273 from frappe/pot_develop_2026-05-03
chore: update POT file
2026-05-04 10:37:49 +05:30
frappe-pr-bot
079f50508a chore: update POT file 2026-05-03 10:46:20 +00:00
RitvikSardana
0748c3fb4f Merge pull request #3272 from RitvikSardana/default-type
fix: dont always set default type
2026-05-02 14:10:59 +05:30
Ritvik Sardana
1aef41eba9 fix: dont change raised by if exists 2026-05-02 14:07:42 +05:30
Ritvik Sardana
c673db3ecd fix: dont always set default type 2026-05-02 14:05:07 +05:30
RitvikSardana
f0e5d194ee Merge pull request #3271 from RitvikSardana/priority-validation
fix: priority validation
2026-05-02 12:46:09 +05:30
Ritvik Sardana
43c7dfca05 fix: show only enabled priorities 2026-05-02 12:19:11 +05:30
Ritvik Sardana
1d9776aa5a fix: layout adjustment in ticket sidepanel 2026-05-02 12:08:07 +05:30
Ritvik Sardana
4acec85d40 fix: priority validation 2026-05-02 11:52:32 +05:30
RitvikSardana
c7f377367d Merge pull request #3267 from RitvikSardana/disable-priority
fix: add disabled field in ticket priority
2026-05-01 13:20:36 +05:30
RitvikSardana
4ad8769a77 Merge pull request #3268 from frappe/l10n_develop
chore: sync translations from crowdin
2026-05-01 12:30:25 +05:30
Ritvik Sardana
b7750f8172 chore: code cleanup 2026-05-01 12:28:43 +05:30
Ritvik Sardana
c29dba481b test: team and assignment rule creation 2026-05-01 12:25:00 +05:30
MochaMind
c66181a50d chore: Esperanto translations 2026-04-30 23:15:27 +05:30
MochaMind
c34a5ed7fd chore: Russian translations 2026-04-30 23:15:25 +05:30
MochaMind
2282bc310f chore: Serbian (Latin) translations 2026-04-30 23:15:24 +05:30
MochaMind
d5ccac5333 chore: Norwegian Bokmal translations 2026-04-30 23:15:22 +05:30
MochaMind
4cb8e00353 chore: Bosnian translations 2026-04-30 23:15:20 +05:30
MochaMind
c82e2030f2 chore: Burmese translations 2026-04-30 23:15:18 +05:30
MochaMind
6bd5307664 chore: Croatian translations 2026-04-30 23:15:16 +05:30
MochaMind
52668e07b7 chore: Thai translations 2026-04-30 23:15:14 +05:30
MochaMind
cdfc59df2e chore: Persian translations 2026-04-30 23:15:12 +05:30
MochaMind
805c400798 chore: Indonesian translations 2026-04-30 23:15:10 +05:30
MochaMind
50d745c29f chore: Portuguese, Brazilian translations 2026-04-30 23:15:09 +05:30
MochaMind
85df9cbba9 chore: Vietnamese translations 2026-04-30 23:15:07 +05:30
MochaMind
29251e84fb chore: Chinese Simplified translations 2026-04-30 23:15:04 +05:30
MochaMind
801394b84b chore: Turkish translations 2026-04-30 23:15:02 +05:30
MochaMind
8ece98ff2e chore: Swedish translations 2026-04-30 23:15:00 +05:30
MochaMind
66c925150e chore: Serbian (Cyrillic) translations 2026-04-30 23:14:59 +05:30
MochaMind
20f6071b95 chore: Slovenian translations 2026-04-30 23:14:57 +05:30
MochaMind
c543cb1c88 chore: Portuguese translations 2026-04-30 23:14:55 +05:30
MochaMind
aa91da426e chore: Polish translations 2026-04-30 23:14:53 +05:30
MochaMind
d3819bc77e chore: Dutch translations 2026-04-30 23:14:51 +05:30
MochaMind
9869ffa2b5 chore: Italian translations 2026-04-30 23:14:49 +05:30
MochaMind
3fd6a01d58 chore: Hungarian translations 2026-04-30 23:14:48 +05:30
MochaMind
2f5ed0760e chore: German translations 2026-04-30 23:14:46 +05:30
MochaMind
e87dc69716 chore: Danish translations 2026-04-30 23:14:44 +05:30
MochaMind
557dc5b914 chore: Czech translations 2026-04-30 23:14:42 +05:30
MochaMind
676a44820e chore: Arabic translations 2026-04-30 23:14:40 +05:30
MochaMind
a896503b68 chore: Spanish translations 2026-04-30 23:14:39 +05:30
MochaMind
cd3f23df2a chore: French translations 2026-04-30 23:14:37 +05:30
Ritvik Sardana
c3e496ff4a test: setting ticket_type in hd_ticket 2026-04-30 20:50:08 +05:30
Ritvik Sardana
d2ee2ea9f2 fix: add disable field in ticket priority 2026-04-30 20:02:47 +05:30
Sydney
e9203d21fc fix: allow sending mails only with bcc & cc in frontend 2026-04-30 17:25:41 +05:30
Sydney
13357c7b77 fix: remove ticket raised_by fallback for to mails 2026-04-30 17:25:08 +05:30
RitvikSardana
917b3b3ff0 Merge pull request #2821 from KerollesFathy/ci/setup-semantic-release
ci: setup semantic-release gh workflow
2026-04-30 11:04:16 +05:30
RitvikSardana
5b11590a28 Merge pull request #3249 from frappe/pot_develop_2026-04-26
chore: update POT file
2026-04-30 10:25:29 +05:30
RitvikSardana
8bc7021a6f Merge branch 'develop' into pot_develop_2026-04-26 2026-04-30 10:23:49 +05:30
RitvikSardana
c7752c1100 Merge pull request #3219 from frappe/pot_develop_2026-04-19
chore: update POT file
2026-04-30 10:16:33 +05:30
RitvikSardana
95ad921f49 Merge branch 'develop' into pot_develop_2026-04-19 2026-04-30 10:15:49 +05:30
RitvikSardana
ad73fa05c7 Merge pull request #3212 from frappe/pot_develop_2026-04-12
chore: update POT file
2026-04-30 10:11:13 +05:30
RitvikSardana
8f88be70cb Merge pull request #3202 from frappe/l10n_develop
chore: sync translations from crowdin
2026-04-30 09:51:34 +05:30
Sydney Gomes
32266df424 Merge pull request #3151 from aerodeval/fix/agent-assign-bug
fix: add mandatory agent check for agent actions
2026-04-29 23:09:34 +05:30
Sydney Gomes
9a5f15d907 Merge pull request #3260 from aerodeval/fix-home-dropdown
fix: broken dropdowns in home page
2026-04-29 22:32:51 +05:30
Sydney
3ca26f4526 fix: broken dropdowns in home page 2026-04-29 22:26:58 +05:30
Sydney Gomes
25140f102f Merge pull request #3256 from aerodeval/fix-cached-email-storage
fix: avoid saving signature in cache of email content
2026-04-28 20:40:45 +05:30
Sydney
fce59cfbae fix: avoid saving signature in cache of email content 2026-04-28 20:35:51 +05:30
Sydney Gomes
be2df0416a Merge pull request #3253 from aerodeval/fix-ticket-views-dropdown
fix: update breadcrumbs dropdown code to use new dropdown slots from fui
2026-04-28 16:32:20 +05:30
Sydney
3e06d66288 fix: border-radius for hover button 2026-04-28 16:30:59 +05:30
Sydney
cd53d89ef2 fix: add more menu dropdown in list item 2026-04-28 16:15:45 +05:30
Sydney
88c47f4157 fix: update breadcrumbs dropdown code to use new dropdown slots from frappe ui 2026-04-28 14:48:59 +05:30
Sydney Gomes
f4f8b43255 Merge pull request #3252 from aerodeval/customer-portal-images
fix: keep image size intact in customer portal
2026-04-28 02:51:32 +05:30
Sydney
20a2ca833e fix: keep images intact in customer portal 2026-04-28 02:48:27 +05:30
Sydney Gomes
6f648f07db Merge pull request #3251 from aerodeval/reply-via-contact
fix: add message field allow reply via contact to show ticket message
2026-04-28 02:36:18 +05:30
Sydney
1bfa382d9d fix: ui inconsistency in text in Message header 2026-04-28 02:31:23 +05:30
Sydney
3e1a179767 fix: add message field allow reply via contact to show ticket message 2026-04-28 02:02:16 +05:30
RitvikSardana
d03c8d91ed Merge pull request #3248 from RitvikSardana/cleanup
chore: cleanup helpdesk UI
2026-04-26 23:43:29 +05:30
Ritvik Sardana
4f1617e9b6 fix: reactive relatedActivities 2026-04-26 23:38:57 +05:30
Ritvik Sardana
623ae16748 fix: remove content from local storage 2026-04-26 23:13:38 +05:30
Ritvik Sardana
24d846b00f fix: loading state of activities 2026-04-26 23:13:27 +05:30
Ritvik Sardana
47ca62e0f3 fix: filters length 2026-04-26 22:40:33 +05:30
Ritvik Sardana
8a8e4e4945 chore: upgrade frappe-ui 2026-04-26 22:40:33 +05:30
Ritvik Sardana
44dce9e36f fix: use new assignTo component for mobiel view 2026-04-26 22:40:33 +05:30
Ritvik Sardana
dc7b7ddebc chore: code cleanup 2026-04-26 22:40:33 +05:30
frappe-pr-bot
75da8a7880 chore: update POT file 2026-04-26 10:44:49 +00:00
RitvikSardana
82a81ffe7d Merge branch 'develop' into cleanup 2026-04-25 15:29:27 +05:30
Ritvik Sardana
f18e4c1656 chore: code cleanup 2026-04-25 14:09:56 +05:30
Ritvik Sardana
ccd8185f59 fix: mobile view cleanup 2026-04-25 14:09:56 +05:30
Ritvik Sardana
308c6613d8 fix: add log for closing tickets 2026-04-25 14:09:56 +05:30
Sydney
569c26d18b chore: code cleanup 2026-04-25 14:08:33 +05:30
Sydney
bc05cadbc2 fix: add saved replies before signature and fix cursor position 2026-04-25 14:08:33 +05:30
Sydney Gomes
479850f32e feat: email signature (#3188)
* fix: add email signature in frontend

* feat: add email signature in profile frontend

* fix: improve fetching code

* fix: simplfy and improve dirty state handling for signature

* fix: cursor placement bugs and improve editor experience

* fix: remove tiptap focus on end of content code

* fix: remove strict image styles after parsing of image in email content and improve signature editor

* refactor: improve profile section and add preferences section in settings modal

* fix: add autocomplete in place of combobox

* fix: move preferences back to profile and add transitions for badges

* fix: add null check guards and maintain consistency

* fix: remove duplicated code

* fix: keep correct sender in communications and add switching of from email through editor

* fix: add fallback for from as session user if no from mail is found

* fix: expand content on select all for replied content

* fix: add back email content css

* fix: resolve bugs and use email doctype for sender name

* fix: linter issues

* fix: copy changes

* fix: add loading states for button inputs

* fix: use from email through useStorage

* fix: improve code quality

* fix: code cleanup

* fix: add type select back to form control

* fix: sender email bug

* fix: give preference to user info while display communication name

* fix: improve code quality and sync from emails without reload

* fix: use div instead of button for notifications panel

* fix: flicker while selecting from mail

* fix: remove extra padding

* fix: improve dirty badge state in Email Preferences

* fix: reduce number of calls made for emails

* fix: spacing between like and dislike buttons

* fix: improve from email mapping logic and ui fixes
2026-04-25 14:08:33 +05:30
Sydney
b63d026edc fix: spacing between like and dislike buttons 2026-04-25 14:08:33 +05:30
Sydney
980666cc8f fix: article layouting 2026-04-25 14:08:33 +05:30
Sydney
07b72bb47e fix: mobile issues where text was misalligned and agent response was missing 2026-04-25 14:08:33 +05:30
Ritvik Sardana
16d0675ee0 fix: better copy 2026-04-25 13:59:46 +05:30
Ritvik Sardana
374e9252f7 fix: add log for closing tickets 2026-04-25 13:44:17 +05:30
Sydney Gomes
533cb18d09 Merge pull request #3247 from aerodeval/fix-sign-saved-reply
fix: add saved replies before signature and fix cursor position
2026-04-24 23:11:36 +05:30
Sydney
c0b05f9a34 chore: code cleanup 2026-04-24 23:06:06 +05:30
Sydney
3c44b626ab fix: add saved replies before signature and fix cursor position 2026-04-24 23:03:19 +05:30
Harish Chandran
6811819475 Merge remote-tracking branch 'upstream/develop' into feat/dark-mode 2026-04-24 19:35:35 +05:30
Harish Chandran
e17b8f8b85 fix: reviews real chart bars/axis use CSS variables and re-compute on toggle 2026-04-24 19:01:50 +05:30
Harish Chandran
1a24056c04 fix: stacked avatar ring/border use surface-white CSS var so it flips with theme 2026-04-24 18:54:25 +05:30
Harish Chandran
4768111d59 fix: propagate theme toggles into email iframe without reload 2026-04-24 18:47:57 +05:30
Harish Chandran
ee4d54e5ad fix: strip color-related inline styles from email HTML so the iframe theme drives readability 2026-04-24 18:45:00 +05:30
Sydney Gomes
91c175fcad feat: email signature (#3188)
* fix: add email signature in frontend

* feat: add email signature in profile frontend

* fix: improve fetching code

* fix: simplfy and improve dirty state handling for signature

* fix: cursor placement bugs and improve editor experience

* fix: remove tiptap focus on end of content code

* fix: remove strict image styles after parsing of image in email content and improve signature editor

* refactor: improve profile section and add preferences section in settings modal

* fix: add autocomplete in place of combobox

* fix: move preferences back to profile and add transitions for badges

* fix: add null check guards and maintain consistency

* fix: remove duplicated code

* fix: keep correct sender in communications and add switching of from email through editor

* fix: add fallback for from as session user if no from mail is found

* fix: expand content on select all for replied content

* fix: add back email content css

* fix: resolve bugs and use email doctype for sender name

* fix: linter issues

* fix: copy changes

* fix: add loading states for button inputs

* fix: use from email through useStorage

* fix: improve code quality

* fix: code cleanup

* fix: add type select back to form control

* fix: sender email bug

* fix: give preference to user info while display communication name

* fix: improve code quality and sync from emails without reload

* fix: use div instead of button for notifications panel

* fix: flicker while selecting from mail

* fix: remove extra padding

* fix: improve dirty badge state in Email Preferences

* fix: reduce number of calls made for emails

* fix: spacing between like and dislike buttons

* fix: improve from email mapping logic and ui fixes
2026-04-24 17:05:36 +05:30
Harish Chandran
8b2db21839 fix: shared dataTheme ref so JS-driven charts re-run on toggle 2026-04-24 16:58:36 +05:30
Ritvik Sardana
3ef7b18fe6 fix: add column resize 2026-04-24 16:30:08 +05:30
Ritvik Sardana
3a8a5d9724 fix: show error always 2026-04-24 11:39:33 +05:30
Ritvik Sardana
670cc182a9 fix: warnings in ticket view 2026-04-24 11:05:53 +05:30
Harish Chandran
188f1664cf fix: RecentFeedback empty-state placeholder bars and axis use CSS variables 2026-04-24 09:47:42 +05:30
Harish Chandran
f0ee9e8caa fix: chart colors in AvgTimeMetrics use CSS variables and recompute on theme toggle 2026-04-24 09:24:18 +05:30
Harish Chandran
d3c6b6f68a fix: use surface-selected for active sidebar tabs so they contrast in both modes 2026-04-24 09:05:58 +05:30
Harish Chandran
3008f9a9c3 fix: switch hardcoded fill colors to currentColor in custom icons 2026-04-24 08:59:09 +05:30
Harish Chandran
d0a4631dfd fix: make ComboboxInput backgrounds transparent so they flip with theme 2026-04-24 08:55:57 +05:30
Harish Chandran
03c069c40e fix: thumbs icons use currentColor + KB article title uses bg-transparent 2026-04-24 08:14:31 +05:30
Harish Chandran
cae08a7fcd fix: move theme toggle from profile to user dropdown 2026-04-24 08:12:30 +05:30
Sydney Gomes
649858cf1d Merge pull request #3242 from aerodeval/fix-article-title
fix: article layouting
2026-04-24 03:08:44 +05:30
Sydney
1fbf122261 fix: spacing between like and dislike buttons 2026-04-24 03:04:08 +05:30
Sydney
38e5f06913 fix: article layouting 2026-04-24 02:59:32 +05:30
Harish Chandran
a6d04d4281 fix: drop redundant bg-surface-white on search Input labels so corners don't bleed 2026-04-23 20:12:40 +05:30
Harish Chandran
1856449815 feat: add theme switcher (Light/Dark/System) to Profile settings 2026-04-23 19:50:23 +05:30
Sydney Gomes
37ab5233a8 Merge pull request #3241 from aerodeval/fix/mobile-issues
fix: mobile ui issues
2026-04-23 19:46:40 +05:30
Harish Chandran
41d3222eba feat: initialize theme on app mount via frappe-ui useTheme 2026-04-23 18:57:09 +05:30
Harish Chandran
4791dabde7 feat: replace bg-black/text-black with surface-gray-7/ink-gray-9 semantics 2026-04-23 18:48:42 +05:30
Harish Chandran
0165d0f1ae feat: swap remaining bg-gray-400 indicators and bg-gray-600 overlay for exact ink-gray semantics 2026-04-23 18:39:25 +05:30
Harish Chandran
634247f6db feat: replace remaining border-gray-100 with semantic outline token 2026-04-23 17:27:05 +05:30
Harish Chandran
c928c45c6b feat: replace hardcoded gray border in Autocomplete subtle variant 2026-04-23 17:24:25 +05:30
Harish Chandran
bccb862a16 feat: add semantic page background and color-scheme 2026-04-23 16:52:17 +05:30
Sydney
7bccf8e3e0 fix: mobile issues where text was misalligned and agent response was missing 2026-04-23 16:47:06 +05:30
Harish Chandran
de0edefbd7 feat: apply update-tailwind-classes script 2026-04-23 16:21:06 +05:30
RitvikSardana
209e87c8b6 Merge pull request #3232 from RitvikSardana/fix/reply-broken
fix: use v-show instead of v-if
2026-04-23 09:28:06 +05:30
Ritvik Sardana
03e32cf5a2 fix: use v-show instead of v-if 2026-04-23 09:25:47 +05:30
RitvikSardana
94149406c9 Merge pull request #3231 from RitvikSardana/fix/saved-reply
fix: apply saved reply on the email editor
2026-04-23 00:43:21 +05:30
Ritvik Sardana
767fdef7b9 fix: apply saved reply on the email editor 2026-04-23 00:33:42 +05:30
MochaMind
2cc25cdc6a chore: Burmese translations 2026-04-22 22:49:17 +05:30
MochaMind
8e7ae52bd8 chore: Persian translations 2026-04-22 22:49:14 +05:30
MochaMind
207970f51f chore: Indonesian translations 2026-04-22 22:49:12 +05:30
MochaMind
05e6a2ab8f chore: Portuguese, Brazilian translations 2026-04-22 22:49:10 +05:30
MochaMind
7079d22504 chore: Turkish translations 2026-04-22 22:49:06 +05:30
MochaMind
7591625336 chore: Slovenian translations 2026-04-22 22:49:04 +05:30
MochaMind
ff6f299b6b chore: Portuguese translations 2026-04-22 22:49:02 +05:30
MochaMind
cbf3acdd02 chore: Polish translations 2026-04-22 22:49:00 +05:30
MochaMind
16e3cf109b chore: Dutch translations 2026-04-22 22:48:58 +05:30
MochaMind
a58c2ff9b1 chore: Italian translations 2026-04-22 22:48:56 +05:30
MochaMind
6d1b35afa5 chore: Danish translations 2026-04-22 22:48:53 +05:30
MochaMind
4a00bcaec9 chore: Czech translations 2026-04-22 22:48:51 +05:30
MochaMind
b072a11c3e chore: French translations 2026-04-22 22:48:48 +05:30
RitvikSardana
6f3270952d Merge pull request #3230 from RitvikSardana/fix/ticket-title
fix: ticket title
2026-04-22 18:04:13 +05:30
Ritvik Sardana
ce4c09bf39 fix: ticket title 2026-04-22 18:01:43 +05:30
RitvikSardana
b6a749f0e9 Merge pull request #3229 from RitvikSardana/fix/list-status
fix: indicator color
2026-04-22 17:44:02 +05:30
Ritvik Sardana
5275aa94ba fix: indicator color 2026-04-22 17:43:17 +05:30
RitvikSardana
1b3189b83b Merge pull request #3228 from RitvikSardana/fix/ui-ticketview
fix(ticket-view): padding and font
2026-04-22 17:40:29 +05:30
Ritvik Sardana
f9e920ce37 fix(ticket-view): padding and font 2026-04-22 17:39:21 +05:30
RitvikSardana
b761ac266a Merge pull request #3224 from RitvikSardana/fix/sidebar-field
fix(ticketAgent): sidepanel and reply icons
2026-04-22 17:37:11 +05:30
Ritvik Sardana
c885e66366 fix: edit customer dialog 2026-04-22 14:44:36 +05:30
Ritvik Sardana
c749fb52d7 fix: indicator icon size 2026-04-22 13:23:46 +05:30
Ritvik Sardana
36417e2b81 fix: better way to show reply buttons 2026-04-22 12:14:32 +05:30
Ritvik Sardana
405a0acffe fix: show activity header only for calls 2026-04-22 12:11:35 +05:30
Ritvik Sardana
9468c830be fix: animation in communication area 2026-04-22 12:02:18 +05:30
Ritvik Sardana
3913f4bbb4 fix: dont show border when not needed 2026-04-22 11:48:57 +05:30
Ritvik Sardana
9f7407869f fix: unwanted backend calls 2026-04-22 11:32:02 +05:30
Ritvik Sardana
c41ed00541 fix: loading state of activities 2026-04-22 01:26:54 +05:30
Ritvik Sardana
f2040c931f fix: assign message 2026-04-22 00:10:19 +05:30
RitvikSardana
a0249173d2 chore: version bump 2026-04-21 23:50:16 +05:30
RitvikSardana
4f8aaa07f9 Merge pull request #3223 from netchampfaris/fix/app-visibility-for-helpdesk
fix: restrict Helpdesk app visibility by role
2026-04-21 22:39:40 +05:30
Ritvik Sardana
51824e90ff chore: code cleanup 2026-04-21 22:34:54 +05:30
Ritvik Sardana
3cdad35ae6 fix: hover state 2026-04-21 22:29:20 +05:30
Ritvik Sardana
62027ba60e fix: sidepanel styles 2026-04-21 20:42:56 +05:30
Ritvik Sardana
874609f674 chore: code cleanup 2026-04-21 20:20:20 +05:30
Ritvik Sardana
72d8bae552 fix: dont show unnecessary fields in sidebar 2026-04-21 20:19:41 +05:30
Ritvik Sardana
3b1bfdb02a refactor: ticket side panel design 2026-04-21 19:30:40 +05:30
Ritvik Sardana
75c3b4845e fix: email icon color 2026-04-21 18:02:33 +05:30
Ritvik Sardana
d24e30ec28 fix: spacing in reply buttons 2026-04-21 17:53:00 +05:30
Ritvik Sardana
6d8fd5435e fix: activity header dropdown placement 2026-04-21 17:42:09 +05:30
Ritvik Sardana
1c184f8339 fix: icon width 2026-04-21 17:38:25 +05:30
Faris Ansari
d8181027d3 fix: restrict helpdesk app visibility by role 2026-04-21 16:39:19 +05:30
Ritvik Sardana
5395cf92ee fix: navigate to contact from ticket view 2026-04-20 10:54:30 +05:30
RitvikSardana
5a1a06f6fe Merge pull request #3221 from RitvikSardana/placeholder-color
fix: placeholder color
2026-04-20 09:16:13 +05:30
Ritvik Sardana
0932744558 fix: placeholder color 2026-04-19 23:59:31 +05:30
Sydney Gomes
934029a094 Merge pull request #3211 from aerodeval/text-clear-formatting
fix: add clear formatting utility in text editor
2026-04-19 21:56:41 +05:30
Sydney
0aab4f694f fix: add extenstion for text editor used in customer portal 2026-04-19 21:38:53 +05:30
Sydney
dc40aab97a fix: reorganise utilities in customer ticket portal 2026-04-19 21:30:34 +05:30
Sydney
d9c6992a73 fix: improve grouping of editor utilities 2026-04-19 21:18:15 +05:30
frappe-pr-bot
5ee0d85dfc chore: update POT file 2026-04-19 10:44:04 +00:00
MochaMind
4b970831b3 chore: Indonesian translations 2026-04-18 21:03:13 +05:30
MochaMind
255662d424 chore: Portuguese, Brazilian translations 2026-04-18 21:03:12 +05:30
MochaMind
b7112f939f chore: Portuguese translations 2026-04-18 21:03:11 +05:30
MochaMind
28fc229779 chore: Polish translations 2026-04-18 21:03:10 +05:30
MochaMind
6583968b20 chore: Italian translations 2026-04-18 21:03:08 +05:30
MochaMind
d25f12e666 chore: Danish translations 2026-04-18 21:03:07 +05:30
MochaMind
650275150a chore: French translations 2026-04-18 21:03:06 +05:30
Ritvik Sardana
073b60e81b fix: add new contact dialog and composable 2026-04-18 14:57:09 +05:30
Ritvik Sardana
40ba332b1b refactor: customer composable 2026-04-18 14:56:53 +05:30
Ritvik Sardana
8c61c76b4d refactor: add local cache in customer composable 2026-04-18 14:56:53 +05:30
Ritvik Sardana
b8719928da fix: unlink contact from customers 2026-04-18 14:56:32 +05:30
Ritvik Sardana
2609ba9288 fix: delete contact 2026-04-18 14:56:32 +05:30
Ritvik Sardana
81a9f2b1e8 fix: add analytics in contact page
remove email domain linking & hd_customer custom list view perm
2026-04-18 14:56:32 +05:30
Ritvik Sardana
4e64064326 fix: add ticket tab in contact page 2026-04-18 14:56:32 +05:30
Ritvik Sardana
2eafe03235 fix: add contact page 2026-04-18 14:56:32 +05:30
Ritvik Sardana
c291d3be7a fix: edit dialog of customer 2026-04-18 14:55:20 +05:30
Ritvik Sardana
f2efa32681 fix(patch): for moving contact dynamic link to HD Customer contact 2026-04-18 14:47:52 +05:30
Ritvik Sardana
ea8991cdf5 chore: remove old doctypes 2026-04-18 14:47:52 +05:30
Ritvik Sardana
5367cf144d chore: remove old doctypes 2026-04-18 14:47:52 +05:30
Ritvik Sardana
589210319f fix(patch): handle customer and customer manager roles and default perms 2026-04-18 14:47:52 +05:30
Ritvik Sardana
0bb2416e03 test: add test cases for contact in org and multiple orgs 2026-04-18 14:46:49 +05:30
Ritvik Sardana
9892332d2d fix(styles): contact card 2026-04-18 14:45:52 +05:30
Ritvik Sardana
dabe8c7915 feat: add customer analytics 2026-04-18 14:45:52 +05:30
Ritvik Sardana
6ba0837025 chore: code cleanup 2026-04-18 14:45:52 +05:30
Ritvik Sardana
5752a78f40 chore: bump frappe-ui version 2026-04-18 14:45:52 +05:30
Ritvik Sardana
57f854edec fix: show last seen instead of last modified 2026-04-18 14:43:33 +05:30
Ritvik Sardana
c41f6eab4d fix(styles): customer form 2026-04-18 14:43:04 +05:30
Ritvik Sardana
a20e0441a7 fix(patch): custom field in user invitation
Also renames custom_customer to customer
2026-04-18 14:43:04 +05:30
Ritvik Sardana
6e14cd51e9 fix: icon prefix in country 2026-04-18 14:43:04 +05:30
Ritvik Sardana
aa401ad6ca fix: auto fetch user's country 2026-04-18 14:43:04 +05:30
Ritvik Sardana
b8955f18b8 fix: improve revoke animation in pendingInvitesModal 2026-04-18 14:43:04 +05:30
Ritvik Sardana
a9344cde32 fix: remove website field 2026-04-18 14:43:04 +05:30
Ritvik Sardana
c23db5d052 fix(styles): ticket list view 2026-04-18 14:43:04 +05:30
Ritvik Sardana
8581cfdab7 fix: show submenu in dropdown for roles 2026-04-18 14:43:04 +05:30
Ritvik Sardana
a8315dc0f7 fix: improve ticket tab UI 2026-04-18 14:43:04 +05:30
Ritvik Sardana
75f4bcf155 fix: add ticket tab UI 2026-04-18 14:43:04 +05:30
Ritvik Sardana
54d1941bc8 fix(styles): edit and new customer dialog 2026-04-18 14:43:04 +05:30
Ritvik Sardana
a03dd4894c fix: styling in component 2026-04-18 14:42:31 +05:30
Ritvik Sardana
c01896d58c fix: add TagInput for inviting new contacts 2026-04-18 14:42:31 +05:30
Ritvik Sardana
d61abef91e fix: new contact invitation flow 2026-04-18 14:42:31 +05:30
Ritvik Sardana
be33dd85e7 fix: modal for inviting new contacts 2026-04-18 14:42:31 +05:30
Ritvik Sardana
83326a2c3d feat: add existing contact flow 2026-04-18 14:42:31 +05:30
Ritvik Sardana
48b2a1eb6b fix: contacts tab 2026-04-18 14:42:07 +05:30
Ritvik Sardana
5dd33bb455 chore: code cleanup 2026-04-18 14:40:17 +05:30
Ritvik Sardana
b14f28c509 fix: improve ContactCard 2026-04-18 14:40:17 +05:30
Ritvik Sardana
8f321e5687 fix: add contacts tab 2026-04-18 14:40:17 +05:30
Ritvik Sardana
aeda7f8532 refactor: hd_customer doctype 2026-04-18 14:40:17 +05:30
MochaMind
ff16b9dea7 chore: Burmese translations 2026-04-17 20:22:09 +05:30
MochaMind
67778b4022 chore: Persian translations 2026-04-17 20:22:06 +05:30
MochaMind
5a71390b3d chore: Indonesian translations 2026-04-17 20:22:05 +05:30
MochaMind
823a3772fe chore: Portuguese, Brazilian translations 2026-04-17 20:22:03 +05:30
MochaMind
bfdce5a65c chore: Turkish translations 2026-04-17 20:22:01 +05:30
MochaMind
56ea1c109d chore: Slovenian translations 2026-04-17 20:21:59 +05:30
MochaMind
ae646e71d9 chore: Portuguese translations 2026-04-17 20:21:58 +05:30
MochaMind
210e96452b chore: Polish translations 2026-04-17 20:21:57 +05:30
MochaMind
a9cb7597d6 chore: Italian translations 2026-04-17 20:21:55 +05:30
MochaMind
d2120fdf62 chore: Hungarian translations 2026-04-17 20:21:54 +05:30
MochaMind
7d9ed7dfe6 chore: Danish translations 2026-04-17 20:21:52 +05:30
MochaMind
f770dedb7f chore: Czech translations 2026-04-17 20:21:51 +05:30
MochaMind
d541ef0980 chore: French translations 2026-04-17 20:21:49 +05:30
MochaMind
2e6a271561 chore: Burmese translations 2026-04-16 20:11:05 +05:30
MochaMind
fa672a3e8b chore: Persian translations 2026-04-16 20:11:02 +05:30
MochaMind
208ee525b7 chore: Indonesian translations 2026-04-16 20:11:00 +05:30
MochaMind
8ac14bb462 chore: Portuguese, Brazilian translations 2026-04-16 20:10:59 +05:30
MochaMind
f1a973b1c6 chore: Vietnamese translations 2026-04-16 20:10:57 +05:30
MochaMind
a7a974b41e chore: Turkish translations 2026-04-16 20:10:55 +05:30
MochaMind
5a2646d951 chore: Slovenian translations 2026-04-16 20:10:53 +05:30
MochaMind
9d9570fd71 chore: Portuguese translations 2026-04-16 20:10:52 +05:30
MochaMind
86b5ca7b79 chore: Polish translations 2026-04-16 20:10:50 +05:30
MochaMind
616f1ccb09 chore: Dutch translations 2026-04-16 20:10:49 +05:30
MochaMind
155d4d4932 chore: Italian translations 2026-04-16 20:10:47 +05:30
MochaMind
1eaaf2f58e chore: Danish translations 2026-04-16 20:10:44 +05:30
MochaMind
de60d726c1 chore: Czech translations 2026-04-16 20:10:43 +05:30
MochaMind
00ac7b9c9d chore: Spanish translations 2026-04-16 20:10:40 +05:30
MochaMind
571373bd0c chore: French translations 2026-04-16 20:10:39 +05:30
MochaMind
090cceca70 chore: Burmese translations 2026-04-15 20:01:26 +05:30
MochaMind
a9975abb12 chore: Persian translations 2026-04-15 20:01:23 +05:30
MochaMind
b7cd6c1b11 chore: Indonesian translations 2026-04-15 20:01:22 +05:30
MochaMind
d1c76d940d chore: Portuguese, Brazilian translations 2026-04-15 20:01:20 +05:30
MochaMind
6abc29f5f0 chore: Vietnamese translations 2026-04-15 20:01:19 +05:30
MochaMind
37e472c377 chore: Turkish translations 2026-04-15 20:01:16 +05:30
MochaMind
0b24d4481e chore: Slovenian translations 2026-04-15 20:01:15 +05:30
MochaMind
7c631a47d2 chore: Portuguese translations 2026-04-15 20:01:13 +05:30
MochaMind
8a3f31b37a chore: Polish translations 2026-04-15 20:01:11 +05:30
MochaMind
e900050d5b chore: Italian translations 2026-04-15 20:01:09 +05:30
MochaMind
826cebc1c8 chore: Danish translations 2026-04-15 20:01:06 +05:30
MochaMind
b245b67999 chore: Czech translations 2026-04-15 20:01:04 +05:30
MochaMind
e8d40fb369 chore: French translations 2026-04-15 20:01:01 +05:30
Sydney Gomes
b252c68072 Merge pull request #3218 from aerodeval/bump-fui
chore: bump fui
2026-04-15 19:59:25 +05:30
RitvikSardana
38ddf58a5f Merge pull request #3199 from RitvikSardana/ui-fixes2
fix: UI inconsistency in ticket view
2026-04-15 16:37:59 +05:30
Sydney
003e90d369 fix: update frappe ui to latest version 2026-04-15 12:51:28 +05:30
Sydney Gomes
a59fa105de Merge pull request #3215 from aerodeval/fix-email-threading
fix: handle missing headers in email threading
2026-04-14 19:59:41 +05:30
MochaMind
9106b8cf2f chore: Thai translations 2026-04-14 19:47:58 +05:30
Sydney
11c8e281d8 fix: improve logic for threading and cleanup code 2026-04-14 13:06:54 +05:30
Ritvik Sardana
d2fd934b5e fix: spacing in SLA 2026-04-13 22:42:54 +05:30
Ritvik Sardana
bf1f9307df chore: code cleanup 2026-04-13 22:10:37 +05:30
Sydney
3257a438f6 fix: handle forwarded and missing headers in email threading 2026-04-13 14:04:49 +05:30
Ritvik Sardana
5b539f090d fix: better time display in Datetime fields 2026-04-12 23:32:03 +05:30
Ritvik Sardana
08efce2d30 chore: inject using '!' 2026-04-12 23:17:00 +05:30
Ritvik Sardana
2375838424 chore: update AGENTS.md 2026-04-12 22:32:23 +05:30
MochaMind
2c718daf3c chore: Croatian translations 2026-04-12 19:35:52 +05:30
frappe-pr-bot
171e92ce23 chore: update POT file 2026-04-12 10:43:52 +00:00
Ritvik Sardana
19fd0b1684 fix: component styling 2026-04-11 17:51:47 +05:30
Ritvik Sardana
d477d790d1 fix: styling contact tab 2026-04-11 17:03:04 +05:30
Ritvik Sardana
17c6c35ea9 fix: spellings 2026-04-11 16:33:46 +05:30
Ritvik Sardana
58d2ba2622 fix: email styles 2026-04-11 13:57:30 +05:30
Ritvik Sardana
832ed56213 fix: editor styling 2026-04-11 13:51:41 +05:30
Ritvik Sardana
cc6ef6637f fix: alignment in editor, activity, listview & sidebar 2026-04-11 13:42:18 +05:30
MochaMind
dfccb2f813 chore: Swedish translations 2026-04-10 19:34:57 +05:30
Sydney
3a6798d6b0 chore: improve formatting of css code 2026-04-10 17:48:45 +05:30
Sydney
fd1ff8fffd fix: add stricter selection for pre tags 2026-04-10 17:47:07 +05:30
Sydney
3fac3ab859 feat: add clear formatting utility for all text editor based components 2026-04-10 17:32:35 +05:30
Sydney
2b9209a859 fix: add proper direction handling based on text and fix pre tag bugs 2026-04-10 17:20:43 +05:30
Ritvik Sardana
92165a04a6 chore: code cleanup 2026-04-09 11:44:25 +05:30
Ritvik Sardana
a269b3d6e9 fix: import function 2026-04-09 11:40:43 +05:30
Ritvik Sardana
4f9e2a25d5 fix: user full name in assign dropdown 2026-04-09 11:39:13 +05:30
Ritvik Sardana
2d152662cf chore: code cleanup 2026-04-09 11:08:10 +05:30
Ritvik Sardana
aaf1a2973e fix: subject cut in Ticket View 2026-04-09 10:09:22 +05:30
Ritvik Sardana
233859cb8d fix: alignment in timeline 2026-04-09 03:23:10 +05:30
Ritvik Sardana
12c46e576c chore: remove duplicate code 2026-04-09 03:12:34 +05:30
Ritvik Sardana
1e1a345bfa fix: editor styles 2026-04-09 02:43:02 +05:30
Ritvik Sardana
47885e1d90 fix: add new assignTo component 2026-04-09 02:42:45 +05:30
Ritvik Sardana
89f7b26703 fix: breadcrumbs padding 2026-04-09 00:45:25 +05:30
Ritvik Sardana
b6eabf0d53 fix: layout shift when toggle between contact and details tab 2026-04-09 00:00:20 +05:30
Pratik
fcf773d077 fix: update_default_views bug 2026-04-08 12:20:00 +05:30
Pratik
bc23f61970 fix: improve update function 2026-04-08 12:15:03 +05:30
Pratik
431795036f Merge remote-tracking branch 'upstream/develop' into fix/agent-landing-page-bugs 2026-04-08 12:07:58 +05:30
Pratik
e56b8fb99c fix: default view filters 2026-04-08 12:05:07 +05:30
MochaMind
0a40369688 chore: German translations 2026-04-07 18:59:02 +05:30
Sydney Gomes
5ad32a69ad Merge pull request #3206 from aerodeval/fix-fiter-bug
fix:  filter unexpected behaviour and view routing
2026-04-07 17:24:20 +05:30
Sydney
c780b3b60f fix: remove row click routing to ticket 2026-04-07 16:38:50 +05:30
Sydney
e0c04bcc41 fix: revert a11y code added for tab navigation in list view 2026-04-07 12:14:39 +05:30
Ritvik Sardana
dff9c1620c fix(styles): email and comment box 2026-04-06 19:33:12 +05:30
MochaMind
a460667826 chore: Esperanto translations 2026-04-06 18:41:30 +05:30
MochaMind
e56454ed41 chore: Russian translations 2026-04-06 18:41:29 +05:30
MochaMind
ca5cb4677a chore: Serbian (Latin) translations 2026-04-06 18:41:28 +05:30
MochaMind
ae19aaf296 chore: Norwegian Bokmal translations 2026-04-06 18:41:26 +05:30
MochaMind
45710b3af1 chore: Bosnian translations 2026-04-06 18:41:25 +05:30
MochaMind
4b66df45fc chore: Burmese translations 2026-04-06 18:41:23 +05:30
MochaMind
e1e6975728 chore: Croatian translations 2026-04-06 18:41:22 +05:30
MochaMind
c1adfa0c5d chore: Thai translations 2026-04-06 18:41:21 +05:30
MochaMind
689a0a36e0 chore: Persian translations 2026-04-06 18:41:19 +05:30
MochaMind
edacc94a74 chore: Indonesian translations 2026-04-06 18:41:17 +05:30
MochaMind
c72fddfea0 chore: Portuguese, Brazilian translations 2026-04-06 18:41:16 +05:30
MochaMind
aaf6e8c8f0 chore: Vietnamese translations 2026-04-06 18:41:15 +05:30
MochaMind
f8c92eaece chore: Chinese Simplified translations 2026-04-06 18:41:13 +05:30
MochaMind
e4f4294601 chore: Turkish translations 2026-04-06 18:41:12 +05:30
MochaMind
bb3a2f75c5 chore: Swedish translations 2026-04-06 18:41:10 +05:30
MochaMind
1f19dbd72c chore: Serbian (Cyrillic) translations 2026-04-06 18:41:08 +05:30
MochaMind
c4c39d30ec chore: Slovenian translations 2026-04-06 18:41:07 +05:30
MochaMind
1e3dd15269 chore: Portuguese translations 2026-04-06 18:41:06 +05:30
MochaMind
3a76af7b46 chore: Polish translations 2026-04-06 18:41:04 +05:30
MochaMind
ae91fde698 chore: Dutch translations 2026-04-06 18:41:03 +05:30
MochaMind
1a758ac4a2 chore: Italian translations 2026-04-06 18:41:02 +05:30
MochaMind
588d5624d2 chore: Hungarian translations 2026-04-06 18:41:00 +05:30
MochaMind
c26e1ba9b8 chore: German translations 2026-04-06 18:40:59 +05:30
MochaMind
cb184d6301 chore: Danish translations 2026-04-06 18:40:58 +05:30
MochaMind
80cccf6864 chore: Czech translations 2026-04-06 18:40:57 +05:30
MochaMind
69379c3560 chore: Arabic translations 2026-04-06 18:40:55 +05:30
MochaMind
9e56ae2e11 chore: Spanish translations 2026-04-06 18:40:54 +05:30
MochaMind
5c2b2f5e81 chore: French translations 2026-04-06 18:40:52 +05:30
Ritvik Sardana
544798207c fix: font-color in To 2026-04-06 17:55:57 +05:30
Sydney Gomes
b76d2571f9 Merge pull request #3175 from aerodeval/settings-menu-fix
fix: truncate text for translations in settings menu
2026-04-06 16:09:27 +05:30
Sydney
4b458035f0 fix: add translation for tooltip labels 2026-04-06 16:00:35 +05:30
RitvikSardana
6f585e367a Merge pull request #3065 from dankgarlic1/fix/missing-role-checks
fix: patch authorization and privilege escalation
2026-04-06 15:49:09 +05:30
Sydney
c3079c9061 fix: truncation bugs for mobile and sidebar 2026-04-06 15:48:11 +05:30
Ritvik Sardana
68e003526c fix: resizer in sidebar 2026-04-06 15:45:11 +05:30
Ritvik Sardana
9d12326136 fix(ticket-sidebar): fix layout overflow and field truncation in details tab 2026-04-06 15:36:15 +05:30
Ritvik Sardana
50c60fa24a fix: max height in side panel 2026-04-06 15:33:42 +05:30
Ritvik Sardana
af453a119c fix(vite): dedupe prosemirror packages to prevent duplicate instances with local frappe-ui 2026-04-06 15:00:35 +05:30
Ritvik Sardana
fb87a14638 fix(autocomplete): fix x button overlapping search input text 2026-04-06 14:49:28 +05:30
RitvikSardana
ce5bca182c Merge pull request #3187 from frappe/pot_develop_2026-04-05
chore: update POT file
2026-04-06 13:53:49 +05:30
RitvikSardana
42901f018d Merge pull request #3171 from frappe/l10n_develop
chore: sync translations from crowdin
2026-04-06 13:50:32 +05:30
RitvikSardana
6dc4d8ad97 Merge pull request #3192 from RitvikSardana/fix/iframe-click
fix: iframe click does not close popover
2026-04-06 13:13:25 +05:30
RitvikSardana
6e4d02b874 Merge pull request #3191 from RitvikSardana/workspace-setup
fix: new submodule setup
2026-04-06 13:12:30 +05:30
Ritvik Sardana
1716acc427 fix: iframe click does not close popover 2026-04-06 13:05:26 +05:30
Ritvik Sardana
b2ae6d8fc0 fix: new submodule setup 2026-04-06 12:59:20 +05:30
Sydney Gomes
a483967f00 Merge pull request #3176 from aerodeval/scrollbar-fix
fix: improve sidepanel scrollbar  and fix quick filters
2026-04-06 12:45:18 +05:30
Sydney Gomes
08a4c7746b Merge pull request #3178 from aerodeval/filter-debounce
fix: add debounce to filter component
2026-04-06 12:39:50 +05:30
frappe-pr-bot
11bfebe13f chore: update POT file 2026-04-05 10:43:45 +00:00
Sydney
a4ede50fa4 fix: add debounce to filters 2026-04-03 17:40:38 +05:30
MochaMind
bcb483fe57 chore: Bosnian translations 2026-04-03 17:23:04 +05:30
MochaMind
4669332bc9 chore: Croatian translations 2026-04-03 17:23:02 +05:30
Sydney
7c3088de2c fix: improve sidepanel scrollbar and fix quick filters 2026-04-03 02:01:45 +05:30
Sydney
2d583532fd fix: truncate text for translations in settings menu 2026-04-03 00:59:11 +05:30
Sydney Gomes
761a672e09 Merge pull request #3170 from aerodeval/fix-sidebar-datetime
fix: display only date & datetime fields in date format in Ticket SIdebar
2026-04-03 00:10:15 +05:30
Sydney
02cb007537 fix: code cleanup 2026-04-02 19:22:09 +05:30
MochaMind
4b11cca96b chore: Esperanto translations 2026-04-02 16:32:30 +05:30
MochaMind
cf22f8331c chore: Russian translations 2026-04-02 16:32:29 +05:30
MochaMind
9c2fcc283e chore: Serbian (Latin) translations 2026-04-02 16:32:27 +05:30
MochaMind
cf7d0fbb3e chore: Norwegian Bokmal translations 2026-04-02 16:32:26 +05:30
MochaMind
6c24921eb6 chore: Bosnian translations 2026-04-02 16:32:24 +05:30
MochaMind
491fabdf84 chore: Burmese translations 2026-04-02 16:32:23 +05:30
MochaMind
e735f1fc4c chore: Croatian translations 2026-04-02 16:32:22 +05:30
MochaMind
615e5166d0 chore: Thai translations 2026-04-02 16:32:20 +05:30
MochaMind
93bba4b570 chore: Persian translations 2026-04-02 16:32:19 +05:30
MochaMind
a8f836dbcf chore: Indonesian translations 2026-04-02 16:32:17 +05:30
MochaMind
839654b343 chore: Portuguese, Brazilian translations 2026-04-02 16:32:16 +05:30
MochaMind
06c5b1a48e chore: Vietnamese translations 2026-04-02 16:32:14 +05:30
MochaMind
2dfe252029 chore: Chinese Simplified translations 2026-04-02 16:32:12 +05:30
MochaMind
412ada7775 chore: Turkish translations 2026-04-02 16:32:11 +05:30
MochaMind
89996fe94c chore: Swedish translations 2026-04-02 16:32:10 +05:30
MochaMind
26a2dca117 chore: Serbian (Cyrillic) translations 2026-04-02 16:32:08 +05:30
MochaMind
399bea9c20 chore: Slovenian translations 2026-04-02 16:32:06 +05:30
MochaMind
5b64bbec28 chore: Portuguese translations 2026-04-02 16:32:05 +05:30
MochaMind
4cff3ee8e4 chore: Polish translations 2026-04-02 16:32:03 +05:30
MochaMind
d883a09b97 chore: Dutch translations 2026-04-02 16:32:02 +05:30
MochaMind
d0d9a6ac68 chore: Italian translations 2026-04-02 16:32:00 +05:30
MochaMind
573cf539f3 chore: Hungarian translations 2026-04-02 16:31:59 +05:30
MochaMind
550d5a7886 chore: German translations 2026-04-02 16:31:57 +05:30
MochaMind
82dd647877 chore: Danish translations 2026-04-02 16:31:56 +05:30
MochaMind
fef8589dca chore: Czech translations 2026-04-02 16:31:55 +05:30
MochaMind
86722702df chore: Arabic translations 2026-04-02 16:31:53 +05:30
MochaMind
c4d69e16a6 chore: Spanish translations 2026-04-02 16:31:51 +05:30
MochaMind
fb1fa5d9fa chore: French translations 2026-04-02 16:31:50 +05:30
Sydney
8cc10bcd16 fix: prevent non-date fields from being parsed as valid dates 2026-04-01 19:49:05 +05:30
Sydney
e72627aa79 fix: display only date & datetime fields in date format 2026-04-01 19:29:31 +05:30
RitvikSardana
6b800f6b22 Merge pull request #3165 from RitvikSardana/fui-bump
chore: bump fui
2026-04-01 18:53:56 +05:30
Ritvik Sardana
e61c82041f chore: bump fui 2026-04-01 18:47:32 +05:30
RitvikSardana
3b919c4c5a Merge pull request #3150 from frappe/pot_develop_2026-03-29
chore: update POT file
2026-04-01 17:49:34 +05:30
RitvikSardana
4e651fc394 Merge pull request #3147 from frappe/l10n_develop
chore: sync translations from crowdin
2026-04-01 17:48:53 +05:30
RitvikSardana
690ff31ed4 Merge pull request #3160 from frappe/version-bump
chore: version bump
2026-04-01 16:26:54 +05:30
RitvikSardana
e7f927a895 chore: version bump 2026-04-01 16:22:09 +05:30
RitvikSardana
cf36a4b31d Merge pull request #3154 from RitvikSardana/excel-2
fix: scrollable table
2026-04-01 16:20:22 +05:30
Ritvik Sardana
788f1577b0 chore: code cleanup 2026-04-01 16:12:40 +05:30
MochaMind
3196c9c69c chore: Russian translations 2026-04-01 16:07:49 +05:30
Sydney Gomes
6ff5d39f0f Merge pull request #3156 from aerodeval/select-all-fix
fix: utilize tiptap select all command for tiptap based elements in dom
2026-04-01 14:58:58 +05:30
Sydney
160b4c9f83 fix: run tip-tap inbuilt select all after focus guard check 2026-04-01 14:50:21 +05:30
Sydney
3990ce5f6f fix: utilize tiptap select all command for tiptap based elements in dom 2026-04-01 12:37:07 +05:30
Ritvik Sardana
d905078245 fix: scrollable table 2026-03-31 18:17:32 +05:30
Sydney
afcd46a4c0 fix: add additional check for changing raised_by 2026-03-30 02:29:32 +05:30
Sydney
99ca799859 fix: add agent check in agent reply 2026-03-30 02:18:24 +05:30
Sydney
87fab4d643 fix: add agent check while assigning 2026-03-30 02:08:59 +05:30
frappe-pr-bot
8beed8cb04 chore: update POT file 2026-03-29 10:43:34 +00:00
MochaMind
ecbc05aed6 chore: Serbian (Latin) translations 2026-03-28 15:07:08 +05:30
MochaMind
4fd3c7314a chore: Serbian (Cyrillic) translations 2026-03-28 15:07:07 +05:30
Sydney Gomes
0a4de51eb9 refactor: improve welcome email content for onboarding(#3148)
* chore: improve onboarding experience

* fix: fix imports for image and gif

* fix: fix layout issues for the mailer

* fix: update layout of welcome mail and add user name in mail

* fix: update gifs

* chore: use videos instead of gifs for faster load

* fix: incorrect first name fetching in mail

* fix: handle administrator name scenario

* fix: update demo videos

* fix: update videos for demo

* fix: improve copy
2026-03-27 22:17:46 +05:30
MochaMind
16abb01689 chore: Russian translations 2026-03-27 14:15:57 +05:30
Pratik
ee572e7b70 refactor: remove my replies card 2026-03-27 11:39:00 +05:30
Harshit Raizada
2f0689bf5f fix: remove manual override of frappe in_test flag 2026-03-27 02:10:35 +05:30
Pratik
7113d05f93 fix: i18n 2026-03-26 18:01:27 +05:30
Pratik
b053212cba Merge remote-tracking branch 'upstream/develop' into fix/agent-landing-page-bugs 2026-03-26 17:37:54 +05:30
Pratik
2c12d15ac0 fix: default filter for new tickets 2026-03-26 17:30:23 +05:30
Pratik
e31ff19972 refactor: empty state css & filters in query 2026-03-26 16:42:54 +05:30
Pratik
fc6a3cef35 refactor: ui enhancements 2026-03-26 16:16:08 +05:30
Pratik
5e5f0ff488 fix: default view filters & improve empty state 2026-03-26 15:48:35 +05:30
MochaMind
2e23acc137 chore: Esperanto translations 2026-03-26 13:47:59 +05:30
MochaMind
18a93a48c8 chore: Russian translations 2026-03-26 13:47:57 +05:30
MochaMind
6bba0c2a56 chore: Serbian (Latin) translations 2026-03-26 13:47:56 +05:30
MochaMind
1d108baeff chore: Norwegian Bokmal translations 2026-03-26 13:47:54 +05:30
MochaMind
06912cd7c1 chore: Bosnian translations 2026-03-26 13:47:53 +05:30
MochaMind
b73cd1ac70 chore: Burmese translations 2026-03-26 13:47:50 +05:30
MochaMind
85aee81b4f chore: Croatian translations 2026-03-26 13:47:49 +05:30
MochaMind
b7836e7468 chore: Thai translations 2026-03-26 13:47:47 +05:30
MochaMind
43cb0814b1 chore: Persian translations 2026-03-26 13:47:46 +05:30
MochaMind
048b62d255 chore: Indonesian translations 2026-03-26 13:47:44 +05:30
MochaMind
703db19eea chore: Portuguese, Brazilian translations 2026-03-26 13:47:43 +05:30
MochaMind
7a570724ee chore: Vietnamese translations 2026-03-26 13:47:41 +05:30
MochaMind
158cfe2fff chore: Chinese Simplified translations 2026-03-26 13:47:40 +05:30
MochaMind
7b712e1e87 chore: Turkish translations 2026-03-26 13:47:38 +05:30
MochaMind
3f08f67d1e chore: Swedish translations 2026-03-26 13:47:37 +05:30
MochaMind
0693d0c34a chore: Serbian (Cyrillic) translations 2026-03-26 13:47:35 +05:30
MochaMind
da3d1099ec chore: Slovenian translations 2026-03-26 13:47:34 +05:30
MochaMind
8b0ddf74f2 chore: Portuguese translations 2026-03-26 13:47:33 +05:30
MochaMind
98214151af chore: Polish translations 2026-03-26 13:47:30 +05:30
MochaMind
21b2a3f7c4 chore: Dutch translations 2026-03-26 13:47:29 +05:30
MochaMind
4dbc5c1722 chore: Italian translations 2026-03-26 13:47:27 +05:30
MochaMind
1ace98c4d8 chore: Hungarian translations 2026-03-26 13:47:25 +05:30
MochaMind
d800ae4571 chore: German translations 2026-03-26 13:47:24 +05:30
MochaMind
a5a04535ec chore: Danish translations 2026-03-26 13:47:22 +05:30
MochaMind
b6bf8b6f3d chore: Czech translations 2026-03-26 13:47:21 +05:30
MochaMind
d78984ca0d chore: Arabic translations 2026-03-26 13:47:20 +05:30
MochaMind
e3c7e5935f chore: Spanish translations 2026-03-26 13:47:18 +05:30
MochaMind
3af0fabc87 chore: French translations 2026-03-26 13:47:17 +05:30
Sydney Gomes
15b246e31a Merge pull request #2969 from aerodeval/fix/-fix-ui-inconsistency
refactor: fix UI inconsistency all over Helpdesk
2026-03-26 04:28:30 +05:30
Sydney Gomes
8a3f1c13bb Merge pull request #3131 from aerodeval/fix/bump-tiptap-v3
fix: update tiptap to v3 and fix breaking changes
2026-03-25 19:45:31 +05:30
Ritvik Sardana
22647bbba2 chore: remove unwanted code 2026-03-25 17:31:50 +05:30
Ritvik Sardana
5002fde520 fix: add prosemirror package in optimizeDeps 2026-03-25 16:56:30 +05:30
Ritvik Sardana
04126b7100 fix: add yarn.lock back 2026-03-25 16:17:52 +05:30
RitvikSardana
a3b3625435 Merge pull request #3140 from frappe/l10n_develop
chore: sync translations from crowdin
2026-03-25 16:09:17 +05:30
RitvikSardana
d43588a97e Merge pull request #3139 from frappe/pot_develop_2026-03-22
chore: update POT file
2026-03-25 16:09:01 +05:30
MochaMind
9bac2c2c62 chore: Vietnamese translations 2026-03-25 13:40:26 +05:30
MochaMind
5b7c2f33df chore: Spanish translations 2026-03-25 13:40:25 +05:30
Sydney
6f303717e7 fix: update yarn.lock to latest versions 2026-03-25 10:22:52 +05:30
Jayanta
a853d8eb60 fix: set Link field page limit to 999 (#3137)
* fix: set pageLength to 999 for Link field type
2026-03-25 09:55:17 +05:30
Sydney Gomes
6177b1d3c4 Merge branch 'develop' into fix/bump-tiptap-v3 2026-03-24 16:34:52 +05:30
Sydney
77aa6106b5 fix: remove deduping and resolve gapcursor extension 2026-03-24 15:51:42 +05:30
Pratik
09ea167300 feat: add My Replies card 2026-03-24 14:39:20 +05:30
Pratik
9ed433499c fix: standard view filter & some ui bugs 2026-03-23 17:58:43 +05:30
MochaMind
40c9741396 chore: Swedish translations 2026-03-23 13:26:37 +05:30
frappe-pr-bot
b115e0875e chore: update POT file 2026-03-22 10:41:38 +00:00
Sydney
6c1065bdd7 fix: bump frappe-ui and code cleanup 2026-03-19 20:36:12 +05:30
RitvikSardana
21bc3aaef1 Merge pull request #2796 from pratikb64/feat/agent-landing-page
feat: add agent landing page
2026-03-19 16:42:24 +05:30
Harshit Raizada
95bfd47f92 fix: address reviewer comments 2026-03-19 13:20:08 +05:30
Harshit Raizada
03fb98df2f fix: isort ordering for frappe imports 2026-03-19 12:41:10 +05:30
Harshit Raizada
90a3319825 chore(format): fix linting and formatting for CI 2026-03-19 12:41:10 +05:30
Harshit Raizada
f339ea98fb chore(underscore): removed extra underscore 2026-03-19 12:41:10 +05:30
Harshit Raizada
738909db35 chore(leftover): forgot to stage 2026-03-19 12:41:10 +05:30
Harshit Raizada
3f23513ca6 test(security): add tests for agent role escalation and parameter injection 2026-03-19 12:41:10 +05:30
Harshit Raizada
345c34a6c6 test(hd_ticket): add security tests for unauthorized actions and info disclosure 2026-03-19 12:41:10 +05:30
Harshit Raizada
e79107eec0 fix(security): patch authorization and privilege escalation vulnerabilities 2026-03-19 12:41:10 +05:30
Ritvik Sardana
a26feb091d fix: add telemetry event for editing home page 2026-03-19 12:08:21 +05:30
Ritvik Sardana
67926881e8 refactor: pending tickets component 2026-03-19 11:55:25 +05:30
Ritvik Sardana
aca353709f fix: show standard badge in ViewBreadcrumbs 2026-03-19 11:28:35 +05:30
Ritvik Sardana
3726340b7f fix: use install and patch instead of fixtures
This ensures that for existing sites the sidebar remains the same
2026-03-19 10:44:05 +05:30
Ritvik Sardana
1425bf6467 fix(styles): recent feedback 2026-03-19 10:40:43 +05:30
Sydney
f562911bc4 fix: dependency bug by deduping and resolving prosemirror packages 2026-03-19 01:50:46 +05:30
Ritvik Sardana
6e076772c2 fix: downgrade frappe-ui version 2026-03-19 00:21:44 +05:30
Ritvik Sardana
f69bf283bd fix: revert viewcontrols condition in LVB 2026-03-19 00:16:28 +05:30
Pratik
7bf45926d8 fix: UI bugs 2026-03-18 15:14:08 +05:30
RitvikSardana
995140c666 Merge pull request #3126 from frappe/pot_develop_2026-03-15
chore: update POT file
2026-03-18 12:34:05 +05:30
Ritvik Sardana
f3882facd6 chore: rearrange functions 2026-03-18 12:17:04 +05:30
Sydney
246c0257d7 fix: update yarn.lock and imports 2026-03-18 11:36:08 +05:30
RitvikSardana
678e1ae4db Merge pull request #3118 from frappe/l10n_develop
chore: sync translations from crowdin
2026-03-18 11:16:47 +05:30
MochaMind
02afcd4c3c chore: Spanish translations 2026-03-18 03:52:39 +05:30
Sydney
b2e09375eb fix: bump up frappe-ui 2026-03-18 02:32:28 +05:30
Sydney
9aeac5b16c fix: update tiptap to v3 and fix breaking changes 2026-03-18 02:05:23 +05:30
Pratik
97a0265bd8 fix: text clipping in table 2026-03-17 16:41:24 +05:30
Pratik
20295eda92 fix: linter 2026-03-17 15:17:36 +05:30
Pratik
fdfdc5d90c fix: apply filters from params 2026-03-17 15:06:22 +05:30
Pratik
0687c1a816 fix: feedback fixture 2026-03-17 12:43:55 +05:30
Pratik
342ee4a68b fix: bugs 2026-03-17 12:28:18 +05:30
MochaMind
865bd99df0 chore: Portuguese, Brazilian translations 2026-03-16 03:18:57 +05:30
frappe-pr-bot
20b5ee931f chore: update POT file 2026-03-15 10:42:56 +00:00
MochaMind
4870d001d5 chore: Swedish translations 2026-03-13 01:04:51 +05:30
Pratik
715c0b02ad fix: hd view before_save logic 2026-03-12 22:53:52 +05:30
Pratik
1458840124 fix: is_standard bug & some other changes 2026-03-12 22:29:17 +05:30
Pratik
75ccfcba58 fix: pending tickets tabs order 2026-03-12 20:56:43 +05:30
Pratik
e0c1592b8d fix: fixture label title case 2026-03-12 17:16:00 +05:30
Pratik
228d1bfd69 fix: types 2026-03-12 16:45:41 +05:30
Pratik
635fe6f3ab fix: linter & props structure 2026-03-12 16:16:01 +05:30
Pratik
82b6bfac47 fix: add types & remove duplicate code 2026-03-12 14:43:56 +05:30
MochaMind
1cc3671930 chore: Swedish translations 2026-03-11 01:02:11 +05:30
Pratik
15985d6106 Merge remote-tracking branch 'upstream/develop' into feat/agent-landing-page 2026-03-10 12:53:04 +05:30
RitvikSardana
5fc838158c Merge pull request #3115 from RitvikSardana/fui-update
chore: bump frappe-ui
2026-03-10 01:00:22 +05:30
Ritvik Sardana
0d4ca83f5f chore: bump frappe-ui 2026-03-10 00:55:31 +05:30
RitvikSardana
ad35d8cb1b Merge pull request #3110 from frappe/pot_develop_2026-03-08
chore: update POT file
2026-03-10 00:32:44 +05:30
RitvikSardana
75a3690310 Merge pull request #3107 from frappe/l10n_develop
chore: sync translations from crowdin
2026-03-10 00:32:28 +05:30
RitvikSardana
375044022a Merge pull request #3106 from RitvikSardana/enhance-excel-paste
fix: excel pasting formating in text editor
2026-03-10 00:32:01 +05:30
Ritvik Sardana
daac5f8ccc fix: move cursor to after the table 2026-03-10 00:18:23 +05:30
Sydney
08f3cd3cad fix: add additional support for tabular data 2026-03-09 23:15:46 +05:30
Ritvik Sardana
401dcb85c9 chore: version bump 2026-03-09 23:15:46 +05:30
Ritvik Sardana
16429b781e fix: enhance excel pasted data 2026-03-09 23:15:46 +05:30
Pratik
ed1d6fe90b fix: linter error 2026-03-09 17:16:33 +05:30
Pratik
5864bb359a refactor: code cleanup 2026-03-09 12:29:38 +05:30
frappe-pr-bot
6be18800ac chore: update POT file 2026-03-08 10:40:23 +00:00
Sydney Gomes
8c4c502576 Merge pull request #3108 from aerodeval/handle-next-line
fix: allow shift+enter by removing .stop from keydown handler
2026-03-08 15:22:04 +05:30
MochaMind
86ca1cc939 chore: Serbian (Latin) translations 2026-03-08 01:38:21 +05:30
MochaMind
cf67e5982e chore: Serbian (Cyrillic) translations 2026-03-08 01:38:20 +05:30
Sydney
4b3b431749 fix: allow shift+enter by removing .stop from keydown handler 2026-03-07 21:59:39 +05:30
MochaMind
f7ae670d3e chore: Esperanto translations 2026-03-07 01:16:09 +05:30
MochaMind
06c7cb2e0d chore: Serbian (Latin) translations 2026-03-07 01:16:08 +05:30
MochaMind
ce5fd9807d chore: Norwegian Bokmal translations 2026-03-07 01:16:07 +05:30
MochaMind
beee851fc8 chore: Bosnian translations 2026-03-07 01:16:05 +05:30
MochaMind
97cc8df3a1 chore: Burmese translations 2026-03-07 01:16:04 +05:30
MochaMind
47e6aa93cf chore: Croatian translations 2026-03-07 01:16:03 +05:30
MochaMind
0a39dfc819 chore: Thai translations 2026-03-07 01:16:01 +05:30
MochaMind
f10a0ead76 chore: Persian translations 2026-03-07 01:16:00 +05:30
MochaMind
2f82d784f6 chore: Indonesian translations 2026-03-07 01:15:59 +05:30
MochaMind
445633e1f3 chore: Portuguese, Brazilian translations 2026-03-07 01:15:58 +05:30
MochaMind
a08b365b04 chore: Vietnamese translations 2026-03-07 01:15:56 +05:30
MochaMind
16c09008c0 chore: Chinese Simplified translations 2026-03-07 01:15:55 +05:30
MochaMind
ee02d83e83 chore: Turkish translations 2026-03-07 01:15:54 +05:30
MochaMind
68c56fb564 chore: Swedish translations 2026-03-07 01:15:53 +05:30
MochaMind
99e167563b chore: Serbian (Cyrillic) translations 2026-03-07 01:15:51 +05:30
MochaMind
85b5973a34 chore: Slovenian translations 2026-03-07 01:15:50 +05:30
MochaMind
b0fbd842c2 chore: Portuguese translations 2026-03-07 01:15:49 +05:30
MochaMind
19b847569f chore: Polish translations 2026-03-07 01:15:47 +05:30
MochaMind
3a0b8d0b0c chore: Dutch translations 2026-03-07 01:15:46 +05:30
MochaMind
76ceb80a72 chore: Italian translations 2026-03-07 01:15:45 +05:30
MochaMind
0aa2d91512 chore: Hungarian translations 2026-03-07 01:15:43 +05:30
MochaMind
b931fea9f5 chore: German translations 2026-03-07 01:15:42 +05:30
MochaMind
7e5fdb03dc chore: Danish translations 2026-03-07 01:15:41 +05:30
MochaMind
f689beb8e8 chore: Czech translations 2026-03-07 01:15:40 +05:30
MochaMind
5b092db083 chore: Arabic translations 2026-03-07 01:15:38 +05:30
MochaMind
5f2648c93e chore: Spanish translations 2026-03-07 01:15:37 +05:30
MochaMind
fbffed7ffb chore: French translations 2026-03-07 01:15:36 +05:30
MochaMind
2aae85be24 chore: Russian translations 2026-03-07 01:15:35 +05:30
Pratik
2f194796c6 refactor: empty states 2026-03-06 17:51:09 +05:30
Pratik
648535f99b fix: feedback empty state 2026-03-06 15:56:55 +05:30
Pratik
886737374a refactor: remove duplicate code 2026-03-06 15:15:19 +05:30
Pratik
555cdb2395 fix: hide delete button for standard views 2026-03-06 12:48:23 +05:30
RitvikSardana
59dfde114c Merge pull request #3105 from RitvikSardana/invite-endpoints
fix: add invite endpoints
2026-03-06 12:36:30 +05:30
Ritvik Sardana
f5ac2b1cd7 fix: add invite endpoints 2026-03-06 12:35:32 +05:30
Pratik
8ea79cbb4a Merge remote-tracking branch 'upstream/develop' into feat/agent-landing-page 2026-03-06 10:29:54 +05:30
RitvikSardana
e4bc729151 Merge pull request #3086 from frappe/l10n_develop
chore: sync translations from crowdin
2026-03-06 10:24:08 +05:30
RitvikSardana
d51b4e8a38 Merge pull request #3085 from frappe/pot_develop_2026-03-01
chore: update POT file
2026-03-06 10:23:45 +05:30
RitvikSardana
d60ea9c08e Merge pull request #3098 from RitvikSardana/email-src-tag
fix: dont delete src tag
2026-03-06 01:42:02 +05:30
Ritvik Sardana
4979045fb6 chore: code cleanup 2026-03-06 00:24:51 +05:30
Ritvik Sardana
b0e4b3be64 fix: dont delete src tag 2026-03-06 00:23:34 +05:30
RitvikSardana
1f82854620 Merge pull request #3097 from frappe/revert-3088-feat/custom-badges
Revert "feat: custom badges + first ticket badge"
2026-03-05 17:41:27 +05:30
RitvikSardana
6afaab9979 Revert "feat: custom badges + first ticket badge" 2026-03-05 17:35:42 +05:30
RitvikSardana
5362069adb Merge pull request #2975 from aerodeval/fix/email-formatting-bug
fix: email formatting bug
2026-03-05 17:32:20 +05:30
Pratik
cafc393de5 fix: linter 2026-03-05 17:02:31 +05:30
Pratik
b7a99de099 Merge remote-tracking branch 'upstream/develop' into feat/agent-landing-page 2026-03-05 16:55:49 +05:30
Sydney
bdbb31cb7a fix: simplify code and handle formatting bug 2026-03-05 15:03:26 +05:30
Sydney
ecb6179ff0 fix: handle cleanup of quoted content 2026-03-05 13:19:49 +05:30
Sydney
9cfd0eb596 fix: add to reply bug and caching of quoted content 2026-03-05 13:01:38 +05:30
Sydney
c8bd81b30c chore: solve linter issues 2026-03-05 12:09:50 +05:30
Sydney
bb0077d470 chore: remove unused imports 2026-03-05 10:58:30 +05:30
MochaMind
65a93f7a68 chore: Russian translations 2026-03-05 00:24:59 +05:30
Sydney
c6c820c33e fix: fix broken logic of Send mail button 2026-03-05 00:08:39 +05:30
Sydney
9a6ad95e0a fix: improve selection check for handling delete 2026-03-04 17:37:05 +05:30
Sydney
a5ee07c69e fix: handle select and delete all flaky behaviour 2026-03-04 17:08:05 +05:30
Sydney
5c34099d24 fix: handle select all in text editor 2026-03-04 15:35:38 +05:30
Sydney
738cd35a32 fix: make replied quote seamless with editor 2026-03-04 13:43:36 +05:30
MochaMind
a1d16363b9 chore: Swedish translations 2026-03-02 23:50:37 +05:30
MochaMind
298f899ae9 chore: Spanish translations 2026-03-02 23:50:26 +05:30
MochaMind
678213f31c chore: French translations 2026-03-02 23:50:25 +05:30
MochaMind
662b17f748 chore: Russian translations 2026-03-02 23:50:23 +05:30
Sydney
ea9cbe1fd6 fix: Set inner html as replied content directly on update of replied content 2026-03-02 15:11:20 +05:30
Sydney Gomes
46d751181a Merge pull request #3090 from aerodeval/fix/saved-replies-filter-bug
fix: fix saved replies scope filtering
2026-03-02 13:58:21 +05:30
RitvikSardana
f35ae6f596 Merge pull request #3088 from RitvikSardana/feat/custom-badges
feat: custom badges + first ticket badge
2026-03-02 13:49:20 +05:30
Sydney
91ed2d6c54 fix: fix saved replies scope filtering 2026-03-02 13:20:05 +05:30
Ritvik Sardana
7b317571a5 chore: revert formating 2026-03-02 11:54:39 +05:30
Ritvik Sardana
32f788270a chore: revert old changes 2026-03-02 11:52:14 +05:30
Ritvik Sardana
c2c1106503 fix: default value form script 2026-03-02 11:44:09 +05:30
Ritvik Sardana
16e51b7ce3 fix: add is_first_ticket field 2026-03-02 11:22:44 +05:30
Sydney
f18aa0de9f fix: handle quoted input empty condition 2026-03-02 01:40:05 +05:30
MochaMind
1ad6d8d4f9 chore: Russian translations 2026-03-01 23:42:59 +05:30
frappe-pr-bot
158e6f5bdf chore: update POT file 2026-03-01 10:40:51 +00:00
Sydney
85fbda17fa fix: chore code cleanup 2026-02-28 20:57:29 +05:30
Sydney
e8274e1c6f Merge branch 'develop' of https://github.com/frappe/helpdesk into fix/email-formatting-bug 2026-02-28 20:55:53 +05:30
Sydney
1766b289dd fix: handle replied content in seperate containers 2026-02-28 20:50:19 +05:30
Ritvik Sardana
23ec306ea9 feat: custom badges in agent portal 2026-02-28 17:07:47 +05:30
RitvikSardana
ba7b0b4071 Merge pull request #3082 from frappe/l10n_develop
chore: sync translations from crowdin
2026-02-28 12:10:30 +05:30
MochaMind
e3ffb460d0 chore: Russian translations 2026-02-27 23:18:30 +05:30
MochaMind
52ee89f7ba chore: Spanish translations 2026-02-27 23:18:28 +05:30
RitvikSardana
f583740541 Merge pull request #3079 from RitvikSardana/raised-by-meta
fix: remove depends_on in raised_by field
2026-02-27 16:44:34 +05:30
Ritvik Sardana
e7ba190fbe chore: version bump 2026-02-27 16:43:53 +05:30
Ritvik Sardana
94637de7ff fix: remove unwanted depends on in raised_by field 2026-02-27 16:43:29 +05:30
RitvikSardana
0a0a5d8a96 Merge pull request #3077 from aerodeval/fix/excel-table-paste-fix
fix: handle excel pasting in email editor
2026-02-27 16:14:54 +05:30
Sydney
b8c160f22b fix: fix improper key in extension 2026-02-27 15:59:34 +05:30
Sydney
5f682fe8ef fix: handle excel pasting in email editor 2026-02-27 13:53:50 +05:30
Sydney Gomes
5220bd7762 Merge pull request #3006 from aerodeval/fix/comment-box-mentions
fix: comment box mentions bug on edit
2026-02-27 11:51:37 +05:30
Sydney
7fb269ff04 feat: add keyup event support for saving comments 2026-02-27 11:35:52 +05:30
RitvikSardana
a237928bf9 Merge pull request #3063 from frappe/l10n_develop
chore: sync translations from crowdin
2026-02-27 10:55:06 +05:30
Sydney
0f1abb75a1 fix: comments discard button bug fix and mentions fix 2026-02-27 00:57:34 +05:30
MochaMind
0112488cdd chore: Serbian (Latin) translations 2026-02-26 23:00:46 +05:30
MochaMind
0006e98d44 chore: Swedish translations 2026-02-26 23:00:45 +05:30
MochaMind
0f056060fc chore: Serbian (Cyrillic) translations 2026-02-26 23:00:43 +05:30
MochaMind
ee8b612ca7 chore: Spanish translations 2026-02-26 23:00:42 +05:30
Sydney Gomes
943e96e176 Merge pull request #3074 from aerodeval/chore/remove-stop-session-develop
chore: remove unused code
2026-02-26 19:55:08 +05:30
Sydney
8945580d1d chore: remove unused code 2026-02-26 19:47:40 +05:30
RitvikSardana
1139ae97b9 Merge pull request #3071 from RitvikSardana/fix/export-api-auth
fix(auth): allow export api endpoint
2026-02-26 17:38:34 +05:30
Ritvik Sardana
2a062809e3 fix(auth): allow export api endpoint 2026-02-26 17:37:43 +05:30
Sydney Gomes
a1c6b46f39 Merge pull request #3009 from aerodeval/fix/time-localization
fix: time localisation bug
2026-02-26 15:54:52 +05:30
Sydney Gomes
025e3956dd Merge branch 'develop' into fix/time-localization 2026-02-26 15:35:40 +05:30
Sydney
a2394c2b59 chore: code cleanup 2026-02-26 15:18:21 +05:30
Sydney
e8cb8fc799 chore: truncate text for autocomplete 2026-02-26 15:08:12 +05:30
RitvikSardana
1eabe5ad50 Merge pull request #3068 from RitvikSardana/fix/select-field-no-options
fix(field): handle case when no options are set for select field
2026-02-26 13:25:27 +05:30
Sydney
5a0edfe4d7 fix: autocomplete import fixed 2026-02-26 13:13:09 +05:30
Ritvik Sardana
2f5099fbba fix(field): handle case when no options are set for select field 2026-02-26 13:09:30 +05:30
Sydney
173cd745eb chore: code cleanup 2026-02-26 12:05:17 +05:30
Sydney
a4bf8967c4 fix: hard reload after timezone is changed 2026-02-26 12:04:29 +05:30
Sydney
85824b6908 fix: force hard reload after language change and delay it so toast is visible 2026-02-26 12:02:27 +05:30
Sydney
70c621225e feat: add autocomplete switching in profile settings 2026-02-26 12:02:01 +05:30
MochaMind
890dedad69 chore: Esperanto translations 2026-02-25 22:59:02 +05:30
MochaMind
62518c0843 chore: Serbian (Latin) translations 2026-02-25 22:59:01 +05:30
MochaMind
44750ec641 chore: Norwegian Bokmal translations 2026-02-25 22:58:59 +05:30
MochaMind
343809f489 chore: Bosnian translations 2026-02-25 22:58:57 +05:30
MochaMind
9acf110b42 chore: Burmese translations 2026-02-25 22:58:56 +05:30
MochaMind
5eec882f14 chore: Croatian translations 2026-02-25 22:58:54 +05:30
MochaMind
fb97216bca chore: Thai translations 2026-02-25 22:58:53 +05:30
MochaMind
01437abc7e chore: Persian translations 2026-02-25 22:58:51 +05:30
MochaMind
8f7df1627a chore: Indonesian translations 2026-02-25 22:58:49 +05:30
MochaMind
dfd08b6c85 chore: Portuguese, Brazilian translations 2026-02-25 22:58:48 +05:30
MochaMind
5a63a92b5c chore: Vietnamese translations 2026-02-25 22:58:46 +05:30
MochaMind
944b9f7b19 chore: Chinese Simplified translations 2026-02-25 22:58:44 +05:30
MochaMind
a9bbcfad00 chore: Turkish translations 2026-02-25 22:58:43 +05:30
MochaMind
e609efad07 chore: Swedish translations 2026-02-25 22:58:41 +05:30
MochaMind
f441217c31 chore: Serbian (Cyrillic) translations 2026-02-25 22:58:39 +05:30
MochaMind
bcd6c0a3a0 chore: Slovenian translations 2026-02-25 22:58:38 +05:30
MochaMind
4f664db460 chore: Russian translations 2026-02-25 22:58:36 +05:30
MochaMind
fd2dc00e21 chore: Portuguese translations 2026-02-25 22:58:35 +05:30
MochaMind
18f3d81410 chore: Polish translations 2026-02-25 22:58:33 +05:30
MochaMind
ec934009ad chore: Dutch translations 2026-02-25 22:58:32 +05:30
MochaMind
bf64aa02ee chore: Italian translations 2026-02-25 22:58:30 +05:30
MochaMind
0ae8afb535 chore: Hungarian translations 2026-02-25 22:58:28 +05:30
MochaMind
a33bc988b7 chore: German translations 2026-02-25 22:58:27 +05:30
MochaMind
e2ccef3334 chore: Danish translations 2026-02-25 22:58:25 +05:30
MochaMind
95226cd273 chore: Czech translations 2026-02-25 22:58:24 +05:30
MochaMind
99d6710e0b chore: Arabic translations 2026-02-25 22:58:22 +05:30
MochaMind
c189521c0a chore: Spanish translations 2026-02-25 22:58:21 +05:30
RitvikSardana
91eabfa87a Merge pull request #3057 from RitvikSardana/analytics
feat: telemetry with pulse
2026-02-25 16:08:28 +05:30
Ritvik Sardana
b6ade86b5b chore: bump version 2026-02-25 16:02:51 +05:30
Ritvik Sardana
fbc2c66d08 fix: better placeholder if no user is found 2026-02-25 16:02:21 +05:30
Sydney
f7fbcce42e chore: Revert chore: Delete desk/src/types/doctypes.ts
This reverts commit 90e2a7bd3f.
2026-02-25 15:53:09 +05:30
Sydney Gomes
90e2a7bd3f chore: Delete desk/src/types/doctypes.ts 2026-02-25 15:50:04 +05:30
Sydney
0928581960 chore: code cleanup 2026-02-25 15:37:27 +05:30
Ritvik Sardana
334345f73f fix: dont capture events for pre-filled data 2026-02-25 11:58:09 +05:30
Ritvik Sardana
a6a88791f2 fix: dont index hd_ticket twice 2026-02-25 11:10:42 +05:30
RitvikSardana
3e56bcb804 Merge pull request #3050 from aerodeval/fix/saved-replies-bug
fix: email send button bug and null value appearing in saved replies
2026-02-25 11:09:00 +05:30
RitvikSardana
93ae4cde40 Merge pull request #3056 from RitvikSardana/filter-select
fix(filters): model-value for select component
2026-02-25 11:03:22 +05:30
Sydney
5f03f0d144 fix: fix flaky mentions rendering when agent list not populated 2026-02-25 02:17:46 +05:30
Sydney
f6a7c1868f fix: broken replies section and reply content bug 2026-02-25 01:59:00 +05:30
Sydney
bb8ef76156 chore: cleanup redundant code 2026-02-24 21:00:12 +05:30
Ritvik Sardana
7c80701fe2 chore: add site data for analytics 2026-02-24 19:49:21 +05:30
Ritvik Sardana
17fa75759a fix: add event for hd_team 2026-02-24 19:48:56 +05:30
Ritvik Sardana
7f07d6bfca feat: telemetry with pulse 2026-02-24 19:42:23 +05:30
Sydney
1bedf1eab6 Merge branch 'fix/email-formatting-bug' of https://github.com/aerodeval/helpdesk into fix/email-formatting-bug 2026-02-24 17:25:38 +05:30
Sydney
0e0c6dd8ce chore: improve utils definition and semantic 2026-02-24 17:24:40 +05:30
Sydney Gomes
9d467be009 chore: delete desk/src/types/doctypes.ts 2026-02-24 16:32:46 +05:30
Ritvik Sardana
d917e42dcb chore: code cleanup 2026-02-24 16:32:02 +05:30
Ritvik Sardana
d3aeefb327 fix(filters): model-value for select component 2026-02-24 15:18:04 +05:30
RitvikSardana
4d87285876 Merge pull request #3047 from frappe/pot_develop_2026-02-22
chore: update POT file
2026-02-24 10:22:16 +05:30
RitvikSardana
42f527b006 Merge pull request #3046 from frappe/l10n_develop
chore: sync translations from crowdin
2026-02-24 10:21:58 +05:30
RitvikSardana
c75e564391 Merge pull request #3051 from RitvikSardana/disable-ticket-type
fix: add disabled field in HD Ticket Type
2026-02-24 09:31:12 +05:30
Ritvik Sardana
c00ab76168 fix: add disabled field in HD Ticket Type 2026-02-24 09:10:43 +05:30
MochaMind
fcd775ba0b chore: Swedish translations 2026-02-23 22:11:54 +05:30
Sydney
0ba855e8c1 chore: add suport for tabular data content breaking 2026-02-23 18:08:57 +05:30
Sydney
4262a7b8aa chore: code cleanup 2026-02-23 17:56:11 +05:30
Sydney
b107ca3c25 Revert "chore: code cleanup"
This reverts commit f60a6f9fc7.
2026-02-23 16:04:11 +05:30
Sydney
f60a6f9fc7 chore: code cleanup 2026-02-23 14:57:17 +05:30
Sydney
ee3f456009 fix: email send button bug where null value allowed send button to be active 2026-02-23 12:23:37 +05:30
RitvikSardana
0cc334d2de Merge pull request #3048 from RitvikSardana/sla-resolution-time
fix: check for months and years while formatting date
2026-02-23 12:01:29 +05:30
Ritvik Sardana
69504c0ec4 fix: check for months and years while formatting date 2026-02-23 11:57:02 +05:30
frappe-pr-bot
fcea5fd3ae chore: update POT file 2026-02-22 10:40:48 +00:00
Sydney
0bb463acc2 fix: add table extension for tiptap and apply css using that and fix excel pasting 2026-02-21 02:15:11 +05:30
Sydney
c3ec7929b9 fix: adding some preventive css for borders appearing everywhere and fixing extension 2026-02-21 01:00:54 +05:30
MochaMind
b64e56e40c chore: Thai translations 2026-02-20 21:30:49 +05:30
MochaMind
1e33702c79 chore: Vietnamese translations 2026-02-20 21:30:43 +05:30
MochaMind
15f86f2026 chore: Swedish translations 2026-02-20 21:30:37 +05:30
MochaMind
3cd5fcbbe5 chore: Serbian (Cyrillic) translations 2026-02-20 21:30:35 +05:30
MochaMind
6c588ead84 chore: Dutch translations 2026-02-20 21:30:27 +05:30
MochaMind
7a8716b465 chore: Spanish translations 2026-02-20 21:30:15 +05:30
Sydney
3cf6330845 fix: remove enforced css causing tables to break 2026-02-20 10:22:50 -05:00
Sydney
8d59c75e6a chore: add fix for email editor 2026-02-20 10:20:51 -05:00
Sydney
efe9448944 fix: email formatting bug and excel bug fix where image is pasted instead of excel 2026-02-20 10:18:22 -05:00
Sydney
b9b82b48cd chore: remove unecessary storeToRef 2026-02-20 09:59:42 -05:00
Sydney
13e5a14867 chore: remove redundant code 2026-02-20 09:45:14 -05:00
Sydney
d2ea5904b5 fix: fix localization all over helpdesk and fix config fetching and setting 2026-02-20 06:38:53 -05:00
Sydney
bfa2dcc2a3 chore: code cleanup 2026-02-19 20:16:27 +05:30
RitvikSardana
82d2e495d7 Merge pull request #3041 from RitvikSardana/vb
chore: bump version
2026-02-19 18:34:25 +05:30
Ritvik Sardana
abc5058b0b chore: bump version 2026-02-19 18:26:27 +05:30
RitvikSardana
65cc377e17 Merge pull request #3004 from frappe/l10n_develop
chore: sync translations from crowdin
2026-02-19 14:12:53 +05:30
MochaMind
1b4fe28b01 chore: Serbian (Latin) translations 2026-02-18 20:27:26 +05:30
MochaMind
fa2c98cb0d chore: Persian translations 2026-02-18 20:27:25 +05:30
MochaMind
ed91deca37 chore: Spanish translations 2026-02-18 20:27:23 +05:30
Sydney
e6f13d4cf3 fix: use prettyDate function to handle edge cases 2026-02-18 10:51:27 +05:30
RitvikSardana
8eb7859a47 Merge pull request #3024 from RitvikSardana/api-type-check2
fix: type check for ticket doctype with int | str
2026-02-17 21:24:14 +05:30
Ritvik Sardana
edc3d29d74 fix: type check for ticket doctype with int | str 2026-02-17 21:23:50 +05:30
RitvikSardana
07550539ef Merge pull request #2623 from MostafaKadry/fix/stop_auto_message_from_triggering_on_reopen
fix: ignore automated system notifications in ticket communication from automatically reopens the ticket.
2026-02-17 20:06:40 +05:30
RitvikSardana
f2ea2e60fc Merge pull request #3000 from RitvikSardana/socket-cp
fix: socket and saved reply
2026-02-17 20:06:10 +05:30
MochaMind
5468734b33 chore: Esperanto translations 2026-02-17 19:14:52 +05:30
MochaMind
6ea82815fd chore: Serbian (Latin) translations 2026-02-17 19:14:51 +05:30
MochaMind
0872507484 chore: Norwegian Bokmal translations 2026-02-17 19:14:49 +05:30
MochaMind
c23c23f542 chore: Bosnian translations 2026-02-17 19:14:48 +05:30
MochaMind
4144fb67a2 chore: Burmese translations 2026-02-17 19:14:46 +05:30
MochaMind
65921275f6 chore: Croatian translations 2026-02-17 19:14:44 +05:30
MochaMind
a94a3b9244 chore: Thai translations 2026-02-17 19:14:43 +05:30
MochaMind
951bd54aae chore: Persian translations 2026-02-17 19:14:41 +05:30
MochaMind
bef6f0ce92 chore: Indonesian translations 2026-02-17 19:14:40 +05:30
MochaMind
884e7eab6c chore: Portuguese, Brazilian translations 2026-02-17 19:14:38 +05:30
MochaMind
ec49fa7546 chore: Vietnamese translations 2026-02-17 19:14:37 +05:30
MochaMind
fb75199f52 chore: Chinese Simplified translations 2026-02-17 19:14:35 +05:30
MochaMind
123abdce36 chore: Turkish translations 2026-02-17 19:14:33 +05:30
MochaMind
6c27f5b9d5 chore: Swedish translations 2026-02-17 19:14:32 +05:30
MochaMind
e609c19d96 chore: Serbian (Cyrillic) translations 2026-02-17 19:14:30 +05:30
MochaMind
ec63f41e73 chore: Slovenian translations 2026-02-17 19:14:29 +05:30
MochaMind
1da2f63f16 chore: Russian translations 2026-02-17 19:14:27 +05:30
RitvikSardana
47e3f06e94 Merge pull request #3020 from pratikb64/fix/saved-reply-admin-perm
fix: filter personal replies from list
2026-02-17 18:04:34 +05:30
Ritvik Sardana
60584fcd4d fix: revert CP changes 2026-02-17 17:44:08 +05:30
Ritvik Sardana
a096e1b19f fix: order_by in Ticket Summary report 2026-02-17 17:28:55 +05:30
Pratik
576432d380 fix: saved reply admin test 2026-02-17 17:23:30 +05:30
Pratik
c13aabdbdc fix: filter personal replies from list 2026-02-17 17:10:21 +05:30
Ankush Menat
d6be9938ad chore: project URLs (#3018) 2026-02-17 15:50:24 +05:30
Ritvik Sardana
d6d4c4dfe9 fix: dont send notification if agent is same as session.user 2026-02-17 13:51:18 +05:30
Ritvik Sardana
adb3ee3b0d fix: redisearch syntax error 2026-02-17 13:51:17 +05:30
Ritvik Sardana
93bc04605e fix: redisearch syntax error 2026-02-17 13:51:17 +05:30
Ritvik Sardana
29ad7da211 fix: CP styles 2026-02-17 13:51:17 +05:30
Ritvik Sardana
ea60812f81 fix: append saved reply to email content instead of replacing it 2026-02-17 13:51:17 +05:30
Ritvik Sardana
69e047f69e fix: socket and saved reply 2026-02-17 13:51:17 +05:30
RitvikSardana
f9ce80da1d Merge pull request #3016 from RitvikSardana/api-type-check
fix: add type hints for all API endpoints
2026-02-17 13:46:45 +05:30
Ritvik Sardana
bfd4cb6a9b fix: add hook for type checking 2026-02-17 13:27:22 +05:30
MochaMind
2084349da9 chore: Portuguese translations 2026-02-16 19:14:04 +05:30
MochaMind
b7a30c80be chore: Polish translations 2026-02-16 19:14:02 +05:30
MochaMind
9e4505a18e chore: Dutch translations 2026-02-16 19:14:01 +05:30
MochaMind
138825d6b4 chore: Italian translations 2026-02-16 19:13:59 +05:30
MochaMind
1ddb560979 chore: Hungarian translations 2026-02-16 19:13:58 +05:30
MochaMind
ae5990e1dc chore: German translations 2026-02-16 19:13:56 +05:30
MochaMind
6c540b917f chore: Danish translations 2026-02-16 19:13:55 +05:30
MochaMind
f4266ebaa0 chore: Czech translations 2026-02-16 19:13:53 +05:30
MochaMind
c211f817ae chore: Arabic translations 2026-02-16 19:13:52 +05:30
MochaMind
d8db4a0e6b chore: Spanish translations 2026-02-16 19:13:51 +05:30
MochaMind
015a800d90 chore: French translations 2026-02-16 19:13:49 +05:30
Ritvik Sardana
02b1b0473c fix: add type hints for all API endpoints 2026-02-16 19:13:13 +05:30
RitvikSardana
385a88fad3 Merge pull request #3012 from frappe/pot_develop_2026-02-15
chore: update POT file
2026-02-16 15:52:31 +05:30
frappe-pr-bot
3c4eee38cd chore: update POT file 2026-02-15 10:41:34 +00:00
MochaMind
9c87f0d089 chore: Persian translations 2026-02-14 18:56:19 +05:30
Sydney
d46429165f chore: add dayjs local for support in different timezones 2026-02-13 00:52:20 +05:30
MochaMind
40d72b6af2 chore: Serbian (Latin) translations 2026-02-12 17:21:15 +05:30
MochaMind
228fa9ff8d chore: Serbian (Cyrillic) translations 2026-02-12 17:21:13 +05:30
MochaMind
fb802a0ea7 chore: Russian translations 2026-02-12 17:21:11 +05:30
MochaMind
0541976cea chore: Spanish translations 2026-02-12 17:21:09 +05:30
Sydney
f8311a98c7 chore: remove debugging print statements 2026-02-12 16:17:39 +05:30
Sydney
bdda40443c chore: added test case to check if multiple notifications are not sent on adding mention when edited 2026-02-12 16:02:25 +05:30
MochaMind
8b182a2201 chore: Serbian (Latin) translations 2026-02-11 17:25:27 +05:30
MochaMind
03cc38c06f chore: Burmese translations 2026-02-11 17:25:25 +05:30
MochaMind
164f8e3c91 chore: Bosnian translations 2026-02-10 17:21:24 +05:30
MochaMind
e1024b017a chore: Croatian translations 2026-02-10 17:21:22 +05:30
MochaMind
4b2c340d84 chore: Swedish translations 2026-02-10 17:21:14 +05:30
MochaMind
27b8b014ea chore: Italian translations 2026-02-10 17:21:06 +05:30
Sydney
fd94e0bc12 fix: comment box edit mentions fix 2026-02-10 01:06:29 +05:30
RitvikSardana
d7633c8bca Merge pull request #2850 from artykbasar/feat/email-domain-desk-links-clean
feat(settings): select Email Domain for custom email accounts
2026-02-09 17:02:15 +05:30
Ritvik Sardana
d8117201d0 fix: api in email type 2026-02-09 16:56:15 +05:30
Ritvik Sardana
816a347d4c chore: code cleanup 2026-02-09 16:42:37 +05:30
Ritvik Sardana
1b4b0a3739 fix: remove service field in custom email account 2026-02-09 16:31:03 +05:30
RitvikSardana
138f1f0b55 Merge pull request #2999 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-02-09 15:51:42 +05:30
Jannat Patel
efb274ecbd chore: Swedish translations 2026-02-09 15:35:56 +05:30
RitvikSardana
e9c650f38a Merge pull request #2990 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-02-09 15:28:30 +05:30
RitvikSardana
1ef1171742 Merge pull request #2811 from samarth93/fix/datepicker-custom-fields-2669-develop
fix: add DatePicker support for custom Date/Datetime fields in UniInput
2026-02-09 15:21:25 +05:30
Ritvik Sardana
8e64cf9cca fix: add support in ticket creation form 2026-02-09 15:15:09 +05:30
Ritvik Sardana
df7deec952 fix: add date/datetime support in agent and customer ticket view 2026-02-09 15:06:45 +05:30
samarth93
5e3173ac5a fix: prettier formatting 2026-02-09 14:57:16 +05:30
samarth93
d42af58d88 fix: add DatePicker support for custom Date/Datetime fields in UniInput
Fixes #2669

Custom Date and Datetime fields on the new ticket form were rendering
as plain text inputs instead of proper DatePicker/DateTimePicker
components.

Changes:
- Import DatePicker and DateTimePicker from frappe-ui
- Add handling for 'Date' fieldtype to render DatePicker component
- Add handling for 'Datetime' fieldtype to render DateTimePicker component
- Add date formatting in transValue for proper display
2026-02-09 14:57:16 +05:30
Jannat Patel
8bb50ac347 chore: Esperanto translations 2026-02-09 13:03:15 +05:30
Jannat Patel
b858fdcf54 chore: Serbian (Latin) translations 2026-02-09 13:03:14 +05:30
Jannat Patel
20615dc619 chore: Norwegian Bokmal translations 2026-02-09 13:03:13 +05:30
Jannat Patel
cbd313d18d chore: Bosnian translations 2026-02-09 13:03:12 +05:30
Jannat Patel
7f8135f1ba chore: Burmese translations 2026-02-09 13:03:10 +05:30
Jannat Patel
76915db34a chore: Croatian translations 2026-02-09 13:03:09 +05:30
Jannat Patel
29efce94ed chore: Thai translations 2026-02-09 13:03:08 +05:30
Jannat Patel
e9fafea0ac chore: Persian translations 2026-02-09 13:03:07 +05:30
Jannat Patel
fd7751756f chore: Indonesian translations 2026-02-09 13:03:05 +05:30
Jannat Patel
d04cea4724 chore: Portuguese, Brazilian translations 2026-02-09 13:03:04 +05:30
Jannat Patel
46f5c903e4 chore: Vietnamese translations 2026-02-09 13:03:03 +05:30
Jannat Patel
f94bafd44f chore: Chinese Simplified translations 2026-02-09 13:03:01 +05:30
Jannat Patel
afe1a18779 chore: Turkish translations 2026-02-09 13:03:00 +05:30
Jannat Patel
1fc57f4325 chore: Swedish translations 2026-02-09 13:02:59 +05:30
Jannat Patel
fdc3c610aa chore: Serbian (Cyrillic) translations 2026-02-09 13:02:58 +05:30
Jannat Patel
2eef45125e chore: Slovenian translations 2026-02-09 13:02:57 +05:30
Jannat Patel
7c62431e4c chore: Russian translations 2026-02-09 13:02:55 +05:30
Jannat Patel
d26d2c90f5 chore: Portuguese translations 2026-02-09 13:02:54 +05:30
Jannat Patel
e835e58695 chore: Polish translations 2026-02-09 13:02:53 +05:30
Jannat Patel
e3c3967465 chore: Dutch translations 2026-02-09 13:02:51 +05:30
Jannat Patel
680053d1a3 chore: Italian translations 2026-02-09 13:02:50 +05:30
Jannat Patel
80052ed221 chore: Hungarian translations 2026-02-09 13:02:49 +05:30
Jannat Patel
c37250eb06 chore: German translations 2026-02-09 13:02:47 +05:30
Jannat Patel
042d496497 chore: Danish translations 2026-02-09 13:02:46 +05:30
Jannat Patel
2c1a9775e1 chore: Czech translations 2026-02-09 13:02:45 +05:30
Jannat Patel
87b3a32814 chore: Arabic translations 2026-02-09 13:02:44 +05:30
Jannat Patel
61d4738a84 chore: Spanish translations 2026-02-09 13:02:42 +05:30
Jannat Patel
b266ee5072 chore: French translations 2026-02-09 13:02:41 +05:30
RitvikSardana
1b9cd0e04f Merge pull request #2996 from RitvikSardana/auth-fix
fix: add login endpoints in auth
2026-02-09 12:10:11 +05:30
Ritvik Sardana
8478892383 fix: add login endpoints in auth 2026-02-09 12:08:07 +05:30
RitvikSardana
fc5d6fae63 Merge pull request #2991 from frappe/pot_develop_2026-02-08
chore: update POT file
2026-02-09 11:31:15 +05:30
Sydney
ff0c25d49f fix: enforce white background on mail content for table data 2026-02-09 01:32:58 +05:30
artykbasar
d6274a0e52 Merge branch 'develop' into feat/email-domain-desk-links-clean 2026-02-08 17:19:51 +00:00
frappe-pr-bot
c47a44096c chore: update POT file 2026-02-08 10:41:56 +00:00
Jannat Patel
9cebf53ea4 chore: Italian translations 2026-02-08 12:46:58 +05:30
RitvikSardana
46e7f2354e Merge pull request #2971 from pratikb64/fix/saved-reply-bug
fix: save bug in Saved Replies
2026-02-07 19:47:22 +05:30
RitvikSardana
97d4737efb chore: bump version 2026-02-07 19:39:48 +05:30
RitvikSardana
a6e96c7066 Merge pull request #2973 from aerodeval/fix/reply-all-issue
fix: added support for mail with display names and latin characters
2026-02-06 14:35:38 +05:30
RitvikSardana
0d34ff530b Merge pull request #2977 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-02-06 14:33:28 +05:30
RitvikSardana
e2e6c44097 Merge pull request #2972 from aerodeval/fix/disable-ticket-image-captions
fix: hide captions in text editor
2026-02-06 14:13:42 +05:30
RitvikSardana
750beb343f Merge pull request #2981 from RitvikSardana/spri-fixes-1
fix: sprint fixes-1
2026-02-06 14:13:15 +05:30
Ritvik Sardana
4c73593c8a chore: add type hints to api 2026-02-06 12:54:34 +05:30
Ritvik Sardana
4c594535a6 fix: persist count in LV 2026-02-06 12:12:25 +05:30
Sydney
64f94848ea chore: disable image captions all over 2026-02-05 16:13:41 +05:30
Sydney
3b5b2ce9c6 fix: add deboubnce and use zod validation for email 2026-02-05 15:37:30 +05:30
Sydney
d00431f51c fix: fixed mail backspace bug and added support for multiinput for normalization 2026-02-05 15:25:34 +05:30
Jannat Patel
e6c267338c chore: Persian translations 2026-02-05 12:13:13 +05:30
Jannat Patel
85a0644593 chore: Portuguese, Brazilian translations 2026-02-05 12:13:11 +05:30
Jannat Patel
a91f4867c4 chore: Swedish translations 2026-02-05 12:13:08 +05:30
Jannat Patel
420ef47b66 chore: Dutch translations 2026-02-05 12:13:03 +05:30
Jannat Patel
cc665c838f chore: Italian translations 2026-02-05 12:13:02 +05:30
Jannat Patel
22901fd402 chore: Spanish translations 2026-02-05 12:12:57 +05:30
RitvikSardana
0b95e687f7 Merge pull request #2956 from frappe/setup-fix
chore(setup): Add build info in pyproject
2026-02-05 11:29:13 +05:30
Ritvik Sardana
5a4f152d52 fix: add seen functionality in list view 2026-02-05 10:57:07 +05:30
Sydney
f1637135a8 chore: add colspan and rowspan attributes 2026-02-04 23:58:43 +05:30
Sydney
4999195a14 fix: add additional table attributes for emailer content support 2026-02-04 22:06:03 +05:30
Sydney
3d25f4b624 fix: add support for table tag attributes while sanitizing 2026-02-04 22:04:04 +05:30
Sydney
df37362516 fix: email formatting issue due to hardcoded table css 2026-02-04 21:44:18 +05:30
Ritvik Sardana
a7135bc17b fix(search): prepare doc if field exists 2026-02-04 00:26:26 +05:30
Ritvik Sardana
e92963357f fix: persist rows count in list view 2026-02-04 00:25:05 +05:30
Sydney
257c12e79f fix: add regex support for latin characters and input support for MultiInput for mail ids 2026-02-03 22:27:20 +05:30
Sydney
233b935393 fix: add support for display names in mail 2026-02-03 15:21:51 +05:30
Sydney
23571113f6 fix: disable input for image captions in textinput 2026-02-03 13:47:09 +05:30
Pratik
cf0bcb1897 Merge remote-tracking branch 'upstream/develop' into fix/saved-reply-bug 2026-02-03 13:06:07 +05:30
RitvikSardana
6ca100fd15 Merge pull request #2970 from RitvikSardana/sag-fix
fix: add type hints
2026-02-03 12:30:21 +05:30
Pratik
ebf145f432 fix: save bug 2026-02-03 12:29:36 +05:30
Ritvik Sardana
f9240bc248 fix: add type hints 2026-02-03 10:55:36 +05:30
RitvikSardana
71840b9aa5 Merge pull request #2939 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-02-02 21:44:39 +05:30
RitvikSardana
7fc9a611d0 Merge pull request #2960 from RitvikSardana/security-fix
fix(security): remove roles and unwanted files
2026-02-02 21:03:29 +05:30
Pratik
69c8626f80 fix: use standard api for fetching agent 2026-02-02 20:52:05 +05:30
Pratik
b3b6ac9a04 fix: security issues 2026-02-02 20:52:05 +05:30
Ritvik Sardana
b219aefb20 fix: version bump 2026-02-02 20:52:05 +05:30
Ritvik Sardana
d150c290fb fix: version bump 2026-02-02 20:52:05 +05:30
Ritvik Sardana
81810afdf8 fix: add signup auth and redirect route perm 2026-02-02 20:52:05 +05:30
RitvikSardana
783ee55658 Merge pull request #2966 from pratikb64/fix/security-fixes
fix: security issues
2026-02-02 20:47:34 +05:30
Ritvik Sardana
d2dbddd5a4 fix(test): update test cases 2026-02-02 20:18:31 +05:30
Ritvik Sardana
e4ee29f95e fix: add perm checks for API endpoints 2026-02-02 17:59:28 +05:30
Ritvik Sardana
0da0b73dc8 fix: select perms instead of read perm for correct doctypes 2026-02-02 17:34:09 +05:30
Pratik
064110d940 fix: use standard api for fetching agent 2026-02-02 17:29:22 +05:30
Aradhya-Tripathi
a76ff09d6e chore(build): Setup out dir and index from project root 2026-02-02 15:55:15 +05:30
Pratik
348884a912 fix: security issues 2026-02-02 15:35:24 +05:30
RitvikSardana
07ef28dbca Merge pull request #2961 from RitvikSardana/imm-fix
fix: add signup auth and redirect route perm
2026-02-02 13:34:41 +05:30
Ritvik Sardana
a3116603c7 fix: version bump 2026-02-02 13:29:39 +05:30
Ritvik Sardana
1ce95fcab5 fix: version bump 2026-02-02 13:29:05 +05:30
Ritvik Sardana
b625a95e22 fix: add signup auth and redirect route perm 2026-02-02 13:21:48 +05:30
Ritvik Sardana
b45a658b01 fix(security): remove roles and unwanted files 2026-02-02 13:10:03 +05:30
RitvikSardana
0376a3626d Merge pull request #2904 from earona/patch-1
ci: fix generate POT workflow
2026-02-02 09:34:14 +05:30
Aradhya-Tripathi
ce4c738898 chore(setup): Add build info in pyproject 2026-01-30 14:39:37 +05:30
Pratik
fd4d53b35e fix: avg time metrics months 2026-01-30 13:08:29 +05:30
Pratik
de9754904a fix: CI errors 2026-01-30 11:59:26 +05:30
Pratik
7cc1b46903 test: remove unnecessary test 2026-01-29 18:04:26 +05:30
Pratik
70d6fb4e15 fix: HD View is_standard edit permission 2026-01-29 18:01:11 +05:30
Pratik
174377e0fe fix: hd view is_standard form edit 2026-01-29 16:46:13 +05:30
Pratik
fb78980e56 fix: pending tickets placehoder 2026-01-29 16:09:22 +05:30
Pratik
fd5276eb0b fix: fixtures 2026-01-29 15:44:58 +05:30
Pratik
a56f24a75d fix: code cleanup 2026-01-29 15:20:42 +05:30
Pratik
51d3af5dd2 feat: refactor pending tickets component to table layout & add SLA urgency coloring 2026-01-29 14:32:20 +05:30
Sydney Gomes
43038be819 Merge pull request #2954 from aerodeval/feat/uniInput-missing-translation
Feat/uni input missing translation
2026-01-29 12:58:42 +05:30
Sydney Gomes
a4f7a0013e Merge branch 'frappe:develop' into feat/uniInput-missing-translation 2026-01-29 12:12:03 +05:30
Sydney
3396306457 fix: add translation strings for uniInput 2026-01-29 12:10:43 +05:30
Pratik
989f9cd9e6 feat: add fixtures & handle redirects 2026-01-28 22:57:18 +05:30
Shariq Ansari
5e1bb213c1 Merge branch 'develop' into l10n_develop2 2026-01-28 21:29:41 +05:30
Shariq Ansari
33785829c1 Merge pull request #2951 from aerodeval/fix/posthog-issue 2026-01-28 20:53:55 +05:30
Sydney
82d3751f80 fix: posthog optional chaining added to fix close button bug 2026-01-28 20:45:55 +05:30
Pratik
c3d7d3baae Merge remote-tracking branch 'upstream/develop' into feat/agent-landing-page 2026-01-28 15:25:43 +05:30
Pratik
e87f82cf21 feat: handle __assigned_on filter 2026-01-28 15:23:34 +05:30
Pratik
9c6d8be648 feat: add is_standard field 2026-01-27 23:49:18 +05:30
artykbasar
c3d7ba0255 Merge branch 'frappe:develop' into feat/email-domain-desk-links-clean 2026-01-26 08:52:30 +00:00
RitvikSardana
614d2855ea Merge pull request #2943 from RitvikSardana/auth-paths
fix: add OAuth2 paths
2026-01-26 13:15:58 +05:30
Ritvik Sardana
a94013e8d6 fix: add OAuth2 paths 2026-01-26 13:14:33 +05:30
Sydney Gomes
7d2cfdf401 Merge pull request #2902 from aerodeval/feat/after-hours-banner
feat: display a dismissible banner if ticket is raised outside working hours
2026-01-23 22:06:58 +05:30
Sydney Gomes
acd43e2657 Merge branch 'develop' into feat/after-hours-banner 2026-01-23 21:56:57 +05:30
Sydney
c8193ed6bc fix: compare wrt to date and time for next day 2026-01-23 20:28:24 +05:30
Sydney
78812bb9c4 refactor: rename field for better understanding and code cleanup 2026-01-23 18:44:52 +05:30
RitvikSardana
b1be315b12 Merge pull request #2941 from RitvikSardana/auth-custom-login2
fix: custom login auth path
2026-01-23 17:50:16 +05:30
Ritvik Sardana
2dc26b378d fix: custom login auth path 2026-01-23 17:45:13 +05:30
RitvikSardana
27d685e77c Merge pull request #2940 from frappe/revert-2938-auth-custom-login
Revert "fix: allow custom login api"
2026-01-23 17:42:27 +05:30
RitvikSardana
710f567c40 Revert "fix: allow custom login api" 2026-01-23 17:42:01 +05:30
RitvikSardana
29ca9701c2 chore: Arabic translations 2026-01-23 17:38:17 +05:30
RitvikSardana
09552d7e90 chore: Dutch translations 2026-01-23 17:38:12 +05:30
RitvikSardana
3f8107db49 Merge pull request #2938 from RitvikSardana/auth-custom-login
fix: allow custom login api
2026-01-23 17:30:05 +05:30
Ritvik Sardana
73332cd4f0 fix: allow custom login api 2026-01-23 17:28:59 +05:30
RitvikSardana
2445eacac5 Merge pull request #2933 from RitvikSardana/allowed-endpoints
fix(security): limit auth hooks
2026-01-23 17:00:14 +05:30
RitvikSardana
15f5d969c4 chore: version bump 2026-01-23 16:57:15 +05:30
RitvikSardana
32dc6ef93d Merge pull request #2934 from RitvikSardana/fix-fail-install
fix: fail install
2026-01-23 16:48:44 +05:30
Ritvik Sardana
402dd042ae chore: code cleanup 2026-01-23 16:44:32 +05:30
Ritvik Sardana
012b6b91ea fix: add agent_name field while installing 2026-01-23 16:43:56 +05:30
Ritvik Sardana
a7596090c4 fix(security): limit auth hooks 2026-01-23 16:29:49 +05:30
RitvikSardana
c1a1939ade Merge pull request #2932 from RitvikSardana/fix-patch-agent-customer
fix(patch): ticket id not found
2026-01-23 15:43:42 +05:30
Ritvik Sardana
73604e7f11 fix(patch): ticket id not found 2026-01-23 15:42:32 +05:30
Sydney
a7cba78972 refactor: fix test cases 2026-01-23 15:14:10 +05:30
RitvikSardana
30f2a99592 Merge pull request #2900 from RitvikSardana/fix-last-reply
fix: add fields for last customer and agent response
2026-01-23 14:15:21 +05:30
RitvikSardana
53a93d3e54 Merge branch 'develop' into fix-last-reply 2026-01-23 14:04:46 +05:30
Sydney
b30c71462c refactor: refactor code changes 2026-01-23 13:37:58 +05:30
RitvikSardana
dfc69c1d41 chore: version bump 2026-01-23 12:45:04 +05:30
RitvikSardana
84b9d42799 Merge pull request #2930 from RitvikSardana/notification-seen
fix: notification styles and order by
2026-01-23 12:38:14 +05:30
Ritvik Sardana
c96b00db75 fix: notification styles and order by 2026-01-23 12:36:45 +05:30
Sydney
21115455b0 fix: fix logic for day calculation 2026-01-23 01:17:41 +05:30
Sydney
d00e2b6dbf chore: code cleanup for early exit 2026-01-22 23:46:15 +05:30
RitvikSardana
3431805b73 Merge pull request #2922 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-01-22 23:41:02 +05:30
Sydney
2f787394b0 fix: handle issue where ticket is being shown after next day 2026-01-22 23:08:22 +05:30
RitvikSardana
e4372f72cf chore: Bosnian translations 2026-01-22 17:31:54 +05:30
RitvikSardana
aa70598d7a chore: Swedish translations 2026-01-22 17:31:50 +05:30
RitvikSardana
2c4ec37fb1 chore: Dutch translations 2026-01-22 17:31:49 +05:30
Sydney
bea7b1bb98 chore: add documentation link for the settings 2026-01-22 17:16:00 +05:30
RitvikSardana
e17fda5e2a Merge pull request #2923 from pratikb64/fix/saved-reply-patch
fix(patch): rename canned response
2026-01-22 15:44:25 +05:30
Sydney
f34377d8a9 chore: code cleanup 2026-01-22 15:20:20 +05:30
Sydney
af281d4277 chore: fix ui inconsistency 2026-01-22 15:17:46 +05:30
Pratik
1888f24f46 fix: rename canned response patch 2026-01-22 13:07:46 +05:30
Sydney
e29792803b chore: make changes in banner for better consistency 2026-01-22 12:44:15 +05:30
RitvikSardana
774b9073a1 chore: Norwegian Bokmal translations 2026-01-21 17:10:22 +05:30
RitvikSardana
41fd854f2d chore: Bosnian translations 2026-01-21 17:10:20 +05:30
RitvikSardana
78282416d1 chore: Croatian translations 2026-01-21 17:10:17 +05:30
RitvikSardana
3da40a1fed chore: Portuguese, Brazilian translations 2026-01-21 17:10:15 +05:30
RitvikSardana
c968fdf0f3 chore: Vietnamese translations 2026-01-21 17:10:14 +05:30
RitvikSardana
52c01e93e9 chore: Chinese Simplified translations 2026-01-21 17:10:13 +05:30
RitvikSardana
4b7a065bae chore: Turkish translations 2026-01-21 17:10:12 +05:30
RitvikSardana
7932b40420 chore: Swedish translations 2026-01-21 17:10:11 +05:30
RitvikSardana
66b50e3799 chore: Slovenian translations 2026-01-21 17:10:08 +05:30
RitvikSardana
71780d5e04 chore: Portuguese translations 2026-01-21 17:10:07 +05:30
RitvikSardana
45bf04d592 chore: Polish translations 2026-01-21 17:10:05 +05:30
RitvikSardana
6f161fd717 chore: Italian translations 2026-01-21 17:10:04 +05:30
RitvikSardana
0ca91888d2 chore: German translations 2026-01-21 17:10:03 +05:30
RitvikSardana
c18eb9f361 chore: Arabic translations 2026-01-21 17:10:00 +05:30
RitvikSardana
f9b2b423af chore: Spanish translations 2026-01-21 17:09:58 +05:30
RitvikSardana
57d00bb726 chore: French translations 2026-01-21 17:09:57 +05:30
RitvikSardana
e60cedd170 chore: Indonesian translations 2026-01-21 17:09:56 +05:30
RitvikSardana
1dbec24253 chore: Russian translations 2026-01-21 17:09:55 +05:30
RitvikSardana
dc1bb0596a chore: Persian translations 2026-01-21 17:09:53 +05:30
RitvikSardana
cc9a1638f6 chore: Dutch translations 2026-01-21 17:09:52 +05:30
RitvikSardana
9447c8ccd4 chore: Hungarian translations 2026-01-21 17:09:51 +05:30
Sydney
a37ca84c28 refactor: change variable name and fix ui 2026-01-21 11:38:01 +05:30
Ritvik Sardana
8ff1ffa54e chore: code cleanup 2026-01-21 01:02:43 +05:30
Ritvik Sardana
b3204cc1c4 fix(settings): outside hours title and desc 2026-01-21 01:00:31 +05:30
Ritvik Sardana
447fa003ff chore: update frappe-ui 2026-01-21 00:47:33 +05:30
RitvikSardana
6a8407aca8 Merge branch 'develop' into feat/after-hours-banner 2026-01-21 00:41:29 +05:30
Ritvik Sardana
c4a00bc6df refactor: api functions and UI 2026-01-21 00:31:06 +05:30
Sydney
0a728d17cb fix: fixing reset button issue for banner message 2026-01-20 22:51:46 +05:30
Sydney
c8e01f42cd chore: running precommits 2026-01-20 22:15:16 +05:30
Sydney
0e8f6c1070 chore: update better titles for setting input 2026-01-20 22:06:45 +05:30
Sydney
7d760c3a57 chore: update default message doctype and test cases 2026-01-20 21:45:50 +05:30
Sydney
8ae3d1ef97 refactor: make ui changes as per requirement and fix general settings 2026-01-20 20:02:15 +05:30
RitvikSardana
696e267623 Merge pull request #2880 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-01-20 17:07:21 +05:30
Pratik
3ea717978a Merge remote-tracking branch 'upstream/develop' into feat/agent-landing-page 2026-01-20 16:33:06 +05:30
Pratik
e3b0469908 fix: labels 2026-01-20 16:29:59 +05:30
Pratik
bddb574f5f chore: update frappe-ui version 2026-01-20 15:53:18 +05:30
RitvikSardana
b969659b97 Merge pull request #2914 from RitvikSardana/agent-datastructure
fix(HD Agent): make fields mandatory
2026-01-20 00:41:32 +05:30
Ritvik Sardana
8b765ba34d chore: code cleanup 2026-01-19 23:48:46 +05:30
Ritvik Sardana
6944796c8e test: add agent_name field in test utils 2026-01-19 23:43:19 +05:30
Ritvik Sardana
04d289bb95 test: add agent_name field in test utils 2026-01-19 23:32:56 +05:30
Ritvik Sardana
c34dfc18ac fix(HD Agent): make fields mandatory 2026-01-19 23:23:13 +05:30
Sydney
c319c21579 feat: enhance outside working hours banner logic and improve test coverage 2026-01-19 19:51:58 +05:30
Sydney
f237f3bc3a fix: Initialize textarea with default message when banner is enabled 2026-01-19 19:15:23 +05:30
Sydney
6a48ea4cca feat: update outside hours banner logic and remove unused banner message code 2026-01-19 18:45:21 +05:30
RitvikSardana
da67591173 Merge pull request #2913 from RitvikSardana/lockFile-update
fix: update yarn lock in frontend folder
2026-01-19 18:36:57 +05:30
Ritvik Sardana
b784ea807f fix: update yarn lock in frontend folder 2026-01-19 18:36:19 +05:30
Sydney Gomes
6463e7b0cb Merge branch 'frappe:develop' into feat/after-hours-banner 2026-01-19 18:28:22 +05:30
RitvikSardana
cc1ec875e3 Merge pull request #2912 from RitvikSardana/chore-lockFile-update
fix: update lock file
2026-01-19 18:13:05 +05:30
Ritvik Sardana
6d6d3498cf fix: update lock file 2026-01-19 18:12:35 +05:30
RitvikSardana
30cd25eead Merge pull request #2910 from RitvikSardana/fix-comment-mention
fix(reactivity): mentions dropdown
2026-01-19 17:47:02 +05:30
Ritvik Sardana
53fe99dc8a fix(reactivity): mentions dropdown 2026-01-19 17:42:36 +05:30
Sydney
321af210c2 feat: implement default banner message for tickets created outside working hours though helpers 2026-01-19 15:10:46 +05:30
Sydney
f700832f00 chore: add test cases to check if ticket is outside working hours 2026-01-19 14:26:07 +05:30
RitvikSardana
c657b1c901 Merge pull request #2907 from RitvikSardana/notification-mention-duplicate
fix: duplicate mention notification
2026-01-19 14:01:28 +05:30
Ritvik Sardana
07ea05aa37 fix: notify only when comment is updated 2026-01-19 13:54:05 +05:30
Ritvik Sardana
67a753e5fb fix: duplicate mention notification 2026-01-19 13:46:40 +05:30
Ritvik Sardana
af46f0b818 chore: code cleanup 2026-01-19 12:48:04 +05:30
Ritvik Sardana
8f2baba838 chore: code cleanup 2026-01-19 12:47:38 +05:30
Ritvik Sardana
41578afca1 fix: better patch 2026-01-19 12:47:38 +05:30
Ritvik Sardana
b3fa7241d7 fix: better patch 2026-01-19 12:47:38 +05:30
Ritvik Sardana
9eb3902f63 fix: add fields for last customer and agent response 2026-01-19 12:47:38 +05:30
RitvikSardana
485a457e04 Merge pull request #2905 from RitvikSardana/fix-email-scroll
fix: email scroll & notifications
2026-01-19 11:02:42 +05:30
Ritvik Sardana
c696a595dd fix: render notifications properly 2026-01-19 10:56:54 +05:30
Ritvik Sardana
f4339b51e1 fix(EmailBox): load content properly 2026-01-19 10:56:39 +05:30
Sydney
5d55c08e36 chore: apply formatting from pre‑commit 2026-01-18 22:59:46 +05:30
Sydney
accef0aeb6 fix: removing unused import 2026-01-18 20:59:16 +05:30
Exequiel Arona
73b4ecc351 ci: fix generate POT workflow 2026-01-17 17:21:48 -03:00
artykbasar
f4b7224aaa Merge branch 'frappe:develop' into feat/email-domain-desk-links-clean 2026-01-17 20:08:58 +00:00
Sydney
a682ace80a fix: fix the reset button in general settings and display button only if banner setting is enabled 2026-01-16 22:16:12 +05:30
Sydney Gomes
8de148249b Merge branch 'frappe:develop' into feat/after-hours-banner 2026-01-16 21:07:00 +05:30
Sydney
0bb4ced6ae feat: set custom template in doctype for after hours banner message field 2026-01-16 21:00:16 +05:30
Sydney
b22ec72400 refactor: display tooltip explanation only for resolution 2026-01-16 20:57:04 +05:30
Sydney
b8be41911f feat: add template rendering in general settings to configure after hours banner message 2026-01-16 20:56:17 +05:30
RitvikSardana
a361a60429 Merge pull request #2812 from samarth93/feature/comment-reactions
feat: add comment reactions (thumbs up) feature
2026-01-16 13:01:37 +05:30
RitvikSardana
e11ebf6af8 Merge pull request #2897 from RitvikSardana/fix-tests-cr
test(saved-reply): revert user to admin
2026-01-16 12:10:22 +05:30
Ritvik Sardana
6c9d66e06b test(saved-reply): revert user to admin 2026-01-16 12:05:26 +05:30
RitvikSardana
f4ce0caac6 chore: version bump 2026-01-16 11:54:05 +05:30
Sydney Gomes
62f0356506 Merge branch 'frappe:develop' into feat/after-hours-banner 2026-01-16 11:20:16 +05:30
RitvikSardana
d692dd8952 chore: handle versioning properly 2026-01-16 10:52:22 +05:30
Sydney Gomes
b59b678555 Merge branch 'frappe:develop' into feat/after-hours-banner 2026-01-16 00:30:09 +05:30
Sydney
047c826a91 refactor: refine logic for detecting tickets outside working hours 2026-01-16 00:29:04 +05:30
Sydney
4ea2a59865 feat: add after hours banner settings in setting modal 2026-01-16 00:26:52 +05:30
Sydney
1caa02ef69 chore: improve UX by explaining SLA-based due times 2026-01-16 00:26:05 +05:30
Sydney
8637599c13 refactor: fetch priority description through createListResource 2026-01-16 00:24:52 +05:30
RitvikSardana
9858c5384c Merge pull request #2891 from RitvikSardana/dashboard-count
fix(dashboard): order by count
2026-01-15 15:36:43 +05:30
Ritvik Sardana
44877bd0e8 fix(dashboard): order by count 2026-01-15 15:34:59 +05:30
Sydney Gomes
57dc8d23ba Merge branch 'frappe:develop' into feat/after-hours-banner 2026-01-15 02:11:50 +05:30
Sydney
f782a96c3c feat: add method to query ticket priorities and their descriptions 2026-01-15 02:09:52 +05:30
Sydney
fdb6c4fd42 feat: display banner on ticket based on ticket sla conditions 2026-01-15 02:06:26 +05:30
Sydney
c7753beb44 chore: add is_outside field to track tickets raised outside working hours 2026-01-15 02:01:47 +05:30
Sydney
1a01f4adf6 feat: display the selected priority description through tooltip 2026-01-15 02:00:25 +05:30
Pratik
ff502ebdee refactor: use frappe-ui EChart component 2026-01-14 16:22:32 +00:00
Pratik
bf8e7de854 test: add tests 2026-01-14 14:10:11 +00:00
RitvikSardana
b389b80436 chore: code cleanup 2026-01-14 17:01:24 +05:30
RitvikSardana
59f93c3c9c chore: Update frappe dependency version 2026-01-14 17:00:11 +05:30
RitvikSardana
d54d1bc888 Merge pull request #2886 from frappe/v16-support-2
fix: better support for v16
2026-01-14 16:57:39 +05:30
Ritvik Sardana
3b85a3b269 fix: better support for v16 2026-01-14 16:51:44 +05:30
RitvikSardana
61b3ab2e4a Merge pull request #2884 from RitvikSardana/search-char
fix: sanitize search query
2026-01-14 13:48:51 +05:30
Ritvik Sardana
5182126876 fix: sanitize search query 2026-01-14 13:48:22 +05:30
Pratik
15e530c007 refactor: feedback card 2026-01-14 07:34:13 +00:00
RitvikSardana
ca8b551696 Merge pull request #2878 from RitvikSardana/test-split-merge
test: add tests for ticket split and merge
2026-01-14 13:01:26 +05:30
RitvikSardana
3e1e20e827 Merge pull request #2881 from RitvikSardana/minor-fixes
fix: minor UI fixes
2026-01-14 13:01:06 +05:30
Ritvik Sardana
2c3e8dbcb2 fix(styles): reaction tooltip 2026-01-14 12:59:23 +05:30
Ritvik Sardana
1515970c39 fix(tests): add email domain 2026-01-14 12:38:03 +05:30
Ritvik Sardana
1ce75aa717 chore: code cleanup 2026-01-14 12:18:02 +05:30
Ritvik Sardana
6bd424ff83 fix: loading state in editor 2026-01-14 11:02:20 +05:30
Ritvik Sardana
9602681745 fix: minor style changes 2026-01-14 10:28:22 +05:30
Ritvik Sardana
b5ff021b86 fix: order_by in ticket_summary report 2026-01-14 09:46:44 +05:30
RitvikSardana
79943278e9 chore: French translations 2026-01-14 08:26:48 +05:30
artykbasar
43038ecd99 feat(settings): support Email Domain for custom email accounts
- Add Email Domain link field for Custom provider and auto-fill server settings
- Open Email Domain/Email Account in Desk for create/edit
- Avoid sending Custom-domain fields for non-Custom providers
- Add service aliases for legacy Email Account service values (e.g. Outlook.com, Yahoo Mail, Yandex.Mail)
2026-01-13 20:06:58 +00:00
samarth93
96faca2b87 test: enable comment reactions setting in test setup 2026-01-13 20:32:53 +05:30
Ritvik Sardana
b652cb9c65 test: add tests for ticket split and merge 2026-01-13 19:05:13 +05:30
RitvikSardana
8127421856 Merge pull request #2859 from frappe/l10n_develop2
chore: sync translations from crowdin
2026-01-13 16:12:31 +05:30
Pratik
dc731d26f2 refactor: pending tickets chart 2026-01-13 10:00:49 +00:00
Sydney Gomes
52f9e2db96 Merge branch 'frappe:develop' into feat/after-hours-banner 2026-01-13 14:57:16 +05:30
Sydney
9368a5a2d2 feat: banner for outside working hours displayed on frontend 2026-01-13 14:47:15 +05:30
Sydney
928652344d feat: add method to check working hours based on existing SLA's 2026-01-13 14:45:53 +05:30
Sydney
d82bf598dc feat:Add check and custom field to display after hours banner in settings doctype 2026-01-13 14:44:40 +05:30
RitvikSardana
cf0e6acc80 Merge branch 'develop' into feature/comment-reactions 2026-01-13 11:51:59 +05:30
Ritvik Sardana
813841f2b2 chore: arrange child table properly 2026-01-13 11:50:48 +05:30
Ritvik Sardana
c8ab762d89 fix: add socket support and make color changes 2026-01-13 11:37:28 +05:30
RitvikSardana
e5f9eebb51 Merge pull request #2874 from RitvikSardana/readme-update
chore: update readme
2026-01-13 10:37:03 +05:30
Ritvik Sardana
03fe225df7 chore: update readme 2026-01-13 10:18:57 +05:30
RitvikSardana
48b7dd3aa0 Merge pull request #2873 from RitvikSardana/test-fixes
test: handle dates properly
2026-01-13 10:12:52 +05:30
Ritvik Sardana
90c365a26c test: handle dates properly 2026-01-13 10:07:36 +05:30
RitvikSardana
f2f195e855 chore: Indonesian translations 2026-01-11 08:23:43 +05:30
RitvikSardana
ec42c8fcff Merge pull request #2867 from vkWeb/fix-@mention
fix: rendering @mention in comments
2026-01-09 17:41:00 +05:30
Vivek Agrawal
d8228382b0 fix: rendering @mention in comments 2026-01-09 15:00:48 +05:30
Pratik
114fcfecc9 Merge remote-tracking branch 'upstream/develop' into feat/agent-landing-page 2026-01-09 13:07:01 +05:30
RitvikSardana
3fe45bcf7e chore: Russian translations 2026-01-09 08:06:02 +05:30
RitvikSardana
b8a4803824 chore: Hungarian translations 2026-01-07 07:32:55 +05:30
RitvikSardana
0858dbdc03 Merge pull request #2863 from RitvikSardana/fix-custom-button
fix: dont run onClick twice
2026-01-07 00:24:25 +05:30
RitvikSardana
ec82ee91a9 fix: revert translation changes 2026-01-07 00:23:57 +05:30
Ritvik Sardana
4e1e261a51 fix: dont run onClick twice 2026-01-07 00:20:24 +05:30
RitvikSardana
5d8a7ba05c chore: Dutch translations 2026-01-06 07:25:25 +05:30
RitvikSardana
4004daf191 chore: Hungarian translations 2026-01-06 07:25:24 +05:30
samarth93
0b05a7c021 feat: add enable_comment_reactions setting to toggle feature 2026-01-04 21:12:17 +05:30
RitvikSardana
d23fb1287c chore: Bump version from 1.18.0 to 1.18.1 2026-01-02 13:04:43 +05:30
RitvikSardana
a5d9530c41 chore: version bump 2026-01-01 21:26:01 +05:30
RitvikSardana
89a180a876 Merge pull request #2854 from pratikb64/fix/saved-reply-bugs
fix: saved reply renaming
2026-01-01 21:23:43 +05:30
Pratik
819c28a805 fix: saved reply renaming 2026-01-01 06:43:17 +00:00
samarth93
7a18b330ca fix: use format() instead of f-string for Python 3.14 compatibility 2025-12-31 23:26:07 +05:30
samarth93
8d1101c7eb style: remove trailing comma for prettier 2025-12-31 23:20:47 +05:30
samarth93
de070ed4ef test: update assertions for new notification message format 2025-12-31 23:15:15 +05:30
samarth93
1ca388de7b style: apply prettier formatting 2025-12-31 23:14:00 +05:30
samarth93
f69de39a09 fix: improve reaction notification display and routing 2025-12-31 23:07:13 +05:30
RitvikSardana
77a88bacd7 Merge pull request #2708 from pratikb64/refactor/canned-responses 2025-12-31 12:03:49 +05:30
Pratik
78d42bd505 fix: ci lint error 2025-12-31 06:02:22 +00:00
Pratik
0789b95561 Merge remote-tracking branch 'upstream/develop' into refactor/canned-responses 2025-12-31 06:00:12 +00:00
RitvikSardana
4b191edbc1 Merge pull request #2836 from bvisible/feat/i18n-vue-components
feat(i18n): Add translation wrappers to Vue components
2025-12-31 03:23:04 +05:30
RitvikSardana
4532c42f64 fix: add translation strings for sidebar label 2025-12-31 03:13:38 +05:30
RitvikSardana
c025d431e4 Merge pull request #2839 from KerollesFathy/fix-hd-reports-routing
fix(workspace): helpdesk reports routing
2025-12-31 03:04:04 +05:30
RitvikSardana
e858a3dc05 Merge pull request #2842 from asaura08/patch-1
fix: last_synced_at to last_received_at in email sync
2025-12-31 03:03:20 +05:30
RitvikSardana
0a80fb6850 Merge pull request #2846 from RitvikSardana/bump-python
chore: bump python to v3.14
2025-12-31 02:54:16 +05:30
Ritvik Sardana
20ea608045 fix(tests): handle holiday cases in utils 2025-12-31 02:40:26 +05:30
KerollesFathy
c395c8b67a fix(workspace): helpdesk reports routing 2025-12-30 17:58:09 +00:00
KerollesFathy
ae97eafff5 revert: helpdesk reports routing"
This reverts commit 7f785be3b8.
2025-12-30 17:38:29 +00:00
Ritvik Sardana
cae4d382ec chore: bump pre commit versioning 2025-12-30 18:26:21 +05:30
Ritvik Sardana
5853587bbb chore(ci): correct command for coverage 2025-12-30 18:22:17 +05:30
Ritvik Sardana
66c0790f98 chore: bump node version 2025-12-30 18:17:53 +05:30
Ritvik Sardana
ca74bcd41e fix: show test coverage 2025-12-30 18:12:40 +05:30
Ritvik Sardana
5cf21e2003 chore: bump python version to 3.14 2025-12-30 18:10:57 +05:30
Ritvik Sardana
75d6fa56c5 fix: add list count 2025-12-30 18:01:48 +05:30
Pratik
ef5f3938ca chore: add translation 2025-12-30 10:50:17 +00:00
Pratik
37ee3c5ca8 chore: add translation 2025-12-30 10:37:13 +00:00
Pratik
a89aab3a42 fix: avatar label & loading state in reply selector modal 2025-12-30 10:24:04 +00:00
Ritvik Sardana
1068f8bc86 fix(naming): use title field for naming in Saved Reply 2025-12-30 15:03:44 +05:30
Jérémy Christillin
58dfc6d95d style: apply prettier formatting 2025-12-30 10:03:12 +01:00
RitvikSardana
83bc341b88 Merge pull request #2837 from frappe/pot_develop_2025-12-21
chore: update POT file
2025-12-30 12:55:00 +05:30
RitvikSardana
501e1a903e Merge pull request #2833 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-12-30 12:45:31 +05:30
RitvikSardana
1218e9219a chore: Persian translations 2025-12-30 07:09:38 +05:30
RitvikSardana
930e8ec184 chore: Spanish translations 2025-12-30 07:09:37 +05:30
Elnegren
7432d3e9d3 fix: last_synced_at to last_received_at in email sync 2025-12-29 23:24:46 +01:00
RitvikSardana
847e485fb4 chore: Persian translations 2025-12-28 06:55:27 +05:30
Pratik
5b7a458b42 fix: focus on reply selector modal open 2025-12-24 06:39:06 +00:00
KerollesFathy
7f785be3b8 fix(workspace): helpdesk reports routing 2025-12-22 13:34:29 +00:00
Pratik
386632051d fix: tiptap extension suggestion search 2025-12-22 10:08:34 +00:00
Pratik
ead471ceb1 chore: fix typo 2025-12-22 09:34:20 +00:00
Pratik
833a185855 Merge remote-tracking branch 'upstream/develop' into refactor/canned-responses 2025-12-22 07:03:39 +00:00
Pratik
ecfdd6d193 test: add test for editing personal reply 2025-12-22 05:32:32 +00:00
Pratik
40942f6759 refactor: rewrite sql into frappe qb 2025-12-22 05:08:40 +00:00
Pratik
ad3359f785 test: add tests 2025-12-21 14:49:42 +00:00
frappe-pr-bot
63d4442a54 chore: update POT file 2025-12-21 10:37:35 +00:00
Jérémy Christillin
838ffcdf74 feat(i18n): Add French translations for new i18n strings
Add 40+ French translations for settings, SLA, and workflow strings.
2025-12-20 14:26:28 +01:00
Jérémy Christillin
f6db2f72d7 feat(i18n): Add translation wrappers to Vue components
Add __() translation function wrappers to hardcoded strings in Vue/TypeScript
components to enable internationalization support.

Files modified:
- ListViewBuilder, SearchMultiSelect, FilterPopover components
- Settings: NewTeamModal, ExportModal
- Ticket views: TicketAgent, MobileTicketAgent, TicketCustomer, TicketAgentSidebar
- Knowledge Base: Article, NewArticle, KnowledgeBase views
- Contact: ContactDialog, SetContactPhoneModal
- Notifications: MobileNotifications
- Activity: ActivityHeader, CommunicationArea
- Customer: NewCustomerDialog

This enables proper translation of UI strings through PO files.
2025-12-20 14:16:22 +01:00
samarth93
f313fd00b1 fix: wrap user-facing strings in translate function 2025-12-19 22:54:20 +05:30
samarth93
270ece00f9 fix: prettier formatting 2025-12-19 22:51:35 +05:30
samarth93
73673ea050 fix: pre-commit formatting 2025-12-19 22:39:54 +05:30
samarth93
7535c416a1 feat: grouped notifications and UI refinements for comment reactions 2025-12-19 22:26:45 +05:30
Pratik
d2d24a516c fix: loading indicator fix 2025-12-18 05:44:28 +00:00
Pratik
a24986d1bb fix: minor conditional logic fix 2025-12-18 05:23:41 +00:00
RitvikSardana
2adbe1cdba chore: Hungarian translations 2025-12-17 04:49:29 +05:30
RitvikSardana
5eddc89075 chore: Hungarian translations 2025-12-16 04:34:19 +05:30
RitvikSardana
d0c6e5167a chore: Spanish translations 2025-12-16 04:34:15 +05:30
Pratik
9723ccb5ae refactor: rename canned response to saved reply 2025-12-15 10:12:37 +00:00
RitvikSardana
8baa72c007 Merge pull request #2823 from frappe/pot_develop_2025-12-14
chore: update POT file
2025-12-15 12:51:24 +05:30
RitvikSardana
b7de0086a8 Merge pull request #2816 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-12-15 12:51:00 +05:30
RitvikSardana
63d92eab5c Merge pull request #2798 from samarth93/fix/preserve-query-params-#2790
fix: preserve query parameters when switching tabs (#2790)
2025-12-15 12:46:50 +05:30
RitvikSardana
4084a24649 Merge pull request #2827 from RitvikSardana/count-fix
fix: use get_list to get count
2025-12-15 12:03:58 +05:30
Ritvik Sardana
8d847962f3 fix: add fallback value 2025-12-15 11:47:43 +05:30
Ritvik Sardana
698659f7c4 chore: code cleanup 2025-12-15 11:46:57 +05:30
Ritvik Sardana
28947034ba fix: use get_list to get count 2025-12-15 11:42:56 +05:30
RitvikSardana
6cd0163e2d chore: Russian translations 2025-12-15 04:34:29 +05:30
frappe-pr-bot
c5b6ab3569 chore: update POT file 2025-12-14 10:37:03 +00:00
RitvikSardana
3189db9884 chore: Hungarian translations 2025-12-14 04:10:25 +05:30
RitvikSardana
f2bacb6200 chore: Hungarian translations 2025-12-13 03:57:09 +05:30
Pratik
5e56fc78d2 fix: pending tickets card sorting 2025-12-12 11:30:37 +00:00
Pratik
60c38dbd38 fix: change pending tickets filter logic 2025-12-12 11:10:12 +00:00
RitvikSardana
032e1b0bbe chore: Hungarian translations 2025-12-12 03:47:25 +05:30
KerollesFathy
782313a084 fix: move release.yml to workflows dir 2025-12-11 13:44:42 +00:00
KerollesFathy
df2270a9c8 ci: setup semantic-release workflow 2025-12-11 13:04:52 +00:00
Pratik
6f4a01f476 fix: fix height 2025-12-11 12:50:05 +05:30
RitvikSardana
a1fd77f1f3 chore: Serbian (Latin) translations 2025-12-11 03:33:53 +05:30
RitvikSardana
39e38c27ec chore: Serbian (Cyrillic) translations 2025-12-11 03:33:52 +05:30
RitvikSardana
09e6d637dd chore: Esperanto translations 2025-12-10 03:06:36 +05:30
RitvikSardana
fee0b713c9 chore: Slovenian translations 2025-12-10 03:06:35 +05:30
RitvikSardana
d492740475 chore: French translations 2025-12-10 03:06:34 +05:30
RitvikSardana
211781be1c chore: Serbian (Latin) translations 2025-12-10 03:06:32 +05:30
RitvikSardana
2fc900c02d chore: Norwegian Bokmal translations 2025-12-10 03:06:31 +05:30
RitvikSardana
0744eb5d03 chore: Bosnian translations 2025-12-10 03:06:29 +05:30
RitvikSardana
e3661e9836 chore: Burmese translations 2025-12-10 03:06:27 +05:30
RitvikSardana
13aab390b0 chore: Croatian translations 2025-12-10 03:06:26 +05:30
RitvikSardana
c64dcea9b8 chore: Thai translations 2025-12-10 03:06:25 +05:30
RitvikSardana
ecb076eec8 chore: Tamil translations 2025-12-10 03:06:24 +05:30
RitvikSardana
b51a22c2b6 chore: Persian translations 2025-12-10 03:06:23 +05:30
RitvikSardana
50fb1c20fc chore: Indonesian translations 2025-12-10 03:06:22 +05:30
RitvikSardana
abd6d46963 chore: Portuguese, Brazilian translations 2025-12-10 03:06:20 +05:30
RitvikSardana
da338ef7b0 chore: Vietnamese translations 2025-12-10 03:06:19 +05:30
RitvikSardana
b5cee665b9 chore: Chinese Simplified translations 2025-12-10 03:06:18 +05:30
RitvikSardana
e3a7ea1516 chore: Turkish translations 2025-12-10 03:06:16 +05:30
RitvikSardana
68ad2efdf9 chore: Swedish translations 2025-12-10 03:06:15 +05:30
RitvikSardana
8acff3fc51 chore: Serbian (Cyrillic) translations 2025-12-10 03:06:14 +05:30
RitvikSardana
420c33787e chore: Russian translations 2025-12-10 03:06:12 +05:30
RitvikSardana
93f3504009 chore: Portuguese translations 2025-12-10 03:06:11 +05:30
RitvikSardana
7b9ff2497f chore: Polish translations 2025-12-10 03:06:10 +05:30
RitvikSardana
f1ad5b8e03 chore: Dutch translations 2025-12-10 03:06:09 +05:30
RitvikSardana
393ad9073b chore: Italian translations 2025-12-10 03:06:07 +05:30
RitvikSardana
ecc17e12e7 chore: Hungarian translations 2025-12-10 03:06:06 +05:30
RitvikSardana
4357bd544f chore: German translations 2025-12-10 03:06:05 +05:30
RitvikSardana
8c74a69625 chore: Danish translations 2025-12-10 03:06:04 +05:30
RitvikSardana
61b971c0d1 chore: Czech translations 2025-12-10 03:06:03 +05:30
RitvikSardana
b2776805f6 chore: Arabic translations 2025-12-10 03:06:02 +05:30
RitvikSardana
97e424c56b chore: Spanish translations 2025-12-10 03:06:00 +05:30
Pratik
29ecceec63 Merge branch 'develop' into refactor/canned-responses 2025-12-09 15:47:48 +00:00
samarth93
18c2aa1f69 chore: remove temporary pr description file 2025-12-09 21:13:00 +05:30
samarth93
3a3c3941df fix: use dynamic ticket name in tests
The test was failing because it tried to use hardcoded
'TEST-TICKET-001' name but HD Ticket uses autoname.

Changed to:
- Create ticket dynamically and store in self.test_ticket
- Reference self.test_ticket.name for comments
- Clean up ticket in tearDown
2025-12-09 21:04:37 +05:30
Pratik
712a09eaba fix: bug in saved reply select modal 2025-12-09 15:29:56 +00:00
samarth93
6733de3f00 fix: remove unused username variable
Fixes linter error from unused variable
2025-12-09 20:48:40 +05:30
Pratik
3c92a8d39e fix: code cleanup & refactor patches 2025-12-09 14:56:39 +00:00
samarth93
5671b4baa8 feat: implement preset emojis with validation and comprehensive tests
Addressing feedback:
1. One reaction per user (not multiple)
2. Preset emojis only: ['👍', '👎', '❤️', '🎉', '👀', '✅']
3. API validation to block non-preset emojis
4. Updated notification message: 'reacted with X on your comment'
5. Comprehensive test cases covering all requirements

Backend changes:
- Add PRESET_EMOJIS constant
- Add emoji validation in toggle_reaction API
- Change to one reaction per user (replaces on different emoji)
- Update notification message format
- Add get_preset_emojis() API

Frontend changes:
- Update emojiList to match preset emojis

Test cases added:
- test_one_reaction_per_user: Verifies only one reaction allowed
- test_reaction_count_accuracy: Tests count with 10 hearts, 3 thumbs up
- test_notification_created_on_reaction: Validates HD Notification creation
- test_only_preset_emojis_allowed: Ensures only preset emojis accepted
- test_reaction_toggle_behavior: Tests toggle on/off behavior
- test_get_reactions_returns_correct_data: Validates API response structure
2025-12-09 20:16:39 +05:30
Pratik
7f45ec6f18 refactor: add & use HD Saved Reply doctype 2025-12-09 13:38:39 +00:00
samarth93
26a20ac1d7 fix: rename to HD Comment Reaction and allow multiple reactions
Addressing feedback from @RitvikSardana:
1. Renamed child table from 'HD Reaction' to 'HD Comment Reaction'
2. Allow users to add multiple different reactions (not just one)
3. Updated PR description with screenshot

Changes:
- Rename HD Reaction → HD Comment Reaction (DocType name)
- Update toggle_reaction logic to allow multiple reactions per user
- Users can now react with different emojis (👍 + ❤️ + 🎉 etc)
- Clicking same emoji toggles it off
- Updated all references in code
2025-12-09 17:55:58 +05:30
samarth93
55ce67903b refactor: store reactions as child table with emoji picker
Based on feedback from @RitvikSardana:
- Replace standalone HD Comment Reaction with HD Reaction child table
- Store reactions inside HD Ticket Comment (like Gameplan)
- Add emoji picker with multiple emoji options (👍👎❤️😄🎉😕👀🚀)
- Users can only have one reaction per comment (clicking different emoji replaces)

Changes:
- New HD Reaction DocType (child table with emoji + user)
- Add reactions Table field to HD Ticket Comment
- Add toggle_reaction() and get_reactions() APIs
- Update CommentBox.vue with emoji picker popover
- Remove old HD Comment Reaction standalone DocType
2025-12-09 17:35:00 +05:30
RitvikSardana
8827be45c3 Merge pull request #2809 from frappe/pot_develop_2025-12-07
chore: update POT file
2025-12-09 15:50:46 +05:30
RitvikSardana
804251888c Merge pull request #2805 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-12-09 15:50:20 +05:30
Pratik
c442a6b895 refactor: remove unused imports 2025-12-09 09:44:16 +00:00
Pratik
cd367702a8 Merge remote-tracking branch 'origin/develop' into feat/agent-landing-page 2025-12-09 09:36:42 +00:00
RitvikSardana
a9e35f9b9b chore: Russian translations 2025-12-09 03:05:45 +05:30
RitvikSardana
6b44eac9d1 chore: Hungarian translations 2025-12-09 03:05:41 +05:30
samarth93
d8c1f8ca19 feat: add comment reactions (thumbs up) feature
Allows users to react to comments with a thumbs up emoji instead of
creating new comments. This reduces ticket clutter when acknowledging
a comment.

Changes:
- Add new HD Comment Reaction DocType to store reactions
- Add toggle_reaction and get_comment_reactions API endpoints
- Add reaction UI to CommentBox.vue with hover tooltip showing users
- Send notification to comment author when reaction is added

Backend:
- helpdesk/helpdesk/doctype/hd_comment_reaction/ - new DocType
- API: toggle_reaction(comment, reaction) - add/remove reaction
- API: get_comment_reactions(comment) - list reactions with users

Frontend:
- CommentBox.vue - thumbs up button with count and user tooltip
- Highlights button when current user has reacted
- Real-time update via publish_event
2025-12-08 20:49:00 +05:30
Pratik
b9dd1018ff refactor: change file name 2025-12-08 11:18:42 +00:00
Pratik
d32b4e4998 fix: add icons in scope select 2025-12-08 11:12:05 +00:00
Pratik
722739a284 fix: minor enhancements 2025-12-08 09:39:38 +00:00
Pratik
328bf41309 refactor: rename canned response to saved reply 2025-12-07 17:12:52 +00:00
Pratik
ee33c13f88 fix: improve toast error message in saved replies preview dialog 2025-12-07 15:58:25 +00:00
Pratik
122cb44579 fix: improve patch logic 2025-12-07 15:12:20 +00:00
Pratik
a54dda5095 fix: owner field not getting set in patch 2025-12-07 15:00:58 +00:00
Pratik
87d273a3ff fix: patch order 2025-12-07 14:49:15 +00:00
Pratik
8012a59466 fix: add patch for email template fields 2025-12-07 14:43:46 +00:00
Pratik
e50de00a34 fix: code cleanup & enhancements 2025-12-07 14:34:52 +00:00
frappe-pr-bot
0d5d0e2b1d chore: update POT file 2025-12-07 10:36:14 +00:00
RitvikSardana
20fb1b7e42 chore: Russian translations 2025-12-07 02:51:54 +05:30
RitvikSardana
bdce05270a chore: Hungarian translations 2025-12-06 02:52:56 +05:30
RitvikSardana
8f2699c927 Merge pull request #2803 from RitvikSardana/customize-button
fix: add support for button variables in custom buttons
2025-12-05 22:47:27 +05:30
Ritvik Sardana
7f4ad4387a fix: add support for button variables in custom buttons 2025-12-05 22:37:08 +05:30
RitvikSardana
0f08da3b10 Merge pull request #2797 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-12-05 21:19:17 +05:30
RitvikSardana
15965b2f54 Merge pull request #2794 from pratikb64/fix/settings-modal-bugs
fix: bugs related to frappe-ui update
2025-12-05 21:17:02 +05:30
Pratik
5171c3bf22 fix: ticket view tabs alignment 2025-12-05 14:20:13 +00:00
Pratik
cc42501517 fix: revert select element in gemeral settings 2025-12-05 11:11:52 +00:00
Pratik
2c2039faa6 fix: popover width 2025-12-05 10:55:26 +00:00
RitvikSardana
e9d9c3c858 Merge pull request #2795 from RitvikSardana/dashboard-security-fix
fix(security): dashboard APIs & realtime events
2025-12-05 16:06:05 +05:30
Ritvik Sardana
b2f7e029be fix: dont bombard socket events 2025-12-05 15:23:51 +05:30
Pratik
d6775677a2 fix: code cleanup 2025-12-05 07:48:26 +00:00
Pratik
4b43b6d01f fix: select options width & assigmnent rule bugs 2025-12-05 07:12:14 +00:00
Pratik
bb80b724c8 refactor: move tiptap extension 2025-12-05 04:55:23 +00:00
RitvikSardana
6f13958974 chore: Slovenian translations 2025-12-05 02:57:53 +05:30
RitvikSardana
c9b969f3fd chore: Bosnian translations 2025-12-05 02:57:52 +05:30
RitvikSardana
fa44401f6e chore: Croatian translations 2025-12-05 02:57:51 +05:30
RitvikSardana
b927731bcf chore: Russian translations 2025-12-05 02:57:49 +05:30
samarth
3f01c575fb fix: preserve query parameters when switching tabs (#2790)
Fixes #2790 - Active View is removed from the query params when the page is reloaded

When navigating to a ticket from an Active View and switching between tabs,
the query parameters (including view parameter) were being removed from the URL.

Changes:
- Updated router.replace() calls in useActiveTabManager.ts to preserve
  route.query alongside route.path
- Ensures view context is maintained during tab navigation
- Applies to changeTabTo(), setActiveTab(), and hash watch handler

This fix ensures that when users navigate to a ticket from an Active View
and switch between tabs, the view parameter remains in the URL for proper
navigation context.
2025-12-04 21:39:49 +05:30
Pratik
4b8e9064d2 fix: upcoming sla widget badge bug 2025-12-04 15:41:40 +00:00
Pratik
8e6848dc23 fix: bugs introduced by frappe-ui update 2025-12-04 12:33:00 +00:00
Pratik
0da0c7deae fix: tabs component syntax 2025-12-04 11:33:12 +00:00
Pratik
87a7cb0d08 refactor: add HD Field Layout doctype 2025-12-04 10:50:07 +00:00
Pratik
f75834dc70 fix: bugs related to frappe-ui update 2025-12-04 07:26:33 +00:00
Pratik
131c9615b0 fix: bugs introduced by frappe-ui update 2025-12-04 05:41:18 +00:00
RitvikSardana
39d49bba6d chore: Esperanto translations 2025-12-04 02:56:47 +05:30
RitvikSardana
5908ac8cd1 chore: Slovenian translations 2025-12-04 02:56:46 +05:30
RitvikSardana
3d61f226ff chore: French translations 2025-12-04 02:56:45 +05:30
RitvikSardana
526828ca87 chore: Serbian (Latin) translations 2025-12-04 02:56:44 +05:30
RitvikSardana
918aa7582f chore: Norwegian Bokmal translations 2025-12-04 02:56:42 +05:30
RitvikSardana
ab12cc4017 chore: Bosnian translations 2025-12-04 02:56:41 +05:30
RitvikSardana
bb2db66f81 chore: Burmese translations 2025-12-04 02:56:40 +05:30
RitvikSardana
1398427e65 chore: Croatian translations 2025-12-04 02:56:38 +05:30
RitvikSardana
d329acb0d7 chore: Thai translations 2025-12-04 02:56:37 +05:30
RitvikSardana
f39db539e7 chore: Tamil translations 2025-12-04 02:56:36 +05:30
RitvikSardana
6b694352b1 chore: Persian translations 2025-12-04 02:56:35 +05:30
RitvikSardana
6c356d3a73 chore: Indonesian translations 2025-12-04 02:56:34 +05:30
RitvikSardana
7c6311eeb1 chore: Portuguese, Brazilian translations 2025-12-04 02:56:32 +05:30
RitvikSardana
9a6e74f23c chore: Vietnamese translations 2025-12-04 02:56:31 +05:30
RitvikSardana
d6a96a4c6b chore: Chinese Simplified translations 2025-12-04 02:56:30 +05:30
RitvikSardana
90e1b75793 chore: Turkish translations 2025-12-04 02:56:28 +05:30
RitvikSardana
9d6488ac7b chore: Swedish translations 2025-12-04 02:56:27 +05:30
RitvikSardana
38d326294f chore: Serbian (Cyrillic) translations 2025-12-04 02:56:26 +05:30
RitvikSardana
2c8d353105 chore: Russian translations 2025-12-04 02:56:25 +05:30
RitvikSardana
cb80db8bb6 chore: Portuguese translations 2025-12-04 02:56:23 +05:30
RitvikSardana
fb02488cc2 chore: Polish translations 2025-12-04 02:56:22 +05:30
RitvikSardana
bdb401cea8 chore: Dutch translations 2025-12-04 02:56:21 +05:30
RitvikSardana
cec8e1c963 chore: Italian translations 2025-12-04 02:56:19 +05:30
RitvikSardana
4184f067f7 chore: Hungarian translations 2025-12-04 02:56:18 +05:30
RitvikSardana
07764582bc chore: German translations 2025-12-04 02:56:17 +05:30
RitvikSardana
cb5ffcc299 chore: Danish translations 2025-12-04 02:56:16 +05:30
RitvikSardana
1c4b5eb4f5 chore: Czech translations 2025-12-04 02:56:14 +05:30
RitvikSardana
6126dbef15 chore: Arabic translations 2025-12-04 02:56:13 +05:30
RitvikSardana
9a43e789a3 chore: Spanish translations 2025-12-04 02:56:12 +05:30
Pratik
443b5f5c42 feat: add agent landing page 2025-12-03 16:35:38 +00:00
Ritvik Sardana
1e1cd602d6 refactor: convert dashboard api to class and add QB support 2025-12-03 18:28:27 +05:30
Pratik
e2a2d29b84 fix: bugs related to frappe-ui update 2025-12-03 12:19:42 +00:00
RitvikSardana
76533e7317 Merge pull request #2788 from frappe/pot_develop_2025-11-30
chore: update POT file
2025-12-03 14:27:05 +05:30
RitvikSardana
46a17faa73 Merge pull request #2786 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-12-03 14:26:47 +05:30
RitvikSardana
234f70aae2 Merge pull request #2789 from frappe/v16-support
chore: V16 support
2025-12-03 13:56:53 +05:30
Pratik
318bd3d9d3 fix: modal permission & small code cleanup 2025-12-03 06:28:41 +00:00
Ritvik Sardana
b800ee03c2 chore: code cleanup 2025-12-03 11:58:33 +05:30
Ritvik Sardana
d016312f86 fix: add util to check version 2025-12-03 11:46:51 +05:30
RitvikSardana
cf5be1ea67 chore: Persian translations 2025-12-03 02:44:05 +05:30
RitvikSardana
2ed447646d chore: Hungarian translations 2025-12-03 02:44:03 +05:30
Pratik
a180688c2e Merge remote-tracking branch 'origin/develop' into refactor/canned-responses 2025-12-02 16:37:44 +00:00
RitvikSardana
8a87142618 chore: Spanish translations 2025-12-02 01:53:39 +05:30
Ritvik Sardana
7b9dbe748e fix: better way to check version 2025-12-01 13:40:01 +05:30
Ritvik Sardana
246462901e chore: revert py project changes 2025-12-01 13:21:42 +05:30
Ritvik Sardana
2309dd9c9d fix: add version change for testing in v15 2025-12-01 13:07:08 +05:30
Ritvik Sardana
a36e377771 fix: add conditional version support 2025-12-01 12:44:47 +05:30
RitvikSardana
12b9fe46ef chore: Persian translations 2025-12-01 01:37:32 +05:30
frappe-pr-bot
a27001e934 chore: update POT file 2025-11-30 10:36:30 +00:00
RitvikSardana
bca072eb23 chore: Slovenian translations 2025-11-30 01:07:24 +05:30
RitvikSardana
3b2b3a194f chore: Slovenian translations 2025-11-29 01:06:54 +05:30
RitvikSardana
3960d96377 chore: Serbian (Latin) translations 2025-11-29 01:06:52 +05:30
RitvikSardana
b04b4b1aa3 chore: Norwegian Bokmal translations 2025-11-29 01:06:51 +05:30
RitvikSardana
11de025c42 chore: Bosnian translations 2025-11-29 01:06:50 +05:30
RitvikSardana
401ee26e3e chore: Croatian translations 2025-11-29 01:06:48 +05:30
RitvikSardana
35ae4af61a chore: Thai translations 2025-11-29 01:06:47 +05:30
RitvikSardana
6dee7bf2e2 chore: Tamil translations 2025-11-29 01:06:46 +05:30
RitvikSardana
d615d7ae3e chore: Persian translations 2025-11-29 01:06:44 +05:30
RitvikSardana
9cb6df0bee chore: Indonesian translations 2025-11-29 01:06:43 +05:30
RitvikSardana
13aff48429 chore: Portuguese, Brazilian translations 2025-11-29 01:06:42 +05:30
RitvikSardana
c413ef2e48 chore: Vietnamese translations 2025-11-29 01:06:41 +05:30
RitvikSardana
b367cdc828 chore: Chinese Simplified translations 2025-11-29 01:06:40 +05:30
RitvikSardana
5c7efb6372 chore: Turkish translations 2025-11-29 01:06:39 +05:30
RitvikSardana
e3aacdbcdc chore: Swedish translations 2025-11-29 01:06:37 +05:30
RitvikSardana
91dd2f4a54 chore: Serbian (Cyrillic) translations 2025-11-29 01:06:36 +05:30
RitvikSardana
cf629ebd21 chore: Russian translations 2025-11-29 01:06:35 +05:30
RitvikSardana
ce62f05c25 chore: Portuguese translations 2025-11-29 01:06:33 +05:30
RitvikSardana
5150c85284 chore: Polish translations 2025-11-29 01:06:32 +05:30
RitvikSardana
bff28f2642 chore: Dutch translations 2025-11-29 01:06:31 +05:30
RitvikSardana
70da538f78 chore: Italian translations 2025-11-29 01:06:30 +05:30
RitvikSardana
a2827630b1 chore: Hungarian translations 2025-11-29 01:06:29 +05:30
RitvikSardana
d527448d5b chore: German translations 2025-11-29 01:06:28 +05:30
RitvikSardana
3cf62a9a2e chore: Danish translations 2025-11-29 01:06:26 +05:30
RitvikSardana
e86ee3f5e8 chore: Czech translations 2025-11-29 01:06:25 +05:30
RitvikSardana
6789a25986 chore: Arabic translations 2025-11-29 01:06:24 +05:30
Ritvik Sardana
825ec55d80 chore: add v16 support 2025-11-28 15:21:26 +05:30
RitvikSardana
cf61ba1035 Merge pull request #2783 from RitvikSardana/version-bump2
chore: bump version
2025-11-28 11:31:09 +05:30
Ritvik Sardana
8fb56bad25 chore: bump version 2025-11-28 11:30:36 +05:30
RitvikSardana
429d550eb5 Merge pull request #2779 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-28 11:28:28 +05:30
RitvikSardana
c1781bd8b4 chore: Slovenian translations 2025-11-28 00:52:28 +05:30
RitvikSardana
5c4a7ac43c chore: Burmese translations 2025-11-28 00:52:26 +05:30
RitvikSardana
4ea804b0d0 chore: Esperanto translations 2025-11-27 00:51:03 +05:30
RitvikSardana
a546403f1c chore: Slovenian translations 2025-11-27 00:51:02 +05:30
RitvikSardana
5947c38d30 chore: French translations 2025-11-27 00:51:01 +05:30
RitvikSardana
863b543e28 chore: Spanish translations 2025-11-27 00:50:59 +05:30
RitvikSardana
d15fefd620 Merge pull request #2761 from pratikb64/fix/mandatory-feedback-bug
fix: mandatory feedback bug
2025-11-27 00:29:33 +05:30
RitvikSardana
3873e3e3dd Merge pull request #2765 from pratikb64/fix/ticket-reply-comment-bug
fix: auto closing bug in reply & comment section
2025-11-27 00:27:23 +05:30
RitvikSardana
997be470d1 Merge branch 'develop' into fix/ticket-reply-comment-bug 2025-11-27 00:22:35 +05:30
RitvikSardana
41fc7dd672 Merge pull request #2768 from KerollesFathy/fix-publishing-article-without-content
fix: Validate that published HD articles contain content
2025-11-27 00:14:18 +05:30
RitvikSardana
ffee882478 Merge pull request #2758 from frappe/pot_develop_2025-11-16
chore: update POT file
2025-11-27 00:10:27 +05:30
RitvikSardana
36783ef818 Merge branch 'develop' into pot_develop_2025-11-16 2025-11-27 00:10:17 +05:30
RitvikSardana
8f813cc6b6 Merge pull request #2751 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-27 00:09:14 +05:30
RitvikSardana
7dfc99b08a Merge pull request #2766 from frappe/pot_develop_2025-11-23
chore: update POT file
2025-11-27 00:08:30 +05:30
RitvikSardana
f052decc10 Merge pull request #2719 from frappe/helpdesk-cleanup
refactor: reduce bundle size
2025-11-27 00:05:00 +05:30
Ritvik Sardana
f386c33a09 fix: update frappe-ui version 2025-11-26 18:52:10 +05:30
Ritvik Sardana
b457646087 fix: change PWA version 2025-11-26 17:51:07 +05:30
Pratik
77eaa45ee7 fix: revert changes & add ignore args 2025-11-26 12:20:50 +00:00
Pratik
015bb01599 fix: remove feedback field in validation 2025-11-26 11:59:57 +00:00
Ritvik Sardana
d14fc7dd5f fix: resolve for node 18 2025-11-26 17:11:52 +05:30
Ritvik Sardana
88acdd0a4f chore: update frappe-ui version 2025-11-26 16:10:56 +05:30
RitvikSardana
660fd178b9 chore: Slovenian translations 2025-11-26 00:22:55 +05:30
RitvikSardana
6c29b677cb chore: Portuguese, Brazilian translations 2025-11-26 00:22:47 +05:30
Ritvik Sardana
09ef6a1d18 chore: code cleanup 2025-11-25 16:35:06 +05:30
Ritvik Sardana
98c8eb7445 fix: update frappe-ui version 2025-11-25 15:49:43 +05:30
Ritvik Sardana
15fdea2cd1 fix: frappe-ui changes in sidebar 2025-11-25 14:38:48 +05:30
Ritvik Sardana
97a3be2fc7 fix: change frappe-ui commit hash 2025-11-25 13:30:20 +05:30
Ritvik Sardana
538674f68b fix: change frappe-ui commit hash 2025-11-25 12:28:12 +05:30
Ritvik Sardana
c9a6a28af7 fix: incorporate new changes 2025-11-25 01:38:59 +05:30
RitvikSardana
530340a01f chore: Slovenian translations 2025-11-25 00:23:56 +05:30
RitvikSardana
6f104d56f4 chore: French translations 2025-11-25 00:23:55 +05:30
RitvikSardana
5384766355 Merge pull request #2664 from pratikb64/feat/add-pages-in-settings
feat: add profile, general settings module & search bars for lists
2025-11-24 16:40:54 +05:30
Ritvik Sardana
33ff264029 chore(list-view): dont show border-b for last element 2025-11-24 16:39:43 +05:30
KerollesFathy
356c88bc1f fix: validate published articles have content 2025-11-23 21:47:01 +00:00
RitvikSardana
9e66c34d99 chore: Slovenian translations 2025-11-24 00:17:57 +05:30
frappe-pr-bot
6fcae817d4 chore: update POT file 2025-11-23 10:36:38 +00:00
RitvikSardana
144f1b9f6a chore: Slovenian translations 2025-11-23 00:13:18 +05:30
RitvikSardana
e93b3d3912 chore: Slovenian translations 2025-11-22 00:14:54 +05:30
RitvikSardana
b922219bb6 chore: Slovenian translations 2025-11-20 23:15:44 +05:30
RitvikSardana
73adb17b98 chore: Indonesian translations 2025-11-20 23:15:36 +05:30
RitvikSardana
2157d27cbe chore: Portuguese, Brazilian translations 2025-11-20 23:15:35 +05:30
RitvikSardana
4b0c61fa16 chore: Spanish translations 2025-11-20 23:15:23 +05:30
Pratik
e197993dbf fix: auto closing bug in reply & comment section 2025-11-20 09:56:57 +00:00
RitvikSardana
f4670c5fb2 chore: Persian translations 2025-11-19 23:14:51 +05:30
Pratik
86b4440149 fix: mandatory feedback bug 2025-11-18 05:18:17 +00:00
RitvikSardana
ce7ccf1730 chore: French translations 2025-11-16 21:46:06 +05:30
RitvikSardana
4d7601a527 chore: Serbian (Latin) translations 2025-11-16 21:46:05 +05:30
RitvikSardana
6be1784277 chore: Norwegian Bokmal translations 2025-11-16 21:46:04 +05:30
RitvikSardana
c177472a42 chore: Bosnian translations 2025-11-16 21:46:02 +05:30
RitvikSardana
7f80b00860 chore: Croatian translations 2025-11-16 21:46:01 +05:30
RitvikSardana
fd74e6311b chore: Persian translations 2025-11-16 21:45:58 +05:30
RitvikSardana
64b8991408 chore: Indonesian translations 2025-11-16 21:45:57 +05:30
RitvikSardana
ea37dcfc3a chore: Chinese Simplified translations 2025-11-16 21:45:54 +05:30
RitvikSardana
6be74cad34 chore: Turkish translations 2025-11-16 21:45:53 +05:30
RitvikSardana
03556b02b5 chore: Swedish translations 2025-11-16 21:45:52 +05:30
RitvikSardana
a7f42d9d14 chore: Serbian (Cyrillic) translations 2025-11-16 21:45:51 +05:30
RitvikSardana
a83e75a4d3 chore: Polish translations 2025-11-16 21:45:48 +05:30
RitvikSardana
afb5f2cb34 chore: German translations 2025-11-16 21:45:45 +05:30
RitvikSardana
ff30d7d318 chore: Arabic translations 2025-11-16 21:45:43 +05:30
RitvikSardana
952fe5daa0 chore: Spanish translations 2025-11-16 21:45:42 +05:30
frappe-pr-bot
75647b121d chore: update POT file 2025-11-16 10:36:33 +00:00
Pratik
36fb1a5d67 fix: hover state 2025-11-14 11:25:15 +00:00
Pratik
0b6f7b953e fix: agents list filter hover state 2025-11-14 10:06:51 +00:00
Pratik Badhe
7da438efa9 Merge branch 'develop' into feat/add-pages-in-settings 2025-11-14 10:52:17 +05:30
Pratik
f69cd6fe84 chore: code cleanup 2025-11-14 05:12:30 +00:00
RitvikSardana
ffe1f27000 Merge pull request #2752 from rehanrehman389/fix-view
fix: prevent emoji clipping
2025-11-13 21:56:50 +05:30
RitvikSardana
caaf65d63b chore: German translations 2025-11-13 21:04:13 +05:30
Rehan Ansari
e6b221da38 fix: prevent emoji clipping 2025-11-12 21:15:46 +05:30
RitvikSardana
c813319260 chore: Persian translations 2025-11-12 20:47:23 +05:30
Pratik
11a2032b1f chore: fix description 2025-11-12 10:22:43 +00:00
Pratik
b49ba83115 fix: dropdown bug & labels 2025-11-12 11:07:07 +05:30
RitvikSardana
19fa45a864 Merge pull request #2746 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-12 02:29:03 +05:30
RitvikSardana
c60636e65d chore: Bosnian translations 2025-11-11 20:48:59 +05:30
RitvikSardana
64876b97b3 chore: Croatian translations 2025-11-11 20:48:58 +05:30
RitvikSardana
6544352f17 chore: Swedish translations 2025-11-11 20:48:57 +05:30
RitvikSardana
7d363bc811 Merge pull request #2747 from RitvikSardana/raising-ticket
fix: parse API Options correctly while creating tickets
2025-11-11 12:31:55 +05:30
Ritvik Sardana
bf144300ba fix: parse API Options correctly while creating tickets 2025-11-11 12:24:48 +05:30
RitvikSardana
5d3155301e chore: French translations 2025-11-10 19:59:19 +05:30
RitvikSardana
8eae98f9fb chore: Serbian (Latin) translations 2025-11-10 19:59:18 +05:30
RitvikSardana
64730f9c10 chore: Norwegian Bokmal translations 2025-11-10 19:59:17 +05:30
RitvikSardana
4697af4d7b chore: Bosnian translations 2025-11-10 19:59:16 +05:30
RitvikSardana
76f45d5ac0 chore: Croatian translations 2025-11-10 19:59:13 +05:30
RitvikSardana
347205c866 chore: Persian translations 2025-11-10 19:59:10 +05:30
RitvikSardana
13ea1ffc8c chore: Indonesian translations 2025-11-10 19:59:09 +05:30
RitvikSardana
928a9391d0 chore: Portuguese, Brazilian translations 2025-11-10 19:59:08 +05:30
RitvikSardana
240651564a chore: Chinese Simplified translations 2025-11-10 19:59:05 +05:30
RitvikSardana
c2a68b6a6b chore: Turkish translations 2025-11-10 19:59:04 +05:30
RitvikSardana
5df19be765 chore: Swedish translations 2025-11-10 19:59:02 +05:30
RitvikSardana
df85bb2111 chore: Serbian (Cyrillic) translations 2025-11-10 19:59:01 +05:30
RitvikSardana
df1adc6c6a chore: Portuguese translations 2025-11-10 19:58:59 +05:30
RitvikSardana
601b674f8d chore: Polish translations 2025-11-10 19:58:57 +05:30
RitvikSardana
79100fe489 chore: Dutch translations 2025-11-10 19:58:56 +05:30
RitvikSardana
b0a8f48dae chore: Italian translations 2025-11-10 19:58:55 +05:30
RitvikSardana
2a713fa2a4 chore: German translations 2025-11-10 19:58:53 +05:30
RitvikSardana
abb2c8b8df chore: Danish translations 2025-11-10 19:58:52 +05:30
RitvikSardana
b016726247 chore: Arabic translations 2025-11-10 19:58:50 +05:30
RitvikSardana
b019883b91 chore: Spanish translations 2025-11-10 19:58:49 +05:30
RitvikSardana
7b4bb82c75 Merge pull request #2743 from RitvikSardana/api-options-null
fix: filter out null/undefined values in apiOptions
2025-11-10 15:47:57 +05:30
Ritvik Sardana
03583e44d5 chore: code cleanup 2025-11-10 15:46:41 +05:30
Ritvik Sardana
4bb8b7695c fix: filter out null/undefined values in apiOptions 2025-11-10 15:42:44 +05:30
RitvikSardana
d375724f9e Merge pull request #2741 from RitvikSardana/reply-error
fix: validation error when replying on a ticket
2025-11-10 15:31:24 +05:30
Ritvik Sardana
643155e4ec fix: validation error when replying on a ticket 2025-11-10 15:23:53 +05:30
Pratik
5b248ce379 fix: spacing in agents list view dropdowns 2025-11-10 07:52:35 +00:00
RitvikSardana
41288cc957 Merge pull request #2715 from KerollesFathy/refactor-apiOptions-on-UniInput
refactor: update apiOptions transform to map object labels and values
2025-11-09 21:33:50 +05:30
RitvikSardana
e12dc083e7 Merge pull request #2737 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-09 20:49:41 +05:30
Pratik
5a0a632ae3 chore: add Dropdown component imports 2025-11-09 15:17:50 +00:00
RitvikSardana
3d22970159 chore: French translations 2025-11-09 19:42:03 +05:30
RitvikSardana
4d39b43f15 chore: Esperanto translations 2025-11-09 19:42:01 +05:30
RitvikSardana
5489209a0d chore: Serbian (Latin) translations 2025-11-09 19:42:00 +05:30
RitvikSardana
4845183691 chore: Norwegian Bokmal translations 2025-11-09 19:41:59 +05:30
RitvikSardana
e17d9f0123 chore: Bosnian translations 2025-11-09 19:41:58 +05:30
RitvikSardana
30f4654d0f chore: Burmese translations 2025-11-09 19:41:57 +05:30
RitvikSardana
6b6dacea76 chore: Croatian translations 2025-11-09 19:41:56 +05:30
RitvikSardana
0594cba992 chore: Thai translations 2025-11-09 19:41:54 +05:30
RitvikSardana
1cfcbd964b chore: Tamil translations 2025-11-09 19:41:53 +05:30
RitvikSardana
cffd32dda9 chore: Persian translations 2025-11-09 19:41:52 +05:30
RitvikSardana
7e8071f33c chore: Indonesian translations 2025-11-09 19:41:51 +05:30
RitvikSardana
926e21d258 chore: Portuguese, Brazilian translations 2025-11-09 19:41:50 +05:30
RitvikSardana
8af08fe0fa chore: Vietnamese translations 2025-11-09 19:41:49 +05:30
RitvikSardana
0826769b62 chore: Chinese Simplified translations 2025-11-09 19:41:48 +05:30
RitvikSardana
f35457bdb0 chore: Turkish translations 2025-11-09 19:41:46 +05:30
RitvikSardana
6bc303f434 chore: Swedish translations 2025-11-09 19:41:45 +05:30
RitvikSardana
12050328a2 chore: Serbian (Cyrillic) translations 2025-11-09 19:41:44 +05:30
RitvikSardana
5ec5c53024 chore: Russian translations 2025-11-09 19:41:43 +05:30
RitvikSardana
0388bcaf45 chore: Portuguese translations 2025-11-09 19:41:42 +05:30
RitvikSardana
50faed905e chore: Polish translations 2025-11-09 19:41:40 +05:30
RitvikSardana
b97d1e8edc chore: Dutch translations 2025-11-09 19:41:39 +05:30
RitvikSardana
699ce06906 chore: Italian translations 2025-11-09 19:41:38 +05:30
RitvikSardana
28bf477422 chore: Hungarian translations 2025-11-09 19:41:37 +05:30
RitvikSardana
10c206c37f chore: German translations 2025-11-09 19:41:35 +05:30
RitvikSardana
506a5619cb chore: Danish translations 2025-11-09 19:41:34 +05:30
RitvikSardana
56e4a8fa27 chore: Czech translations 2025-11-09 19:41:33 +05:30
RitvikSardana
387e6f65ea chore: Arabic translations 2025-11-09 19:41:32 +05:30
RitvikSardana
6e94d7668e chore: Spanish translations 2025-11-09 19:41:31 +05:30
RitvikSardana
13be2daa16 Merge pull request #2733 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-09 17:55:08 +05:30
RitvikSardana
5bdf0998e3 Merge pull request #2734 from frappe/pot_develop_2025-11-09
chore: update POT file
2025-11-09 17:54:54 +05:30
frappe-pr-bot
ce4f422335 chore: update POT file 2025-11-09 10:36:13 +00:00
RitvikSardana
fbf5bed567 chore: Esperanto translations 2025-11-08 19:15:37 +05:30
RitvikSardana
b7dcf6324c chore: Serbian (Latin) translations 2025-11-08 19:15:36 +05:30
RitvikSardana
fe25d084d9 chore: Norwegian Bokmal translations 2025-11-08 19:15:35 +05:30
RitvikSardana
cb329e9747 chore: Bosnian translations 2025-11-08 19:15:33 +05:30
RitvikSardana
f5720fba05 chore: Burmese translations 2025-11-08 19:15:32 +05:30
RitvikSardana
0745ddf8cd chore: Croatian translations 2025-11-08 19:15:31 +05:30
RitvikSardana
4a6ce11692 chore: Thai translations 2025-11-08 19:15:29 +05:30
RitvikSardana
bed330a7db chore: Tamil translations 2025-11-08 19:15:28 +05:30
RitvikSardana
b939caaf68 chore: Persian translations 2025-11-08 19:15:27 +05:30
RitvikSardana
dae531dd26 chore: Indonesian translations 2025-11-08 19:15:26 +05:30
RitvikSardana
84af9ba07f chore: Portuguese, Brazilian translations 2025-11-08 19:15:24 +05:30
RitvikSardana
d32244a877 chore: Vietnamese translations 2025-11-08 19:15:23 +05:30
RitvikSardana
f2b5213e6e chore: Chinese Simplified translations 2025-11-08 19:15:22 +05:30
RitvikSardana
487ea74b51 chore: Turkish translations 2025-11-08 19:15:21 +05:30
RitvikSardana
124a1a6911 chore: Swedish translations 2025-11-08 19:15:19 +05:30
RitvikSardana
2e85998e72 chore: Serbian (Cyrillic) translations 2025-11-08 19:15:18 +05:30
RitvikSardana
83cefc54a1 chore: Russian translations 2025-11-08 19:15:17 +05:30
RitvikSardana
fb42f359a1 chore: Portuguese translations 2025-11-08 19:15:16 +05:30
RitvikSardana
0334db5ef9 chore: Polish translations 2025-11-08 19:15:15 +05:30
RitvikSardana
305ef5e560 chore: Dutch translations 2025-11-08 19:15:14 +05:30
RitvikSardana
a4f3d1d5b4 chore: Italian translations 2025-11-08 19:15:12 +05:30
RitvikSardana
16f72e287a chore: Hungarian translations 2025-11-08 19:15:11 +05:30
RitvikSardana
de04086da7 chore: German translations 2025-11-08 19:15:10 +05:30
RitvikSardana
557bd6a498 chore: Danish translations 2025-11-08 19:15:09 +05:30
RitvikSardana
4b0704060b chore: Czech translations 2025-11-08 19:15:08 +05:30
RitvikSardana
30debfc42b chore: Arabic translations 2025-11-08 19:15:07 +05:30
RitvikSardana
23568e9b97 chore: Spanish translations 2025-11-08 19:15:06 +05:30
RitvikSardana
2e0eb5033b Merge pull request #2730 from RitvikSardana/bump-new
chore: bump version
2025-11-07 17:00:35 +05:30
Ritvik Sardana
260d15de65 chore: bump version 2025-11-07 17:00:07 +05:30
RitvikSardana
aaaac02979 Merge pull request #2705 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-11-07 16:52:42 +05:30
RitvikSardana
fea1f8f7ed Merge pull request #2721 from frappe/pot_develop_2025-11-02
chore: update POT file
2025-11-07 16:51:46 +05:30
RitvikSardana
b442122556 Merge pull request #2726 from RitvikSardana/fix-email-feedback
fix: remove unnecessary validation while giving feedback for email tickets
2025-11-07 16:51:17 +05:30
Ritvik Sardana
f054d9e209 fix: remove unnecessary validation while giving feedback for email tickets 2025-11-07 16:41:47 +05:30
Ritvik Sardana
0e84f92d32 chore: update git ignore file 2025-11-07 13:28:57 +05:30
Ritvik Sardana
6f02b27ff6 fix: frappe-ui tree shaking compatible 2025-11-07 11:47:08 +05:30
Pratik
42e1f436e5 fix: button label & use symbol for inject/provide 2025-11-06 06:34:48 +00:00
Pratik
f684bfba27 fix: assignee search bug 2025-11-04 04:32:44 +00:00
Pratik
feb93e3459 feat: add ticket based restriction logic 2025-11-03 13:35:57 +00:00
RitvikSardana
01dd4fb42c chore: Chinese Simplified translations 2025-11-03 17:01:23 +05:30
Pratik
e91c510cbf feat: add language setting 2025-11-03 10:31:18 +00:00
Pratik
afe0899dd8 chore: CI lint fix 2025-11-03 09:15:54 +00:00
Pratik Badhe
91c1f97912 Merge branch frappe:develop into feat/add-pages-in-settings 2025-11-03 14:41:06 +05:30
Ritvik Sardana
580ac33487 chore: bump frappe-ui version 2025-11-03 13:27:09 +05:30
Pratik
d5fa63428a fix: ui cleanup & move SettingsLayoutBase component 2025-11-02 15:15:48 +00:00
frappe-pr-bot
7c29161b3b chore: update POT file 2025-11-02 10:35:41 +00:00
Ritvik Sardana
020bd77358 chore: update yarn.lock 2025-11-01 13:19:01 +05:30
Ritvik Sardana
511fa074b4 fix: remove visualizer 2025-11-01 13:11:40 +05:30
Ritvik Sardana
01175b0fc8 refactor: reduce bundle size 2025-11-01 13:05:42 +05:30
RitvikSardana
793f1262e7 Merge pull request #2716 from RitvikSardana/delete-doc
fix: remove index delete from hooks
2025-10-31 13:01:23 +05:30
Ritvik Sardana
84fce9936e fix: remove index delete from hooks 2025-10-31 11:58:03 +05:30
KerollesFathy
f06862057f refactor: update apiOptions transform to handle object labels and values 2025-10-30 19:12:25 +00:00
RitvikSardana
75799fa4a2 chore: Serbian (Latin) translations 2025-10-30 14:37:38 +05:30
RitvikSardana
7fdcc42626 chore: Serbian (Cyrillic) translations 2025-10-30 14:37:37 +05:30
RitvikSardana
c521227e4d chore: version bump 2025-10-30 10:42:52 +05:30
Pratik Badhe
646efa0b6e chore: code cleanup 2025-10-30 05:10:34 +00:00
RitvikSardana
e460df11d2 Merge pull request #2710 from pratikb64/fix/undefined-ref 2025-10-30 10:28:47 +05:30
Pratik Badhe
7a1545ef1b fix: undefined ref 2025-10-30 04:55:20 +00:00
Pratik Badhe
ade64594f5 Merge remote-tracking branch 'origin/develop' into refactor/canned-responses 2025-10-29 12:29:34 +00:00
Pratik Badhe
11f01c05bb feat: add load more button 2025-10-29 12:05:02 +00:00
Pratik Badhe
73c4d5d62d fix: handle case when user does not have team assigned 2025-10-29 11:43:34 +00:00
RitvikSardana
278012c200 Merge pull request #2706 from RitvikSardana/assign-perm
fix(perm): agent should see the assigned tickets
2025-10-29 17:04:36 +05:30
Ritvik Sardana
38d0a0b76e fix(search): add ref ticket only if doc is HD Ticket 2025-10-29 16:14:41 +05:30
Ritvik Sardana
40f0032380 fix: type coercion while setting value 2025-10-29 16:01:31 +05:30
Ritvik Sardana
f564284d96 fix: show ticket to agent is they are assigned to it 2025-10-29 14:25:13 +05:30
RitvikSardana
2a15bfb7e2 chore: Esperanto translations 2025-10-29 14:09:44 +05:30
RitvikSardana
0722e819c3 chore: Burmese translations 2025-10-29 14:09:42 +05:30
RitvikSardana
5f49a5afd1 chore: Tamil translations 2025-10-29 14:09:41 +05:30
RitvikSardana
a3254586e5 chore: Serbian (Latin) translations 2025-10-29 14:09:40 +05:30
RitvikSardana
04c4a57141 chore: Norwegian Bokmal translations 2025-10-29 14:09:39 +05:30
RitvikSardana
d7fd5cda70 chore: Bosnian translations 2025-10-29 14:09:38 +05:30
RitvikSardana
9850e2da85 chore: Croatian translations 2025-10-29 14:09:37 +05:30
RitvikSardana
a0b7fab8da chore: Thai translations 2025-10-29 14:09:35 +05:30
RitvikSardana
96fbb18677 chore: Persian translations 2025-10-29 14:09:34 +05:30
RitvikSardana
9b10275f36 chore: Indonesian translations 2025-10-29 14:09:33 +05:30
RitvikSardana
cb1000f32d chore: Portuguese, Brazilian translations 2025-10-29 14:09:32 +05:30
RitvikSardana
c0c9bf2cae chore: Vietnamese translations 2025-10-29 14:09:31 +05:30
RitvikSardana
1b64db4792 chore: Chinese Simplified translations 2025-10-29 14:09:29 +05:30
RitvikSardana
7631e7f9a4 chore: Turkish translations 2025-10-29 14:09:28 +05:30
RitvikSardana
39f26f0050 chore: Swedish translations 2025-10-29 14:09:27 +05:30
RitvikSardana
2c67d72e4f chore: Serbian (Cyrillic) translations 2025-10-29 14:09:26 +05:30
RitvikSardana
006ead4074 chore: Russian translations 2025-10-29 14:09:25 +05:30
RitvikSardana
ee57aa5fb0 chore: Portuguese translations 2025-10-29 14:09:23 +05:30
RitvikSardana
aec5d229eb chore: Polish translations 2025-10-29 14:09:22 +05:30
RitvikSardana
3ecc905ae8 chore: Dutch translations 2025-10-29 14:09:21 +05:30
RitvikSardana
e1bb325c03 chore: Italian translations 2025-10-29 14:09:20 +05:30
RitvikSardana
037ae628a1 chore: Hungarian translations 2025-10-29 14:09:18 +05:30
RitvikSardana
8af498aa8b chore: German translations 2025-10-29 14:09:17 +05:30
RitvikSardana
3adddec261 chore: Danish translations 2025-10-29 14:09:16 +05:30
RitvikSardana
4225f50f0c chore: Czech translations 2025-10-29 14:09:15 +05:30
RitvikSardana
ef8b183c88 chore: Arabic translations 2025-10-29 14:09:13 +05:30
RitvikSardana
682a25a677 chore: Spanish translations 2025-10-29 14:09:12 +05:30
RitvikSardana
21c3a00c78 chore: French translations 2025-10-29 14:09:11 +05:30
RitvikSardana
be3eeb0248 Merge pull request #2576 from pratikb64/fix/telephony-bugs
fix: tab url hash & make a call button
2025-10-29 12:28:32 +05:30
RitvikSardana
571869adbb Merge branch 'develop' into fix/telephony-bugs 2025-10-29 12:22:31 +05:30
RitvikSardana
a4401fb9f2 Merge pull request #2689 from pratikb64/fix/reply-all-cc-bug
fix: handle edge case for cc & to field
2025-10-29 12:11:05 +05:30
Pratik Badhe
6625efb3a1 fix: remove i18n for week days 2025-10-29 05:40:59 +00:00
Pratik Badhe
d920c03a71 fix: ui alignment 2025-10-29 05:36:11 +00:00
RitvikSardana
3c32e12cdd Merge pull request #2699 from RitvikSardana/version-bump
chore: bump version
2025-10-29 10:45:14 +05:30
Ritvik Sardana
c583974b74 chore: update yarn.lock 2025-10-28 19:41:57 +05:30
Ritvik Sardana
295c1fded4 chore: bump version 2025-10-28 19:36:17 +05:30
RitvikSardana
bcf73ffa3a Merge pull request #2678 from frappe/pot_develop_2025-10-19
chore: update POT file
2025-10-28 18:31:10 +05:30
RitvikSardana
85ded64073 Merge pull request #2679 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-28 18:30:43 +05:30
RitvikSardana
01d1d358a6 Merge branch 'develop' into pot_develop_2025-10-19 2025-10-28 18:29:37 +05:30
RitvikSardana
0b4c744a00 Merge pull request #2687 from frappe/pot_develop_2025-10-26
chore: update POT file
2025-10-28 18:28:58 +05:30
RitvikSardana
42b868a9a3 Merge pull request #2657 from pratikb64/fix/sla-json-edge-case
fix: handle json parse error & json validation on doctype save
2025-10-28 18:27:48 +05:30
RitvikSardana
9257d76dd9 Merge pull request #2688 from siduck/develop
fix: prevent usermenu button overflow in sidebar
2025-10-28 18:17:44 +05:30
Pratik
98f01651f0 feat: add scope & improve UI 2025-10-28 17:25:05 +05:30
RitvikSardana
f851d2720e Merge pull request #2690 from RitvikSardana/int-field-support
fix: minor fixes
2025-10-28 16:19:33 +05:30
Ritvik Sardana
652188cc64 fix(sqlite_search): convert to int only when doctype is HD Ticket 2025-10-28 13:42:26 +05:30
Ritvik Sardana
943da1be93 fix: add handling bad_emails in process_mail 2025-10-28 13:02:29 +05:30
Ritvik Sardana
d7225a82c0 fix: decoding the receiving email properly with try except 2025-10-28 12:39:58 +05:30
Ritvik Sardana
a25c9c0a8a fix: dont add build_index function in hooks.py of helpdesk 2025-10-28 11:57:40 +05:30
Ritvik Sardana
e64fe01ed8 chore: cleanup 2025-10-28 11:44:43 +05:30
Ritvik Sardana
9e1d493ef5 fix: add support for numberFields in side panel 2025-10-28 11:36:15 +05:30
Ritvik Sardana
8b1bd75348 fix: remove unwanted imports 2025-10-28 11:28:17 +05:30
RitvikSardana
1c33ed571a Merge pull request #2676 from RitvikSardana/shortcuts
feat: add Shortcuts to Ticket View
2025-10-28 11:13:06 +05:30
Ritvik Sardana
834c557a2d fix: delete search index when a ticket is deleted 2025-10-28 11:04:02 +05:30
Ritvik Sardana
86a5cf8344 fix: add shortcuts only in Agent Portal 2025-10-28 11:00:23 +05:30
Ritvik Sardana
114f5cd821 chore: code cleanup 2025-10-28 10:52:16 +05:30
Ritvik Sardana
d96f9ef46b Merge remote-tracking branch 'upstream/develop' into shortcuts 2025-10-28 10:51:03 +05:30
Ritvik Sardana
0f90693439 chore: code cleanup 2025-10-27 18:48:19 +05:30
Pratik Badhe
fc45408d77 fix: order list view's by modified desc 2025-10-27 11:23:46 +00:00
Pratik Badhe
ffbb1c5e8f fix: handle edge case for cc & to field 2025-10-27 10:54:23 +00:00
Pratik Badhe
0b87316f2c feat: add teams agents selector 2025-10-27 09:51:51 +00:00
Ritvik Sardana
395969ded9 fix: add shortcut for showing help panel 2025-10-27 15:17:56 +05:30
siduck
856a161164 fix: prevent usermenu button overflow in sidebar
Related to https://github.com/frappe/frappe-ui/issues/298
2025-10-27 14:40:08 +05:30
Ritvik Sardana
459f7a7f39 fix: handle special cases in shortcuts 2025-10-27 14:12:09 +05:30
Pratik Badhe
a2261ece1d fix: remove subject & add toolbar for response editor 2025-10-26 11:48:25 +00:00
frappe-pr-bot
da4008d6ab chore: update POT file 2025-10-26 10:35:56 +00:00
Ritvik Sardana
8a73fb4467 fix: persist redirect URL 2025-10-26 14:59:03 +05:30
RitvikSardana
5300f24b10 chore: Polish translations 2025-10-26 13:33:06 +05:30
Pratik Badhe
4f0ef90eb6 chore: add i18n 2025-10-24 07:24:00 +00:00
Pratik Badhe
f2b04aefe6 ci: remove frappe db commit 2025-10-23 14:21:27 +00:00
Pratik Badhe
dc8a452c14 fix: CI lint error 2025-10-23 14:15:33 +00:00
Pratik Badhe
039c968cd8 refactor: add common settings layout base component 2025-10-23 14:12:22 +00:00
Ritvik Sardana
521b9182a7 chore: UI cleanup 2025-10-23 12:26:35 +05:30
Ritvik Sardana
cf0b0d7c4f chore: code cleanup 2025-10-23 01:06:25 +05:30
Ritvik Sardana
e0a2f29c68 refactor: shortcut composable 2025-10-22 17:51:21 +05:30
Ritvik Sardana
eb452b21eb fix: UI for shortcuts 2025-10-22 14:16:17 +05:30
Ritvik Sardana
5f66778e45 fix: add proper handling to remove key bindings 2025-10-22 12:34:23 +05:30
RitvikSardana
03bf47d082 chore: Polish translations 2025-10-21 11:12:31 +05:30
RitvikSardana
e2ed80369c chore: Persian translations 2025-10-20 10:23:48 +05:30
Pratik Badhe
16c2e2a958 refactor: move canned responses into settings modal 2025-10-19 12:51:01 +00:00
frappe-pr-bot
012cfd179b chore: update POT file 2025-10-19 10:35:53 +00:00
Ritvik Sardana
211908f6de refactor: shortcut handling 2025-10-17 18:21:59 +05:30
Ritvik Sardana
8a5b57d5de fix: handle delete_doc in sqlite search 2025-10-17 16:41:18 +05:30
Ritvik Sardana
0c94dbbbfe fix: replace old shortcuts with new API 2025-10-17 16:40:58 +05:30
Ritvik Sardana
f563d331b0 fix: add shortcuts for ticket navigation 2025-10-17 16:18:58 +05:30
Ritvik Sardana
e11d78a0c8 fix: add shortcuts for reply&comment 2025-10-17 14:44:56 +05:30
Ritvik Sardana
600794dc8d fix: add shortcuts for core fields 2025-10-17 13:27:09 +05:30
Pratik Badhe
43c5e96df1 chore: add i18n and code cleanup 2025-10-16 11:49:39 +00:00
Pratik Badhe
285f490ebf fix: improve telephony dirty state logic 2025-10-16 11:15:31 +00:00
Pratik Badhe
0c2a1df6fd refactor: change loading state handling 2025-10-16 10:32:29 +00:00
Ritvik Sardana
d484c77904 fix: better way to disable shortcuts 2025-10-16 16:00:59 +05:30
Pratik Badhe
8848e49dab fix: favicon icon 2025-10-16 10:23:18 +00:00
Pratik Badhe
37fa7d19e9 refactor: remove some setting options & add i18n 2025-10-16 08:04:02 +00:00
Pratik Badhe
f3f89188ee fix: edge case where tab wont change if call is disabled 2025-10-15 10:59:11 +00:00
RitvikSardana
b95d95f440 Merge pull request #2667 from frappe/pot_develop_2025-10-12
chore: update POT file
2025-10-15 15:20:28 +05:30
RitvikSardana
46465da35a Merge pull request #2652 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-15 15:19:30 +05:30
RitvikSardana
bbbce958f0 Merge pull request #2672 from flaviacastro/chore/translate-dashboards
chore: translate dashboards
2025-10-15 15:01:49 +05:30
RitvikSardana
7f2b33ff36 Merge pull request #2668 from pratikb64/fix/response-resolution-time-popover
fix: tooltip for response and resolution time
2025-10-15 15:01:31 +05:30
Pratik Badhe
7cc23c8caa fix: reset tab state on navigation 2025-10-15 09:29:56 +00:00
Ritvik Sardana
2b62a32f55 chore: format error message in assignment rule 2025-10-15 11:52:54 +05:30
RitvikSardana
48399d258d Merge pull request #2648 from pratikb64/fix/frappe-ui-fix
fix: frappe ui z index issues
2025-10-15 11:43:13 +05:30
Pratik Badhe
417126f22f fix: semgrep lint fix 2025-10-15 05:44:04 +00:00
Pratik Badhe
9a2476155d fix: semgrep lint fix 2025-10-15 05:41:09 +00:00
Pratik Badhe
bdbf4a6408 refactor: improve assignment rule invite agent handling 2025-10-15 05:34:25 +00:00
flaviacastro
9969cb5874 chore: translate dashboards 2025-10-14 20:13:19 -03:00
Pratik Badhe
652c903499 fix: internationalization & state re-rendering 2025-10-13 14:25:20 +00:00
Pratik Badhe
4475450603 fix: tooltip for response and resolution time 2025-10-13 09:59:32 +00:00
RitvikSardana
f9158f4bcf chore: Serbian (Latin) translations 2025-10-13 07:45:18 +05:30
RitvikSardana
246044776f chore: Serbian (Cyrillic) translations 2025-10-13 07:45:10 +05:30
RitvikSardana
25f33001ae chore: Polish translations 2025-10-13 07:45:07 +05:30
Pratik Badhe
c0a10dc003 fix: agent flow test 2025-10-12 17:04:29 +00:00
Pratik Badhe
a904779edd fix: improve team edit 2025-10-12 15:55:17 +00:00
Pratik Badhe
3e3979cf65 fix: add empty state for team edit page 2025-10-12 13:46:21 +00:00
frappe-pr-bot
f6eec48671 chore: update POT file 2025-10-12 10:35:11 +00:00
RitvikSardana
de096bf1bc chore: Polish translations 2025-10-12 07:40:48 +05:30
RitvikSardana
aac3b105d9 chore: Burmese translations 2025-10-11 07:43:15 +05:30
RitvikSardana
98e4852f43 chore: Tamil translations 2025-10-11 07:43:14 +05:30
RitvikSardana
f7ede8edb0 chore: Esperanto translations 2025-10-11 07:43:13 +05:30
RitvikSardana
76dc90c563 chore: Serbian (Latin) translations 2025-10-11 07:43:12 +05:30
RitvikSardana
24ab3d735b chore: Norwegian Bokmal translations 2025-10-11 07:43:11 +05:30
RitvikSardana
75348d5266 chore: Bosnian translations 2025-10-11 07:43:10 +05:30
RitvikSardana
037742eacb chore: Croatian translations 2025-10-11 07:43:09 +05:30
RitvikSardana
31ac3b4bbb chore: Thai translations 2025-10-11 07:43:07 +05:30
RitvikSardana
3afcd2f43c chore: Persian translations 2025-10-11 07:43:06 +05:30
RitvikSardana
db71ab4744 chore: Indonesian translations 2025-10-11 07:43:05 +05:30
RitvikSardana
b6fb4a1024 chore: Portuguese, Brazilian translations 2025-10-11 07:43:04 +05:30
RitvikSardana
11da05c81e chore: Vietnamese translations 2025-10-11 07:43:03 +05:30
RitvikSardana
95dcd882db chore: Chinese Simplified translations 2025-10-11 07:43:02 +05:30
RitvikSardana
6b010d34d4 chore: Turkish translations 2025-10-11 07:43:01 +05:30
RitvikSardana
e4f0f7ace0 chore: Swedish translations 2025-10-11 07:43:00 +05:30
RitvikSardana
42b89e65cf chore: Serbian (Cyrillic) translations 2025-10-11 07:42:58 +05:30
RitvikSardana
131f03a0f6 chore: Russian translations 2025-10-11 07:42:57 +05:30
RitvikSardana
d667d15ee9 chore: Portuguese translations 2025-10-11 07:42:56 +05:30
RitvikSardana
f3cc7920bd chore: Polish translations 2025-10-11 07:42:55 +05:30
RitvikSardana
790bbcd772 chore: Dutch translations 2025-10-11 07:42:54 +05:30
RitvikSardana
11a608ead6 chore: Italian translations 2025-10-11 07:42:53 +05:30
RitvikSardana
785e087e95 chore: Hungarian translations 2025-10-11 07:42:52 +05:30
RitvikSardana
0c1ad5e944 chore: German translations 2025-10-11 07:42:50 +05:30
RitvikSardana
29f6a71881 chore: Danish translations 2025-10-11 07:42:49 +05:30
RitvikSardana
04a05dd50b chore: Czech translations 2025-10-11 07:42:48 +05:30
RitvikSardana
2e2ae7e7db chore: Arabic translations 2025-10-11 07:42:47 +05:30
RitvikSardana
23cfb9afd8 chore: Spanish translations 2025-10-11 07:42:46 +05:30
RitvikSardana
ee3d1e2dea chore: French translations 2025-10-11 07:42:45 +05:30
Pratik Badhe
ca2b3fded3 fix: semgrep CI error 2025-10-10 14:35:48 +00:00
Pratik Badhe
770660eefe fix: horizontal line conditional rendering in general settings 2025-10-10 14:28:48 +00:00
Pratik Badhe
7ee3a1b7dd fix: import paths 2025-10-10 12:05:56 +00:00
Pratik Badhe
7b0f9d5c81 Merge branch 'develop' into fix/telephony-bugs 2025-10-10 11:37:58 +00:00
Pratik Badhe
c21247015b fix: handle on hover apps dropdown 2025-10-10 10:16:47 +00:00
Pratik Badhe
aa0c887504 fix: handle long email and name in assignee search 2025-10-10 10:12:12 +00:00
Pratik Badhe
4fd9a38860 fix: bug in assignment rule overwrite handling 2025-10-10 10:09:10 +00:00
Pratik Badhe
fec186af27 Merge branch 'develop' into feat/add-pages-in-settings 2025-10-10 09:57:40 +00:00
Pratik Badhe
6bca4de7b9 fix: layouts & alignments 2025-10-10 09:56:42 +00:00
RitvikSardana
a0df248756 Merge pull request #2649 from frappe/pot_develop_2025-10-05
chore: update POT file
2025-10-10 12:40:44 +05:30
RitvikSardana
e420d1ebf0 Merge pull request #2656 from pratikb64/fix/email-reply
fix: reply all 'To' email bug
2025-10-10 12:40:22 +05:30
RitvikSardana
93a0b55fc1 Merge pull request #2647 from pratikb64/fix/docker-issues
fix: docker bash file
2025-10-10 12:30:57 +05:30
Pratik Badhe
8507c62889 fix: improve logo upload functionality 2025-10-10 06:32:05 +00:00
Pratik Badhe
5aa9d85019 fix: add unsaved changes badge in profile 2025-10-10 06:26:18 +00:00
Pratik Badhe
83a592837a fix: handle roles for settings modal tabs 2025-10-10 06:17:23 +00:00
Ritvik Sardana
c2b8389251 feat: key bindings 2025-10-10 11:27:08 +05:30
Alan
b2b1bf9c5b Merge pull request #2659 from frappe/bump-vueuse
fix: bump vueuse to fix builds
2025-10-10 10:51:44 +05:30
18alantom
4099373b62 fix: bump vueuse to fix builds 2025-10-10 10:45:52 +05:30
Pratik Badhe
dc30039781 fix: profile image upload loading state 2025-10-10 04:49:10 +00:00
Pratik Badhe
cb3c76a55e fix: hide search bar when items are less than 10 2025-10-10 04:23:37 +00:00
Pratik Badhe
37c16f6725 fix: assignment rule new button label 2025-10-09 17:39:14 +00:00
Pratik Badhe
626546b71f fix: logo upload disable state 2025-10-09 17:37:34 +00:00
Pratik Badhe
5abc5e86e4 fix: empty state icon alignment & brand logo, favicon edge case 2025-10-09 17:22:08 +00:00
Pratik Badhe
cab87ec10e fix: agents dropdown placement 2025-10-09 17:04:02 +00:00
Pratik Badhe
910f1b5cbd fix: padding & spacing 2025-10-09 16:54:29 +00:00
Pratik Badhe
718bae37d9 fix: agent name while creating new agent 2025-10-09 14:16:57 +00:00
Pratik Badhe
4778372486 fix: add email account empty state & fix agent before_save hook 2025-10-09 13:38:30 +00:00
Pratik Badhe
6d42461cd6 fix: handle case where agent_name is not available 2025-10-09 13:16:19 +00:00
Pratik Badhe
22d06eea8a fix: assignment rule conditions 2025-10-09 12:01:41 +00:00
Pratik Badhe
6f0467eac4 feat: add profile, general settings page & search bars for lists 2025-10-09 11:26:43 +00:00
Pratik Badhe
5cce62efef fix: show toast if conditions are invalid 2025-10-09 07:49:29 +00:00
Pratik Badhe
746fb39d1c fix: handle json parse error & json validation on doctype save 2025-10-09 07:35:28 +00:00
Pratik Badhe
4636ffab67 fix: reply all 'To' email bug 2025-10-09 05:23:24 +00:00
RitvikSardana
23260de0d7 chore: Burmese translations 2025-10-08 06:01:33 +05:30
RitvikSardana
5959048cae chore: Portuguese, Brazilian translations 2025-10-08 06:01:27 +05:30
Pratik Badhe
95301a2a9f fix: apps list hover state 2025-10-06 11:38:51 +00:00
Pratik Badhe
0175b4e5fc refactor: change apps list UI 2025-10-06 11:25:35 +00:00
frappe-pr-bot
dee4cdf688 chore: update POT file 2025-10-05 10:35:11 +00:00
Pratik Badhe
043780c85d fix: docker bash file 2025-10-03 12:53:40 +00:00
Pratik Badhe
de95204c72 fix: frappe ui z index issues 2025-10-03 10:31:04 +00:00
RitvikSardana
e9ff5a7fd2 Merge pull request #2641 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-03 11:33:53 +05:30
RitvikSardana
8c34993cef Merge pull request #2639 from RitvikSardana/cp-disable
fix: disable command palette when text editor is enabled
2025-10-03 11:33:21 +05:30
RitvikSardana
9afc83e633 chore: Tamil translations 2025-10-02 04:38:56 +05:30
RitvikSardana
9f017522f0 chore: Esperanto translations 2025-10-02 04:38:55 +05:30
RitvikSardana
4bddc7f0ef chore: Serbian (Latin) translations 2025-10-02 04:38:54 +05:30
RitvikSardana
b470a17913 chore: Norwegian Bokmal translations 2025-10-02 04:38:52 +05:30
RitvikSardana
00a2efbfbb chore: Bosnian translations 2025-10-02 04:38:51 +05:30
RitvikSardana
54b1854bdf chore: Croatian translations 2025-10-02 04:38:50 +05:30
RitvikSardana
dbe03e6fd0 chore: Thai translations 2025-10-02 04:38:49 +05:30
RitvikSardana
309c084222 chore: Persian translations 2025-10-02 04:38:48 +05:30
RitvikSardana
51f2a5e428 chore: Indonesian translations 2025-10-02 04:38:47 +05:30
RitvikSardana
5343a006ef chore: Portuguese, Brazilian translations 2025-10-02 04:38:46 +05:30
RitvikSardana
2caf0e94b8 chore: Vietnamese translations 2025-10-02 04:38:45 +05:30
RitvikSardana
8c9eab3d71 chore: Chinese Simplified translations 2025-10-02 04:38:43 +05:30
RitvikSardana
8790567a50 chore: Turkish translations 2025-10-02 04:38:42 +05:30
RitvikSardana
7a75306222 chore: Swedish translations 2025-10-02 04:38:41 +05:30
RitvikSardana
a1e4bb2555 chore: Serbian (Cyrillic) translations 2025-10-02 04:38:40 +05:30
RitvikSardana
0a0fc03f70 chore: Russian translations 2025-10-02 04:38:39 +05:30
RitvikSardana
24b30fb9cb chore: Portuguese translations 2025-10-02 04:38:38 +05:30
RitvikSardana
6c40f9b2a5 chore: Polish translations 2025-10-02 04:38:37 +05:30
RitvikSardana
6e59e71c7c chore: Dutch translations 2025-10-02 04:38:36 +05:30
RitvikSardana
95e10e43a8 chore: Italian translations 2025-10-02 04:38:34 +05:30
RitvikSardana
88396d22a9 chore: Hungarian translations 2025-10-02 04:38:33 +05:30
RitvikSardana
207b8efcc5 chore: German translations 2025-10-02 04:38:32 +05:30
RitvikSardana
309cd9e417 chore: Danish translations 2025-10-02 04:38:31 +05:30
RitvikSardana
69fd0d17ac chore: Czech translations 2025-10-02 04:38:30 +05:30
RitvikSardana
02cf1d1375 chore: Arabic translations 2025-10-02 04:38:29 +05:30
RitvikSardana
faade64fb5 chore: Spanish translations 2025-10-02 04:38:28 +05:30
RitvikSardana
9deb301619 chore: French translations 2025-10-02 04:38:26 +05:30
RitvikSardana
6c87f9d7ad Merge pull request #2625 from frappe/pot_develop_2025-09-28
chore: update POT file
2025-10-01 21:16:53 +05:30
Ritvik Sardana
105e8daff6 fix: disable command palette when text editor is enabled 2025-10-01 21:11:43 +05:30
RitvikSardana
eca4df62b4 Merge pull request #2624 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-10-01 21:05:16 +05:30
RitvikSardana
98d0369f6b Merge pull request #2636 from RitvikSardana/assign-message
fix: assign history message
2025-10-01 12:35:23 +05:30
Ritvik Sardana
046aa8e085 fix: reload activities 2025-10-01 12:29:04 +05:30
Ritvik Sardana
46f0faca0b fix: assign history message 2025-10-01 12:14:20 +05:30
RitvikSardana
5d9bdedb22 chore: Tamil translations 2025-10-01 04:32:40 +05:30
RitvikSardana
add112ff9a chore: Persian translations 2025-10-01 04:32:36 +05:30
RitvikSardana
9bc28bec9e chore: Portuguese, Brazilian translations 2025-10-01 04:32:34 +05:30
RitvikSardana
5b768c0e4f chore: Vietnamese translations 2025-10-01 04:32:33 +05:30
RitvikSardana
9d7bcfcbec chore: Chinese Simplified translations 2025-10-01 04:32:31 +05:30
RitvikSardana
f658054549 chore: Polish translations 2025-10-01 04:32:27 +05:30
RitvikSardana
1919abfcea chore: Hungarian translations 2025-10-01 04:32:24 +05:30
RitvikSardana
d058962498 chore: Arabic translations 2025-10-01 04:32:21 +05:30
RitvikSardana
0434986a00 chore: Spanish translations 2025-10-01 04:32:20 +05:30
RitvikSardana
6c6941d53c chore: French translations 2025-10-01 04:32:18 +05:30
frappe-pr-bot
5bbfeaff35 chore: update POT file 2025-09-28 10:35:23 +00:00
RitvikSardana
32447b2e77 chore: Norwegian Bokmal translations 2025-09-28 03:04:38 +05:30
RitvikSardana
557e0c1d61 chore: Hungarian translations 2025-09-28 03:04:37 +05:30
RitvikSardana
6ea1fb316a chore: Norwegian Bokmal translations 2025-09-27 03:01:08 +05:30
MostafaKadry
ee86eb6a0e fix: ignore automated system notifications in ticket communication handling 2025-09-26 15:31:38 +03:00
RitvikSardana
70cc92f9a1 Merge pull request #2621 from RitvikSardana/readonly-field
fix: respect read_only fields
2025-09-26 16:12:16 +05:30
Ritvik Sardana
3317f1446b fix: respect read_only fields 2025-09-26 16:10:53 +05:30
RitvikSardana
498ff60a73 Merge pull request #2618 from RitvikSardana/fetch-notification
fix: fetch notifications after the user is fetched
2025-09-26 15:58:43 +05:30
RitvikSardana
de2bcebc64 Merge pull request #2617 from RitvikSardana/disable-quick-feedback
fix: add disable field to Feedback Option
2025-09-26 15:58:24 +05:30
Ritvik Sardana
916f039ed3 fix: fetch notifications after the user is fetched 2025-09-26 15:44:52 +05:30
Ritvik Sardana
0bf79c7be8 fix: validate for old options only 2025-09-26 14:53:35 +05:30
Ritvik Sardana
8520a225d1 fix: add disable field to Feedback Option 2025-09-26 13:57:11 +05:30
RitvikSardana
91a7374dc0 Merge pull request #2610 from RitvikSardana/canned-responses-search
fix: search in Canned responses
2025-09-25 20:36:53 +05:30
RitvikSardana
9917846559 Merge pull request #2604 from frappe/pot_develop_2025-09-21
chore: update POT file
2025-09-25 20:32:00 +05:30
RitvikSardana
7b26aa11be Merge pull request #2595 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-09-25 20:31:45 +05:30
Ritvik Sardana
f0060c24c2 fix: add debounce in search 2025-09-25 20:27:09 +05:30
RitvikSardana
7c7c5d5f22 Merge pull request #2608 from pratikb64/fix/email-reply-all
fix: email reply all bug
2025-09-25 20:04:47 +05:30
RitvikSardana
3afaabbc96 Merge pull request #2594 from frappe/sqlite-fts
feat: Full Text Search using SQLite
2025-09-25 20:04:24 +05:30
RitvikSardana
7bcae0fbe9 Merge branch 'develop' into sqlite-fts 2025-09-25 19:56:23 +05:30
Ritvik Sardana
74a434acac chore: version bump 2025-09-25 19:54:55 +05:30
Ritvik Sardana
ecabb8d55d fix: search icon and placeholder 2025-09-25 17:16:27 +05:30
Ritvik Sardana
e9e5a58154 fix: persist filters and remove dump code from hooks and search_sqlite 2025-09-25 16:38:11 +05:30
Pratik Badhe
14f1acea19 fix: email reply all bug 2025-09-25 10:47:57 +00:00
Ritvik Sardana
741200e217 fix: dont index same doc thrice 2025-09-25 14:54:12 +05:30
Ritvik Sardana
5029510766 fix: hide dialog after deleting a comment 2025-09-25 11:43:13 +05:30
Ritvik Sardana
16b358b599 fix: add shortcut to focus on search bar 2025-09-25 00:12:14 +05:30
Ritvik Sardana
0248fcfc48 fix: show white icon for online and offline 2025-09-25 00:00:01 +05:30
Ritvik Sardana
232b30ebdd chore: update yarn.lock 2025-09-24 23:08:26 +05:30
Ritvik Sardana
7ade7b602b fix: linters 2025-09-24 23:06:57 +05:30
Ritvik Sardana
da7bf3d861 fix: add scrollbar in custom fields of customer portal 2025-09-24 22:52:43 +05:30
Ritvik Sardana
f8280ac203 fix: dont show count 2025-09-24 22:06:14 +05:30
Ritvik Sardana
aad177fc73 fix: convert reference_ticket value to int 2025-09-24 21:23:48 +05:30
Ritvik Sardana
ed06002cfc fix: search results with permission query filters 2025-09-24 17:56:00 +05:30
Ritvik Sardana
063c8aeea4 fix: ticket not found error 2025-09-24 13:28:26 +05:30
Ritvik Sardana
efa9ca438e fix: convert command pallete to composition API from options API 2025-09-24 13:27:56 +05:30
Ritvik Sardana
02080dd4c0 fix: ticket agent scroll 2025-09-24 10:59:32 +05:30
RitvikSardana
d67ad04e89 chore: Norwegian Bokmal translations 2025-09-24 02:56:43 +05:30
RitvikSardana
5ea83dccdd chore: Norwegian Bokmal translations 2025-09-23 02:03:58 +05:30
RitvikSardana
db9426279e chore: Persian translations 2025-09-23 02:03:55 +05:30
RitvikSardana
f64e305f91 chore: Norwegian Bokmal translations 2025-09-22 01:47:10 +05:30
frappe-pr-bot
b125c63d7b chore: update POT file 2025-09-21 10:35:14 +00:00
RitvikSardana
a55a148a78 chore: Norwegian Bokmal translations 2025-09-21 01:19:39 +05:30
RitvikSardana
cc3d392909 chore: Bump version to 1.15.1 2025-09-20 17:46:38 +05:30
RitvikSardana
124774b711 Merge pull request #2602 from RitvikSardana/easy-install-update
fix: add telephony in easy install script
2025-09-20 17:36:23 +05:30
Ritvik Sardana
096381bb5c fix: add telephony in easy install script 2025-09-20 17:34:19 +05:30
RitvikSardana
b93128d390 chore: Norwegian Bokmal translations 2025-09-20 01:10:10 +05:30
RitvikSardana
d4936a833f chore: Serbian (Latin) translations 2025-09-19 00:53:00 +05:30
RitvikSardana
781fa723e4 chore: Serbian (Cyrillic) translations 2025-09-19 00:52:58 +05:30
RitvikSardana
2b8fc16dc0 Merge pull request #2596 from RitvikSardana/new-ticket-seen
fix(desk): don't call mark seen api on new ticket
2025-09-18 15:21:57 +05:30
Ritvik Sardana
96efa8624f fix(desk): dont call mark seen api on new ticket 2025-09-18 15:19:58 +05:30
RitvikSardana
d07a8765ed chore: Esperanto translations 2025-09-18 00:57:23 +05:30
RitvikSardana
f513485016 chore: Serbian (Latin) translations 2025-09-18 00:57:21 +05:30
RitvikSardana
e866951197 chore: Norwegian Bokmal translations 2025-09-18 00:57:20 +05:30
RitvikSardana
7bc623051c chore: Bosnian translations 2025-09-18 00:57:19 +05:30
RitvikSardana
998b277216 chore: Croatian translations 2025-09-18 00:57:18 +05:30
RitvikSardana
dbcf6fe866 chore: Thai translations 2025-09-18 00:57:17 +05:30
RitvikSardana
0bd75cec41 chore: Persian translations 2025-09-18 00:57:16 +05:30
RitvikSardana
4e37c4c1d5 chore: Indonesian translations 2025-09-18 00:57:14 +05:30
RitvikSardana
6f6b04e6a9 chore: Portuguese, Brazilian translations 2025-09-18 00:57:13 +05:30
RitvikSardana
1410bd82cf chore: Vietnamese translations 2025-09-18 00:57:12 +05:30
RitvikSardana
6eecc8d8aa chore: Chinese Simplified translations 2025-09-18 00:57:11 +05:30
RitvikSardana
58b27e0261 chore: Turkish translations 2025-09-18 00:57:10 +05:30
RitvikSardana
4e6a00823f chore: Swedish translations 2025-09-18 00:57:09 +05:30
RitvikSardana
e599fd2e5a chore: Serbian (Cyrillic) translations 2025-09-18 00:57:08 +05:30
RitvikSardana
cee68f1b0d chore: Russian translations 2025-09-18 00:57:07 +05:30
RitvikSardana
6358533179 chore: Portuguese translations 2025-09-18 00:57:06 +05:30
RitvikSardana
117aa24a0a chore: Polish translations 2025-09-18 00:57:04 +05:30
RitvikSardana
ff91bd0c15 chore: Dutch translations 2025-09-18 00:57:03 +05:30
RitvikSardana
647fa443fc chore: Italian translations 2025-09-18 00:57:02 +05:30
RitvikSardana
5be3593a13 chore: Hungarian translations 2025-09-18 00:57:01 +05:30
RitvikSardana
93dd13deaf chore: German translations 2025-09-18 00:57:00 +05:30
RitvikSardana
d86fdba523 chore: Danish translations 2025-09-18 00:56:59 +05:30
RitvikSardana
a53450f740 chore: Czech translations 2025-09-18 00:56:57 +05:30
RitvikSardana
f7929ca621 chore: Arabic translations 2025-09-18 00:56:56 +05:30
RitvikSardana
dab44e62d5 chore: Spanish translations 2025-09-18 00:56:55 +05:30
RitvikSardana
dd81a4cdad chore: French translations 2025-09-18 00:56:54 +05:30
Faris Ansari
0c2967f503 feat: Full Text Search using SQLite
- dedicated search page
- route to ticket and scroll to relevant comment or email
- wip
2025-09-17 14:20:15 +05:30
RitvikSardana
72647ec4e0 Merge pull request #2587 from RitvikSardana/portal-url
fix: update portal url param
2025-09-17 00:21:13 +05:30
Ritvik Sardana
ee4f111f05 chore: bump version 2025-09-16 21:40:06 +05:30
Ritvik Sardana
f671d05ba2 fix: update portal url param 2025-09-16 21:21:54 +05:30
Pratik Badhe
b395d0d5d0 fix: url bug for custom view 2025-09-16 11:30:03 +00:00
Pratik Badhe
bca1b3a16e Merge remote-tracking branch 'origin/develop' into fix/telephony-bugs 2025-09-16 09:36:14 +00:00
Pratik Badhe
5d537211d0 fix: use correct variable 2025-09-15 07:21:58 +00:00
Pratik
6674961963 fix: tab url hash & make a call button 2025-09-11 07:58:22 +05:30
MhmdSinanKT
2b6e54a941 feat: enhance search with or filter 2025-08-30 09:43:15 +05:30
MhmdSinanKT
85e8d9d2e9 feat: search for canned responses in hd view 2025-08-30 09:43:15 +05:30
789 changed files with 243827 additions and 37366 deletions

View File

@@ -40,6 +40,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext --branch "develop"
bench get-app telephony
bench get-app helpdesk "${GITHUB_WORKSPACE}"
bench setup requirements --dev
@@ -48,3 +49,7 @@ bench setup requirements --dev
bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
bench --verbose --site test_site install-app erpnext
bench --verbose --site test_site install-app telephony
bench --verbose --site test_site install-app helpdesk

View File

@@ -5,7 +5,7 @@ cd ~ || exit
echo "Setting Up Bench..."
pip install frappe-bench
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --frappe-branch "${BASE_BRANCH}"
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --frappe-branch develop
cd ./frappe-bench || exit
@@ -38,4 +38,4 @@ gh auth setup-git
git push -u upstream "${branch_name}"
echo "Creating a PR..."
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/helpdesk
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/helpdesk

15
.github/workflows/assets.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Pre-build assets
on:
push:
branches: [develop, main]
jobs:
assets:
name: Pre-build assets
uses: frappe/pilot/.github/workflows/app-assets.yml@develop
with:
# main supports Frappe v15 and v16; Pilot benches run v16 or later.
frappe-branch: ${{ github.ref_name == 'main' && 'version-16' || 'develop' }}
permissions:
contents: write

View File

@@ -13,21 +13,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
permissions:
packages: write
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/${{ matrix.arch }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
@@ -38,8 +32,7 @@ jobs:
- name: Set Branch
run: |
export APPS_JSON='[{"url": "https://github.com/frappe/helpdesk","branch": "main"}]'
echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
echo '[{"url": "https://github.com/frappe/telephony","branch": "develop"},{"url": "https://github.com/frappe/helpdesk","branch": "main"}]' > apps.json
echo "FRAPPE_BRANCH=version-15" >> $GITHUB_ENV
- name: Set Image Tag
@@ -51,10 +44,10 @@ jobs:
repository: frappe/frappe_docker
path: builds
- name: Build and push
- name: Build
uses: docker/build-push-action@v6
with:
push: true
load: true
context: builds
file: builds/images/layered/Containerfile
tags: >
@@ -62,4 +55,14 @@ jobs:
ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }}
build-args: |
"FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}"
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
secret-files: |
apps_json=apps.json
- name: Verify apps are installed
run: |
apps=$(docker run --rm --entrypoint ls ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }} apps)
echo "$apps"
grep -qx helpdesk <<< "$apps" && grep -qx telephony <<< "$apps"
- name: Push
run: docker push --all-tags ghcr.io/${{ github.repository }}

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
branch: ["develop"]
branch: ["develop", "main-hotfix"]
permissions:
contents: write
@@ -25,7 +25,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.14"
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
- name: Run script to update POT file
run: |

View File

@@ -17,7 +17,7 @@ jobs:
fetch-depth: 200
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 24
check-latest: true
- name: Check commit titles
run: |
@@ -35,7 +35,7 @@ jobs:
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.10'
python-version: '3.14'
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.1

32
.github/workflows/on_release.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Generate Semantic Release
on:
workflow_dispatch:
push:
branches:
- main
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release

View File

@@ -27,13 +27,13 @@ jobs:
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/crm/releases/generate-notes -f tag_name=$RELEASE_TAG \
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/helpdesk/releases/generate-notes -f tag_name=$RELEASE_TAG \
| jq -r '.body' \
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
| sed -E 's/by @mergify //'
)
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/crm/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/crm/releases/$RELEASE_ID -f body="$NEW_NOTES"
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/helpdesk/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/helpdesk/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -40,7 +40,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.14'
- name: Check for valid Python & Merge Conflicts
if: github.event.pull_request.base.ref == 'develop'
@@ -54,7 +54,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18
node-version: 24
check-latest: true
- name: Install Yarn
@@ -104,7 +104,32 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-tests --app helpdesk'
run: 'cd ~/frappe-bench/ && bench --site test_site run-tests --app helpdesk --coverage'
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
coverage:
name: Coverage Wrap Up
needs: tests
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Upload coverage data
uses: codecov/codecov-action@v4
with:
name: MariaDB
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true

187
.github/workflows/ui-tests.yml vendored Normal file
View File

@@ -0,0 +1,187 @@
name: UI Tests
on:
pull_request:
branches:
- develop
paths:
- "desk/**"
- "e2e/**"
- "playwright.config.ts"
- "package.json"
- "yarn.lock"
- ".github/workflows/ui-tests.yml"
workflow_dispatch:
concurrency:
group: ui-tests-helpdesk-${{ github.event.number || github.ref }}
cancel-in-progress: true
env:
FRAPPE_BRANCH: develop
SITE: helpdesk.test
jobs:
ui-tests:
name: Playwright E2E Tests (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shard: [1, 2]
services:
redis-cache:
image: redis/redis-stack-server:latest
ports:
- 13000:6379
redis-queue:
image: redis:alpine
ports:
- 11000:6379
mariadb:
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
check-latest: true
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: ${{ runner.os }}-pip-
- name: Cache yarn
uses: actions/cache@v4
with:
path: ~/.cache/yarn
key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-ui-
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
- name: Install MariaDB Client
run: |
sudo apt update
sudo apt install -y mariadb-client
- name: Setup Bench
run: |
pip install frappe-bench
bench init --skip-redis-config-generation --skip-assets --frappe-branch "$FRAPPE_BRANCH" --python "$(which python)" ~/frappe-bench
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- name: Install Helpdesk
working-directory: /home/runner/frappe-bench
run: |
bench get-app telephony
bench get-app helpdesk $GITHUB_WORKSPACE
bench setup requirements --dev
bench new-site --db-root-password root --admin-password admin --install-app helpdesk $SITE
bench use $SITE
bench --site $SITE set-config host_name "http://localhost:8000"
bench build --apps frappe,helpdesk
env:
CI: "Yes"
- name: Start Frappe Server
working-directory: /home/runner/frappe-bench
run: |
sed -i 's/^watch:/# watch:/g' Procfile
sed -i 's/^schedule:/# schedule:/g' Procfile
bench start &> bench_start.log &
timeout 120 bash -c "until curl -s http://localhost:8000 > /dev/null; do sleep 2; done"
- name: Install Playwright
run: |
yarn install --frozen-lockfile --ignore-scripts
npx playwright install --with-deps chromium
- name: Run Playwright Tests
run: npx playwright test --shard=${{ matrix.shard }}/2
env:
# localhost is a secure context, which crypto.randomUUID in frappe/ui needs.
BASE_URL: http://localhost:8000
ADMIN_PASSWORD: admin
- name: Upload Blob Report
uses: actions/upload-artifact@v4
if: always()
with:
name: blob-report-${{ matrix.shard }}
path: blob-report/
retention-days: 1
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.shard }}
path: test-results/
retention-days: 7
- name: Show Bench Logs on Failure
if: failure()
working-directory: /home/runner/frappe-bench
run: |
cat bench_start.log || true
cat logs/*.log || true
merge-reports:
name: Merge Playwright Reports
needs: ui-tests
if: always()
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install Playwright
run: yarn install --frozen-lockfile --ignore-scripts
- name: Download Blob Reports
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge Into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload Playwright Report
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7

11
.gitignore vendored
View File

@@ -29,6 +29,12 @@ share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
.playwright-mcp
.claude-flow
.claude
.claude-worktrees
CLAUDE.md
helpdesk/docs/current
helpdesk/public/desk
@@ -36,3 +42,8 @@ helpdesk/public/frontend
helpdesk/public/node_modules
helpdesk/www/helpdesk/index.html
desk/components.d.ts
desk/stats.html
# Playwright
e2e/.auth
playwright-report
test-results

View File

@@ -1,45 +1,42 @@
pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=RitvikSardana
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:
- base=main
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on develop branch.
- name: backport to develop
conditions:
- label="backport develop"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to main-hotfix
conditions:
- label="backport main-hotfix"
actions:
backport:
branches:
- main-hotfix
assignees:
- "{{ author }}"
- name: backport to main
conditions:
- label="backport main"
actions:
backport:
branches:
- main
assignees:
- "{{ author }}"
- name: Auto-close PRs on stable branch
conditions:
- and:
- author != RitvikSardana
- author != frappe-pr-bot
- author != mergify[bot]
- author != aerodeval
- or:
- base = main
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on develop branch.
- name: backport to develop
conditions:
- label = "backport develop"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to main-hotfix
conditions:
- label = "backport main-hotfix"
actions:
backport:
branches:
- main-hotfix
assignees:
- "{{ author }}"
- name: backport to main
conditions:
- label = "backport main"
actions:
backport:
branches:
- main
assignees:
- "{{ author }}"

View File

@@ -4,19 +4,19 @@ fail_fast: false
repos:
- repo: https://github.com/psf/black
rev: 22.6.0
rev: 25.11.0
hooks:
- id: black
types_or: [python, pyi]
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- flake8-comprehensions
- flake8-simplify
- flake8-simplify==0.30.0
args: ['--config', '.github/helpers/.flake8_strict']
- repo: https://github.com/pre-commit/mirrors-prettier

21
.releaserc Normal file
View File

@@ -0,0 +1,21 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular"
},
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" helpdesk/__init__.py'
}
],
[
"@semantic-release/git", {
"assets": ["helpdesk/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}"
}
],
"@semantic-release/github"
]
}

471
AGENTS.md Normal file
View File

@@ -0,0 +1,471 @@
# Copilot Instructions for Frappe Helpdesk
## Skills
For Frappe development tasks, reference the frappe-dev skill:
- **Location**: `~/.agents/skills/frappe-dev/`
- **Usage**: When working on DocType, controllers, whitelisted APIs, background jobs, permissions, or frontend tasks, read the relevant skill reference files:
- `doctype.md` — Creating/modifying DocTypes and fields
- `controllers.md` — Document lifecycle and server logic
- `api.md` — REST endpoints with `@frappe.whitelist()`
- `database.md` — `frappe.db`, queries, raw SQL
- `background-jobs.md` — `frappe.enqueue`, scheduled jobs
- `permissions.md` — Roles, DocType permissions
- `frontend-desk.md`, `frontend-vue.md`, `frontend-portal.md` — Frontend references
## Project Overview
Frappe Helpdesk is an open-source ticket management tool built on the [Frappe Framework](https://github.com/frappe/frappe) (Python backend) and [Frappe UI](https://github.com/frappe/frappe-ui) (Vue.js frontend). It provides agent/customer portals, SLAs, assignment rules, a knowledge base, and saved replies.
## Tech Stack & Architecture
- **Backend**: Frappe Framework with Python
- **Frontend**: Vue 3 + TypeScript SPA with Vite dev server
- **Database**: MariaDB with structured doctypes
- **Real-time**: WebSockets via Socket.IO for live updates
- **UI Library**: frappe-ui for consistent design components
- **Styling**: Tailwind CSS with semantic design tokens
## Architecture
- **Backend (Python):** Located in `helpdesk/`.
- API endpoints: `helpdesk/api/`
- Business logic: `helpdesk/utils.py`, `helpdesk/search.py`, etc.
- Extensibility: `helpdesk/extends/`, `helpdesk/overrides/`
- **Frontend (Vue.js):** Located in `desk/`.
- Main entry: `desk/src/main.js`, `desk/src/App.vue`
- Components: `desk/src/components/`
- State: `desk/src/stores/`
- Routing: `desk/src/router/`
- Styles: Tailwind via `desk/index.css`, config in `desk/tailwind.config.js`
# Frontend: Vue 3 Development Standards
## Frontend Project Context
- `desk/src/` contains the main Vue 3 frontend source code
- Vue 3.x with Composition API as default approach
- TypeScript for type safety and better development experience
- Single File Components (`.vue`) with `<script setup lang="ts">` syntax preferred
- Vite as the build tool for fast development and optimized builds
- Pinia for state management (unlike gameplan which avoids state libraries)
- frappe-ui for UI components and data fetching utilities
## Development Standards
### Architecture & Component Design
- Favor the Composition API (`setup` functions and composables) over the Options API
- Organize components and composables by feature or domain for scalability
- Separate UI-focused components (presentational) from logic-focused components (containers)
- Use PascalCase for component names and file names
- Keep components small and focused on one concern
- Use `<script setup lang="ts">` syntax for brevity and performance
- Favor slots and scoped slots for flexible composition
### TypeScript Integration
- TypeScript is enabled but not in strict mode (as per actual tsconfig.json)
- Use `<script setup lang="ts">` with `defineProps` and `defineEmits`
- Leverage `PropType<T>` for typed props and default values
- Use interfaces or type aliases for complex prop and state shapes
- Define types for event handlers, refs, and `useRoute`/`useRouter` hooks
### Styling Guidelines
- Always prefer Tailwind CSS for styling
- Use utility classes for layout and spacing
- Use semantic class names wherever possible:
- Background colors: `bg-surface-white`, `bg-surface-gray-1` through `bg-surface-gray-9`, `bg-surface-black`
- Text colors: `text-ink-white`, `text-ink-gray-1` through `text-ink-gray-9`, `text-ink-black`
- Fill colors: `fill-ink-*`
- Placeholder colors: `placeholder-ink-*`
- Border colors: `border-outline-white`, `border-outline-gray-1` through `border-outline-gray-5`, `border-outline-black`
- Font sizes: `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `text-3xl`
- Multiline text: `text-p-xs`, `text-p-sm`, `text-p-base`, `text-p-lg`, `text-p-xl`, `text-p-2xl`
- Always use gray shades for everything, never use color shades even for primary states
- Implement mobile-first, responsive design with CSS Grid and Flexbox
- Ensure styles are accessible (contrast, focus states)
### Data Fetching with frappe-ui
- Use `createResource`, `createListResource`, and `createDocumentResource` from frappe-ui for data fetching
- Handle loading, error, and success states explicitly
- These are the older but stable composables used throughout the project
When working with a list of documents:
```ts
const tickets = createListResource({
doctype: "HD Ticket",
filters: { status: "Open" },
fields: ["name", "subject", "modified"],
limit: 20,
auto: true,
});
// Access data
tickets.data; // Array of ticket objects
tickets.loading; // Boolean loading state
tickets.error; // Error object if any
// Operations
tickets.reload(); // Refresh the list
tickets.update({ name: "<id>", subject: "Updated Subject" }); // Update item
tickets.insert({ subject: "New Ticket" }); // Add new item
tickets.delete("<id>"); // Delete item
```
When working with a single document:
```ts
const ticket = createDocumentResource({
doctype: "HD Ticket",
name: "<ticket-id>",
auto: true,
});
// Access data
ticket.doc; // Document object
ticket.loading; // Boolean loading state
ticket.error; // Error object if any
// Operations
ticket.get(); // Fetch/refresh document
ticket.setValue("status", "Resolved"); // Update field
ticket.save(); // Save changes
ticket.delete(); // Delete document
```
For API calls:
```ts
const apiCall = createResource({
url: "helpdesk.api.ticket.assign_ticket_to_agent",
onSuccess(data) {
// Handle success
},
onError(error) {
// Handle error
},
});
// Make the call
apiCall.submit({ ticket_id: "<id>", agent_id: "<agent>" });
```
### Icons
- **Primary icon system**: Lucide icons via unplugin-icons for consistency and modern design
- **Secondary**: FeatherIcon component from frappe-ui (legacy usage only)
- **Rule**: Never introduce `FeatherIcon` in new code. New components must use Lucide
icons (`<LucideInfo />`, `<LucidePlus />`, …) — no exceptions, including small
helper icons like tooltip/info glyphs.
- Use Lucide icons for all new components. They are globally auto-registered
(see `desk/components.d.ts`), so no import is needed when used as a template tag:
```vue
<template>
<LucidePlus class="size-4" />
</template>
```
- For script usage, import Lucide icons like this:
```ts
import LucidePlus from "~icons/lucide/plus";
import LucideTicket from "~icons/lucide/ticket";
```
- Only use FeatherIcon for maintaining existing components:
```vue
<template>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
```
### State Management
- Use Pinia stores for complex shared state
- For simple local state, use `ref` and `reactive` within `setup`
- Use `computed` for derived state
- Keep state normalized for complex structures
### Performance & Accessibility
- Apply `v-once` and `v-memo` for static or infrequently changing elements
- Avoid unnecessary watchers; prefer `computed` where possible
- Use semantic HTML elements and ARIA attributes
- Manage focus for modals and dynamic content
- Ensure keyboard navigation for interactive components
# Backend: Frappe Framework Development Standards
## Backend Project Context
- Frappe Framework provides full-stack capabilities with ORM, permissions, and background jobs
- MariaDB database with structured DocTypes for business entities
- Redis for background workers and caching
- Socket.IO for real-time updates
## Critical Development Workflows
**Local Development Setup**:
- Backend runs via `bench start` from frappe-bench directory
- Python web server: Assume server is already started on port 8000
- Frontend dev server: `cd desk && yarn dev` (typically runs on port 8080)
- Site-specific commands: `bench --site helpdesk.test <command>`
**Data Query Patterns**:
- Always prefer `frappe.get_list()` over `frappe.get_all()` for new code. It handles perms out of the box and is more flexible.
- `frappe.get_all()` is fine for child tables (e.g. `Has Role`, `HD Customer Member`), which have no permissions of their own.
- When `frappe.get_all()` is used on a parent DocType, make sure the caller may see the result some other way, for example a `frappe.has_permission(...)` check first, or `@agent_only` / `@agent_manager_only` (`helpdesk/utils.py`) on the endpoint.
- Use `frappe.qb.get_query(..., ignore_permissions=False)` when permission checks are needed
- Example query patterns from helpdesk context
**Debugging Workflow**:
- Create debug files like `./helpdesk/debug.py` with `def execute():` function
- Run with `bench --site helpdesk.test execute helpdesk.debug.execute`
- Use `print()` statements for console output during debugging
## DocType Architecture Patterns
- Follow Frappe's DocType conventions for business entities
- Implement custom permission logic in `has_permission` hooks
- Use proper field types and validation in DocType definitions
- Leverage DocType methods for business logic encapsulation
## Controller Code Structure
All controller logic must follow this structure strictly:
**Rule: Lifecycle hooks call methods. Methods are defined below on the class.**
```python
class HDFoo(Document):
# 1. Lifecycle hooks at the top — each hook calls methods, no inline logic
def after_insert(self):
self.do_a()
self.do_b()
def on_update(self):
self.do_a()
# 2. Methods below — one responsibility each
def do_a(self):
pass
def do_b(self):
pass
```
**Rules:**
- Lifecycle hooks (`after_insert`, `on_update`, `before_save`, `on_trash`, `after_rename`, etc.) contain **no inline logic** — they only call `self.method()`.
- All logic lives in methods on the class, not in module-level helper functions.
- Methods that mutate a related document (e.g. an Assignment Rule) receive that doc as an argument and mutate it in memory. The **caller** is responsible for the final `doc.save()` — this keeps saves to a minimum (one save per hook).
- Never fetch the same document twice in one hook. Fetch once, pass the doc through the call chain.
- `@staticmethod` is allowed for pure transformations (e.g. building a condition string) that don't touch `self` or any Frappe doc.
- If there is an issue in framework/ui or frappe-ui or frappe framework, dont do a workaround. Instead, raise a blocker in the spec and wait for a fix. Do not implement workarounds that will be removed later. If possible, raise a PR to fix the issue in the framework instead of implementing a workaround in helpdesk.
**Save discipline:**
- Mutate all fields on a document before calling `.save()` once.
- Never call `.save()` inside a helper method — save in the hook or the method that owns the full mutation.
- Exception: if a method truly owns a complete document lifecycle (e.g. `create_assignment_rule` creates and fully initialises a new doc), it may save internally.
Take inspiration from "helpdesk.doctype.hd_team.hd_team.HDTeam" for examples of this pattern in practice.
## Conventions & Patterns
- **API Design:** Python modules in `helpdesk/api/` follow RESTful patterns. Extend via `helpdesk/extends/`.
- **Frontend:** Vue 3 + Vite with TypeScript. Use Pinia for state management. Components auto-imported via `desk/components.d.ts`.
- **Testing:** Follow Frappe's conventions for backend tests.
- **Localization:** Add translations in `helpdesk/locale/`.
- **Custom Logic:** Use `helpdesk/overrides/` for customizations.
## Utilities and Composables
- @vueuse/core is available for common Vue utilities
- Prefer vueuse composables over custom implementations where applicable
- Common composables: `useLocalStorage`, `useDebounce`, `useElementSize`
- Always use frappe-ui's data fetching composables (`createResource`, `createListResource`, `createDocumentResource`), never `useFetch`
## Error Handling & Forms
- Use global error handlers for uncaught errors
- Wrap risky logic in `try/catch` with user-friendly messages
- Build forms with controlled `v-model` bindings
- Validate on blur or input with debouncing for performance
- Ensure accessible labeling and error announcements
## Integration Points
- **Frappe Framework:** All backend logic integrates with Frappe ORM, permissions, and hooks (`helpdesk/hooks.py`).
- **Frappe UI:** Frontend uses Frappe UI for consistent design components and data fetching.
- **Real-time Updates:** WebSockets via Socket.IO for live ticket updates.
- **Crowdin:** Translation sync via `crowdin.yml`.
## Examples
### Adding a New API Endpoint
1. Create a Python file in `helpdesk/api/`
2. Add `@frappe.whitelist()` decorator to functions
3. Register in `helpdesk/hooks.py` if needed
4. Use `frappe.qb.get_query()` for database queries
### Adding a Frontend Component
1. Create Vue component in `desk/src/components/` or `desk/src/pages/`
2. Use `<script setup lang="ts">` syntax
3. Follow Tailwind semantic class conventions
4. Use frappe-ui composables for data fetching
5. Add route in `desk/src/router/` if it's a page component
### Styling Example
```vue
<template>
<div class="bg-surface-white border border-outline-gray-2 rounded-lg p-4">
<h2 class="text-lg text-ink-gray-9 font-semibold mb-2">Ticket Details</h2>
<p class="text-p-sm text-ink-gray-7">{{ ticket.doc.subject }}</p>
<LucideTicket class="size-4 text-ink-gray-6" />
</div>
</template>
<script setup lang="ts">
import { createDocumentResource } from "frappe-ui";
import LucideTicket from "~icons/lucide/ticket";
const ticket = createDocumentResource({
doctype: "HD Ticket",
name: props.ticketId,
auto: true,
});
</script>
```
## Key Files & Directories
### Backend
- `helpdesk/api/` — Backend API endpoints with @frappe.whitelist() decorators
- `helpdesk/utils.py` — Shared backend utilities and helper functions
- `helpdesk/hooks.py` — Frappe hooks
- `helpdesk/overrides/` — Custom logic and extensions
### Frontend
- `desk/src/` — Main Vue 3 frontend source code
- `desk/src/components/` — Reusable Vue components
- `desk/src/pages/` — Page-level components and routes
- `desk/src/stores/` — Pinia stores for state management
- `desk/tailwind.config.js` — Tailwind CSS configuration with semantic tokens
- `desk/components.d.ts` — Auto-generated component type definitions
### Development
- `docker/` — Docker setup for containerized development
## Code Quality & Standards
- Follow Vue's official style guide and best practices
- Use ESLint with Vue 3 recommended rules and Prettier for consistency
- Write meaningful commit messages and maintain clean git history
- Document complex logic with JSDoc/TSDoc comments
- Keep a comment to 2 lines at most. If it needs more to explain, simplify the code instead
- Keep dependencies updated and audit for security vulnerabilities
## References
- [README.md](../README.md) — Full setup and feature list
- [Frappe Docs](https://frappeframework.com/docs/) — Framework documentation
- [Frappe Helpdesk Docs](https://docs.frappe.io/helpdesk) — Project-specific docs
- [Vue 3 Style Guide](https://vuejs.org/style-guide/) — Vue.js best practices
- [Tailwind CSS](https://tailwindcss.com/docs) — Utility-first CSS framework
---
## Agent Working Rules
These rules apply to all agents working in this codebase. Follow them without exception.
### 1. Site Data — Read Before You Write
When a Frappe site is referenced in the conversation (e.g. `erp-hd.localhost`, `hd-tests`),
treat it as the **source of truth for current data state**.
- Before assuming anything about existing records, custom fields, settings, or DocType state —
query the site first using the bench venv Python or `bench execute`.
- **Never write data to a site unless the user explicitly asks you to.**
Querying is always safe. Mutations (insert, update, delete, migrate, patch) require explicit
user instruction.
- If you need to verify a fix worked, re-query the site — do not assume success from code
inspection alone.
```python
# Safe pattern for querying a site
import sys, os
sys.path.insert(0, 'apps/frappe')
os.chdir('sites')
import frappe
frappe.init(site='<site-name>', sites_path='.')
frappe.connect()
# ... read-only queries here ...
frappe.destroy()
```
Run via: `cd /path/to/frappe-bench && env/bin/python /tmp/your_script.py`
---
### 2. Spec-Driven Development — Keep the Spec Current
When a feature has a spec document (e.g. `docs/erpnext-customer-sync.md`), the spec is the
**living source of truth** for that feature. It must be kept in sync with the actual
implementation at all times.
**Rules:**
- **Update the spec in the same session as the code change.** Never defer spec updates to a
later session.
- **The spec reflects what is implemented, not what was originally planned.** If a decision
changes mid-build (e.g. removing a scheduler job, changing how a field is set), update the
spec immediately after the code change.
- **Removed features must be removed from the spec.** Do not leave stale sections, blockers, or
file manifest entries for things that no longer exist.
- **New features must be added to the spec.** If a new API, component, or behaviour is
introduced, add a corresponding section.
- **Decisions and their rationale belong in the spec.** If something was done a specific way to
avoid a bug or framework limitation (e.g. not setting `customer_group` to avoid the group-node
error), document that in the relevant Blocker section.
- When in doubt about whether a spec update is needed — update it.
---
### 3. Test Utilities — Always Use `helpdesk/test_utils.py`
All backend test helpers must live in `helpdesk/test_utils.py` and be imported from there.
Never define helper functions (factories, enable/disable toggles, fixture builders) directly
inside individual test files.
**Rules:**
- **Before writing a helper in a test file**, check `helpdesk/test_utils.py` first — it may
already exist.
- **If a helper doesn't exist**, add it to `helpdesk/test_utils.py` and import it in the test
file. Do not leave it inline.
- **If you write helpers inline during development**, move them to `test_utils.py` before
finishing the task.
- Helper functions in `test_utils.py` must have a docstring explaining what they create or do. Only if the function is completely self-explanatory (e.g. `enable_erpnext_sync`) can the docstring be omitted.
- Naming conventions:
- Factories: `make_<doctype>(...)` — e.g. `make_hd_customer`, `make_ticket`, `make_team`
- Toggle helpers: `enable_<feature>()` / `disable_<feature>()` — e.g. `enable_erpnext_sync`
- Query helpers: descriptive verb — e.g. `get_latest_ticket_communication`
**For updates, merge new conventions here. If anything is unclear or missing, ask for clarification.**
**In the end your code will be reviewed by Codex, but following these rules will help ensure a smooth review process and a high-quality codebase.**

View File

@@ -42,7 +42,7 @@ Managing issues from our customers was a big challenge for us. We were using the
- **Knowledge Base**: Learn how to create and manage help articles to empower users and reduce tickets.
- **Canned Responses**: Pre-written replies for common queries to ensure quick and consistent communication.
- **Saved Replies**: Pre-written replies for common queries to ensure quick and consistent communication.
<details open>
<summary >View Screenshots</summary>
@@ -165,6 +165,7 @@ To setup the repository locally follow the steps mentioned below:
1. Start the server by running `bench start`
1. In a separate terminal window, create a new site by running `bench new-site helpdesk.test`
1. Map your site to localhost with the command `bench --site helpdesk.test add-to-hosts`
1. Get the Telephony app. Run `bench get-app https://github.com/frappe/telephony`
1. Get the Helpdesk app. Run `bench get-app https://github.com/frappe/helpdesk`
1. Run `bench --site helpdesk.test install-app helpdesk`.
1. Run `bench build --app helpdesk`
@@ -187,6 +188,7 @@ To setup the repository locally follow the steps mentioned below:
| Helpdesk Branch | Compatible Frappe Framework Version |
|-----------------|-------------------------------------|
| main | version-15 |
| main | version-16 |
| develop | develop branch |

View File

@@ -6,5 +6,9 @@
// biome-ignore lint: disable
export {}
declare global {
const LucideAlignJustify: typeof import('~icons/lucide/align-justify')['default']
const LucideCornerUpLeft: typeof import('~icons/lucide/corner-up-left')['default']
const LucideFileSearch: typeof import('~icons/lucide/file-search')['default']
const LucideMessageCircle: typeof import('~icons/lucide/message-circle')['default']
const LucideTag: typeof import('~icons/lucide/tag')['default']
}

View File

@@ -1,8 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ boot.lang or 'en' }}" dir="{{ boot.dir or 'ltr' }}">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Helpdesk</title>
<meta name="mobile-web-app-capable" content="yes" />
@@ -206,12 +205,7 @@
<div id="popovers"></div>
<script type="module" src="/src/main.js"></script>
<script>
window.favicon = "{{ favicon }}";
window.site_name = "{{ site_name }}";
document.querySelectorAll("link[rel='icon']").forEach((link) => {
link.href = window.favicon;
});
</script>
</body>
</html>

View File

@@ -1,37 +0,0 @@
import * as LucideIcons from "lucide-static";
const icons = {};
for (const icon in LucideIcons) {
let iconSvg = LucideIcons[icon];
if (icon == "default") {
continue;
}
// set stroke-width to 1.5
if (iconSvg && iconSvg.includes("stroke-width")) {
iconSvg = iconSvg.replace(/stroke-width="2"/g, 'stroke-width="1.5"');
}
icons[icon] = iconSvg;
const dashKeys = camelToDash(icon);
for (const dashKey of dashKeys) {
if (dashKey !== icon) {
icons[dashKey] = iconSvg;
}
}
}
export default icons;
function camelToDash(key) {
// barChart2 -> bar-chart-2
const withNumber = key.replace(/[A-Z0-9]/g, (m) => "-" + m.toLowerCase());
// barChart2 -> bar-chart2
const withoutNumber = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
if (withNumber !== withoutNumber) {
// both are required because unplugin icon resolver doesn't put a dash before numbers
return [withNumber, withoutNumber];
}
return [withNumber];
}

View File

@@ -6,54 +6,56 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
"serve": "vite preview",
"postinstall": "patch-package"
},
"dependencies": {
"@framework/ui": "link:../../frappe/ui",
"@headlessui/vue": "^1.7.22",
"@iconify-json/lucide": "^1.1.99",
"@iconify-json/ph": "^1.1.5",
"@iconify/tools": "^2.2.6",
"@iconify/vue": "^4.1.1",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.9",
"@tiptap/core": "^2.2.4",
"@tiptap/core": "^3.26.0",
"@tiptap/extension-table": "^3.26.0",
"@tiptap/extension-text-align": "^3.26.0",
"@tiptap/extension-text-style": "^3.26.0",
"@tiptap/pm": "^3.26.0",
"@tiptap/starter-kit": "^3.26.0",
"@twilio/voice-sdk": "^2.15.0",
"@vee-validate/zod": "^4.8.2",
"@vitejs/plugin-vue": "^4.2.3",
"@vueuse/core": "^10.0.2",
"@vueuse/integrations": "^12.0.0",
"@vitejs/plugin-vue": "^4.6.2",
"@vueuse/core": "^14.1.0",
"@vueuse/integrations": "^14.1.0",
"dayjs": "^1.11.7",
"echarts": "^5.4.1",
"frappe-ui": "0.1.192",
"gemoji": "^8.1.0",
"lodash": "^4.17.21",
"lucide-static": "^0.276.0",
"echarts": "^6.1.0",
"grid-layout-plus": "^1.1.0",
"frappe-ui": "1.0.0-rc.1",
"mime": "^3.0.0",
"pinia": "^2.0.33",
"pluralize": "^8.0.0",
"sanitize-html": "^2.10.0",
"socket.io-client": "^4.7.2",
"unplugin-icons": "^0.16.1",
"unplugin-vue-components": "^0.25.2",
"vee-validate": "^4.8.2",
"tailwindcss-rtl": "^0.9.0",
"vue": "^3.5.14",
"vue-echarts": "^6.5.4",
"vue-router": "^4.2.2",
"vuedraggable": "^4.1.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.13",
"patch-package": "^8.0.1",
"postcss": "^8.4.5",
"postinstall-postinstall": "^2.1.0",
"prettier": "2.8.4",
"tailwindcss": "^3.4.15",
"typescript": "^5.0.2",
"unplugin-icons": "^22.1.0",
"vite": "^5.4.21",
"vite-plugin-pwa": "0.17.5"
},
"resolutions": {
"cheerio": "1.0.0-rc.12",
"prosemirror-model": "1.25.1"
},
"devDependencies": {
"@vitejs/plugin-vue-jsx": "^3.0.1",
"prettier": "2.8.4",
"typescript": "^5.0.2",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.20.5",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.5",
"tailwindcss": "^3.4.15"
"prosemirror-model": "1.25.9",
"prosemirror-state": "1.4.4",
"prosemirror-view": "1.41.9",
"prosemirror-transform": "1.12.0"
}
}

View File

@@ -0,0 +1,70 @@
diff --git a/node_modules/frappe-ui/src/molecules/editor/extensions/shared/suggestion-renderer.ts b/node_modules/frappe-ui/src/molecules/editor/extensions/shared/suggestion-renderer.ts
index 91c8dc4..2883b93 100644
--- a/node_modules/frappe-ui/src/molecules/editor/extensions/shared/suggestion-renderer.ts
+++ b/node_modules/frappe-ui/src/molecules/editor/extensions/shared/suggestion-renderer.ts
@@ -5,6 +5,7 @@ import type {
} from '@tiptap/suggestion'
import type { Component } from 'vue'
import {
+ autoUpdate,
computePosition,
flip,
offset,
@@ -40,6 +41,9 @@ export function createSuggestionRenderer(
let isActive = false
// Monotonic token; a stale onUpdate from a superseded query bails.
let renderToken = 0
+ // Monotonic token; a stale computePosition settling later bails.
+ let positionToken = 0
+ let cleanupAutoUpdate: (() => void) | null = null
function getListExpose(): SuggestionListExpose | null {
const ref = renderer?.ref as SuggestionListExpose | null | undefined
@@ -52,6 +56,7 @@ export function createSuggestionRenderer(
const rect = getReferenceClientRect()
if (!rect) return
const reference = { getBoundingClientRect: () => rect }
+ const token = ++positionToken
void computePosition(reference, floatingEl, {
placement: floatingOptions?.placement ?? 'bottom-start',
middleware: [
@@ -60,7 +65,7 @@ export function createSuggestionRenderer(
shift({ padding: 8 }),
],
}).then(({ x, y }) => {
- if (!floatingEl) return
+ if (!floatingEl || token !== positionToken) return
Object.assign(floatingEl.style, {
position: 'absolute',
left: `${x}px`,
@@ -93,7 +98,19 @@ export function createSuggestionRenderer(
floatingEl.style.position = 'absolute'
document.body.appendChild(floatingEl)
getReferenceClientRect = props.clientRect as () => DOMRect | null
- updatePosition()
+
+ // Positions once, then keeps the popup on the caret through scroll, window
+ // resize and its own height changes (`flip()` measures a height that async
+ // rows and late fonts change after the fact).
+ cleanupAutoUpdate = autoUpdate(
+ {
+ getBoundingClientRect: () =>
+ getReferenceClientRect?.() ?? new DOMRect(),
+ contextElement: props.editor?.view?.dom as HTMLElement | undefined,
+ },
+ floatingEl,
+ updatePosition,
+ )
}
return {
@@ -139,6 +156,9 @@ export function createSuggestionRenderer(
onExit() {
isActive = false
renderToken++
+ positionToken++
+ cleanupAutoUpdate?.()
+ cleanupAutoUpdate = null
floatingEl?.remove()
renderer?.destroy()
floatingEl = null

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
desk/public/timers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,56 +1,45 @@
<template>
<FrappeUIProvider>
<PortalRoot />
<router-view />
</FrappeUIProvider>
<KeymapDialog />
<Dialogs />
</template>
<script setup lang="ts">
import { Dialogs } from "@/components/dialogs";
import KeymapDialog from "@/pages/KeymapDialog.vue";
import { useConfigStore } from "@/stores/config";
import { stopSession } from "@/telemetry";
import { FrappeUIProvider, toast } from "frappe-ui";
import { computed, defineAsyncComponent, h, onMounted, onUnmounted } from "vue";
import { useFavicon } from "@vueuse/core";
import { FrappeUIProvider, setConfig, toast, useColorScheme } from "frappe-ui";
import { storeToRefs } from "pinia";
import { h, onMounted } from "vue";
import Wifi from "~icons/lucide/wifi";
import WifiOff from "~icons/lucide/wifi-off";
import { useAuthStore } from "./stores/auth";
useConfigStore();
import { __ } from "./translation";
import { isCustomerPortal } from "./utils";
const configStore = useConfigStore();
const { favicon } = storeToRefs(configStore);
useFavicon(favicon);
if (!localStorage.getItem("theme")) {
localStorage.setItem("theme", "light");
}
useColorScheme();
onMounted(() => {
window.addEventListener("online", () => {
toast.create({
message: "You are now online",
icon: h(Wifi),
toast(__("You are now online."), {
icon: () => h(Wifi, { class: "text-ink-base" }),
});
});
window.addEventListener("offline", () => {
toast.create({
message: "You are now offline",
icon: h(WifiOff),
toast(__("You are now offline."), {
icon: () => h(WifiOff, { class: "text-ink-base" }),
});
});
});
const AgentPortalRoot = defineAsyncComponent(
() => import("@/pages/desk/AgentRoot.vue")
);
const CustomerPortalRoot = defineAsyncComponent(
() => import("@/pages/CustomerPortalRoot.vue")
);
const PortalRoot = computed(() => {
const authStore = useAuthStore();
if (authStore.hasDeskAccess && authStore.isAgent) {
return AgentPortalRoot;
} else {
return CustomerPortalRoot;
}
});
onUnmounted(() => {
stopSession();
!isCustomerPortal.value && setConfig("localTimezone", window.timezone?.user);
setConfig("systemTimezone", window.timezone?.system || null);
});
</script>

View File

@@ -1,66 +0,0 @@
<template>
<Popover placement="right-start" class="flex w-full">
<template #target="{ togglePopover }">
<button
:class="[
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-gray-800 hover:bg-gray-100',
]"
@click.prevent="togglePopover()"
>
<div class="flex gap-2">
<AppsIcon />
<span class="whitespace-nowrap">Apps</span>
</div>
<ChevronRight class="h-4 w-4 stroke-1.5" />
</button>
</template>
<template #body>
<div
class="grid grid-cols-3 justify-between mx-3 p-2 rounded-lg border border-gray-100 bg-white shadow-xl"
>
<div v-for="app in apps.data" key="name">
<a
:href="app.route"
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-3 hover:bg-gray-100"
>
<img class="size-8" :src="app.logo" />
<div class="text-sm" @click="app.onClick">
{{ app.title }}
</div>
</a>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import { Popover, createResource } from "frappe-ui";
import AppsIcon from "./icons/AppsIcon.vue";
import ChevronRight from "~icons/lucide/chevron-right";
const apps = createResource({
url: "frappe.apps.get_apps",
cache: "apps",
auto: true,
transform: (data) => {
let _apps = [
{
name: "frappe",
logo: "/assets/helpdesk/desk/desk.png",
title: "Desk",
route: "/app",
},
];
data.map((app) => {
if (app.name === "helpdesk") return;
_apps.push({
name: app.name,
logo: app.logo,
title: app.title,
route: app.route,
});
});
return _apps;
},
});
</script>

View File

@@ -1,201 +0,0 @@
<template>
<Dialog
v-model="show"
:options="{
title: 'Assign To',
size: 'xl',
}"
>
<template #body-content>
<AutocompleteNew
v-if="showRestrictedMembers"
placeholder="Search agents"
:model-value="search"
:options="members"
@update:model-value="
({ _, value }) => {
addAssignee(value);
}
"
>
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :name="option.value" size="sm" />
</template>
<template #item-label="{ option }">
<Tooltip :text="option.value">
{{ getUser(option.value).full_name }}
</Tooltip>
</template>
</AutocompleteNew>
<SearchComplete
v-else
class="form-control"
doctype="HD Agent"
:custom-filters="customFilters"
:reset-input="true"
@change="
(option) => {
addAssignee(option.value);
}
"
>
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :name="option.value" size="sm" />
</template>
<template #item-label="{ option }">
<Tooltip :text="option.value">
{{ getUser(option.value).full_name }}
</Tooltip>
</template>
</SearchComplete>
<div class="mt-3 flex flex-wrap items-center gap-2">
<Tooltip
v-for="currentAssignee in assignees"
:key="currentAssignee.name"
:text="currentAssignee.name"
>
<Button
:label="getUser(currentAssignee.name).full_name"
theme="gray"
variant="outline"
>
<template #prefix>
<UserAvatar :name="currentAssignee.name" size="sm" />
</template>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeCurrentAssignee(currentAssignee.name)"
/>
</template>
</Button>
</Tooltip>
</div>
<ErrorMessage v-if="error" class="mt-2" :message="error" />
</template>
</Dialog>
</template>
<script setup lang="ts">
import { AutocompleteNew, SearchComplete, UserAvatar } from "@/components";
import { useAuthStore } from "@/stores/auth";
import { useConfigStore } from "@/stores/config";
import { useUserStore } from "@/stores/user";
import { call, createResource } from "frappe-ui";
import { useOnboarding } from "frappe-ui/frappe";
import { computed, onMounted, ref, watch } from "vue";
const props = defineProps({
doctype: {
type: String,
required: true,
},
docname: {
type: String,
required: true,
},
assignees: {
type: Array,
required: true,
},
team: {
type: String,
default: "",
},
});
const show = defineModel();
const emit = defineEmits(["update"]);
const { getUser } = useUserStore();
const { updateOnboardingStep } = useOnboarding("helpdesk");
const { isManager } = useAuthStore();
const { teamRestrictionApplied, assignWithinTeam } = useConfigStore();
const error = ref("");
const addAssignee = (value) => {
error.value = "";
createResource({
url: "frappe.desk.form.assign_to.add",
auto: true,
params: {
doctype: props.doctype,
name: props.docname,
assign_to: [value],
},
onSuccess: () => {
emit("update");
if (isManager) {
updateOnboardingStep("assign_to_agent");
}
members.value = members.value.filter((m) => m.value !== value);
},
});
emit("update");
};
const removeCurrentAssignee = (value) => {
createResource({
url: "frappe.desk.form.assign_to.remove",
auto: true,
params: {
doctype: props.doctype,
name: props.docname,
assign_to: value,
},
onSuccess: () => {
emit("update");
members.value.push({
label: value,
value: value,
});
},
});
};
const customFilters = computed(() => {
const filters = {};
filters["is_active"] = ["=", 1];
if (Boolean(props.assignees?.length)) {
filters["name"] = ["not in", [...props.assignees.map((a) => a.name)]];
}
return filters;
});
const search = ref("");
const members = ref([]);
async function getMembers() {
let teamMembers = await call(
"helpdesk.helpdesk.doctype.hd_team.hd_team.get_team_members",
{
team: props.team,
}
);
let assignedMembers = props.assignees.map((a) => a.name);
teamMembers = teamMembers.filter((member: string) => {
return !assignedMembers.includes(member);
});
members.value = teamMembers.map((member: string) => {
return {
label: member,
value: member,
};
});
}
const showRestrictedMembers = computed(() => {
return teamRestrictionApplied && assignWithinTeam && props.team;
});
onMounted(() => {
if (showRestrictedMembers.value) {
getMembers();
}
});
</script>

View File

@@ -1,6 +1,6 @@
<template>
<span>
<a :href="isShowable ? null : url" target="_blank">
<a :href="preview ? undefined : url" target="_blank">
<Button
:label="label"
theme="gray"
@@ -8,72 +8,113 @@
@click="toggleDialog()"
>
<template #prefix>
<component :is="getIcon()" class="h-4 w-4" />
<component :is="icon" class="h-4 w-4" />
</template>
<template #suffix>
<slot name="suffix" />
</template>
</Button>
</a>
<Dialog
v-model="showDialog"
:options="{
title: label,
size: '4xl',
}"
>
<template #body-content>
<Dialog v-model:open="showDialog" :title="label" size="4xl">
<template #default>
<div
v-if="isText"
v-if="preview === 'text'"
class="prose prose-sm max-w-none whitespace-pre-wrap"
>
{{ content }}
</div>
<img v-if="isImage" :src="url" class="m-auto rounded border" />
<img
v-if="preview === 'image'"
:src="url"
class="m-auto rounded-4 border"
/>
<video
v-if="preview === 'video'"
:src="url"
controls
class="m-auto max-h-[70vh] rounded-4 border"
/>
</template>
</Dialog>
</span>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Button, Dialog } from "frappe-ui";
import { getType as getMime } from "mime";
import LucideFileType from "~icons/lucide/file-type";
import LucideFileImage from "~icons/lucide/file-image";
import LucideFileText from "~icons/lucide/file-text";
import LucideFileSpreadsheet from "~icons/lucide/file-spreadsheet";
import { markRaw, ref, type Component } from "vue";
import LucideFile from "~icons/lucide/file";
import LucideFileImage from "~icons/lucide/file-image";
import LucideFileSpreadsheet from "~icons/lucide/file-spreadsheet";
import LucideFileText from "~icons/lucide/file-text";
import LucideFileType from "~icons/lucide/file-type";
import LucideFileVideo from "~icons/lucide/file-video";
interface P {
label: string;
url?: string;
url?: string | null;
}
const props = withDefaults(defineProps<P>(), {
url: null,
});
const showDialog = ref(false);
const mimeType = getMime(props.label) || "";
const isImage = mimeType.startsWith("image/");
const isPdf = mimeType === "application/pdf";
const isSpreadsheet = mimeType.includes("spreadsheet");
const isText = mimeType === "text/plain";
const isShowable = props.url && (isText || isImage);
const content = ref("");
type AttachmentKind =
| "image"
| "video"
| "pdf"
| "spreadsheet"
| "text"
| "file";
function getIcon() {
if (isText) return LucideFileType;
else if (isImage) return LucideFileImage;
else if (isPdf) return LucideFileText;
else if (isSpreadsheet) return LucideFileSpreadsheet;
else return LucideFile;
const ICONS: Record<AttachmentKind, Component> = {
image: markRaw(LucideFileImage),
video: markRaw(LucideFileVideo),
pdf: markRaw(LucideFileText),
spreadsheet: markRaw(LucideFileSpreadsheet),
text: markRaw(LucideFileType),
file: markRaw(LucideFile),
};
// browsers render only some image/video mimes (.dwg is image/vnd.dwg, .avi is
// video/x-msvideo) — anything absent here becomes a download link like pdf
type Preview = "image" | "video" | "text";
const PREVIEWS: Record<string, Preview> = {
"image/png": "image",
"image/jpeg": "image",
"image/gif": "image",
"image/webp": "image",
"image/svg+xml": "image",
"image/avif": "image",
"image/bmp": "image",
"image/apng": "image",
"image/vnd.microsoft.icon": "image",
"video/mp4": "video",
"video/webm": "video",
"video/ogg": "video",
"video/x-m4v": "video",
"text/plain": "text",
};
function getKind(mime: string): AttachmentKind {
if (mime.startsWith("image/")) return "image";
if (mime.startsWith("video/")) return "video";
if (mime === "application/pdf") return "pdf";
if (mime.includes("spreadsheet")) return "spreadsheet";
if (mime === "text/plain") return "text";
return "file";
}
const showDialog = ref(false);
const mimeType = getMime(props.label) || "";
const icon = ICONS[getKind(mimeType)];
const preview = props.url ? PREVIEWS[mimeType] : undefined;
const content = ref("");
function toggleDialog() {
if (!isShowable) return;
if (isText) {
if (!preview) return;
if (preview === "text") {
fetch(props.url).then((res) => res.text().then((t) => (content.value = t)));
}
showDialog.value = !showDialog.value;

View File

@@ -0,0 +1,33 @@
<template>
<div class="flex flex-wrap gap-2">
<AttachmentItem
v-for="attachment in attachments"
:key="attachment.file_url"
:label="attachment.file_name"
:url="attachment.file_url"
>
<template #suffix>
<LucideX
class="size-3.5 cursor-pointer text-ink-gray-6 transition-colors hover:text-ink-gray-9"
@click.stop.prevent="emit('remove', attachment)"
/>
</template>
</AttachmentItem>
</div>
</template>
<script setup lang="ts">
import { AttachmentItem } from "@/components";
import LucideX from "~icons/lucide/x";
interface Attachment {
file_url: string;
file_name: string;
}
withDefaults(defineProps<{ attachments?: Attachment[] }>(), {
attachments: () => [],
});
const emit = defineEmits<{ remove: [attachment: Attachment] }>();
</script>

View File

@@ -1,284 +0,0 @@
<template>
<Combobox v-slot="{ open: isComboboxOpen }" v-model="selectedValue" nullable>
<Popover v-model:show="showOptions">
<template #target="{ open: openPopover, togglePopover }">
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
<div class="w-full -ml-0.5">
<button
class="flex w-full items-center justify-between focus:outline-none"
:class="inputClasses"
@click="
() => {
!disabled && togglePopover();
}
"
>
<div class="flex items-center">
<slot name="prefix" />
<span
v-if="selectedValue"
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
>
{{ displayValue(selectedValue) }}
</span>
<span v-else class="text-base leading-5 text-gray-500">
{{ placeholder || "" }}
</span>
</div>
<FeatherIcon
name="chevron-down"
class="h-4 w-4 text-gray-600"
aria-hidden="true"
/>
</button>
</div>
</slot>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl">
<div class="relative px-1.5 pt-0.5">
<ComboboxInput
ref="search"
class="form-input w-full"
type="text"
:value="query"
autocomplete="off"
placeholder="Search"
@change="
(e) => {
query = e.target.value;
}
"
/>
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="selectedValue = null"
>
<FeatherIcon name="x" class="w-4" />
</button>
</div>
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
>
<div
v-for="group in groups"
v-show="group.items.length > 0"
:key="group.key"
class="mt-1.5"
>
<div
v-if="group.group && !group.hideLabel"
class="px-2.5 py-1.5 text-sm font-medium text-gray-500"
>
{{ group.group }}
</div>
<ComboboxOption
v-for="option in group.items"
:key="option.value"
v-slot="{ active, selected }"
as="template"
:value="option"
>
<li
:class="[
'flex items-center rounded px-2.5 py-1.5 text-base',
{ 'bg-gray-100': active },
]"
>
<slot
name="item-prefix"
v-bind="{ active, selected, option }"
/>
<slot
name="item-label"
v-bind="{ active, selected, option }"
>
{{ option.label }}
</slot>
</li>
</ComboboxOption>
</div>
<li
v-if="groups.length == 0"
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-gray-600"
>
No results found
</li>
</ComboboxOptions>
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
<slot
name="footer"
v-bind="{ value: search?.el._value, close }"
></slot>
</div>
</div>
</div>
</template>
</Popover>
</Combobox>
</template>
<script setup>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from "@headlessui/vue";
import { Popover, Button, FeatherIcon } from "frappe-ui";
import { ref, computed, useAttrs, useSlots, watch, nextTick } from "vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
options: {
type: Array,
default: () => [],
},
size: {
type: String,
default: "md",
},
variant: {
type: String,
default: "subtle",
},
placeholder: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
filterable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(["update:modelValue", "update:query", "change"]);
const query = ref("");
const showOptions = ref(false);
const search = ref(null);
const attrs = useAttrs();
const slots = useSlots();
const valuePropPassed = computed(() => "value" in attrs);
const selectedValue = computed({
get() {
return valuePropPassed.value ? attrs.value : props.modelValue;
},
set(val) {
query.value = "";
if (val) {
showOptions.value = false;
}
emit(valuePropPassed.value ? "change" : "update:modelValue", val);
},
});
function close() {
showOptions.value = false;
}
const groups = computed(() => {
if (!props.options || props.options.length == 0) return [];
let groups = props.options[0]?.group
? props.options
: [{ group: "", items: props.options }];
return groups
.map((group, i) => {
return {
key: i,
group: group.group,
hideLabel: group.hideLabel || false,
items: props.filterable ? filterOptions(group.items) : group.items,
};
})
.filter((group) => group.items.length > 0);
});
function filterOptions(options) {
if (!query.value) {
return options;
}
return options.filter((option) => {
let searchTexts = [option.label, option.value];
return searchTexts.some((text) =>
(text || "").toString().toLowerCase().includes(query.value.toLowerCase())
);
});
}
function displayValue(option) {
if (typeof option === "string") {
let allOptions = groups.value.flatMap((group) => group.items);
let selectedOption = allOptions.find((o) => o.value === option);
return selectedOption?.label || option;
}
return option?.label;
}
watch(query, (q) => {
emit("update:query", q);
});
watch(showOptions, (val) => {
if (val) {
nextTick(() => {
search.value.el.focus();
});
}
});
const textColor = computed(() => {
return props.disabled ? "text-gray-600" : "text-gray-800";
});
const inputClasses = computed(() => {
let sizeClasses = {
sm: "text-base rounded h-7",
md: "text-base rounded h-8",
lg: "text-lg rounded-md h-10",
xl: "text-xl rounded-md h-10",
}[props.size];
let paddingClasses = {
sm: "py-1.5 px-2",
md: "py-1.5 px-2.5",
lg: "py-1.5 px-3",
xl: "py-1.5 px-3",
}[props.size];
let variant = props.disabled ? "disabled" : props.variant;
let variantClasses = {
subtle:
"border border-gray-100 bg-gray-100 placeholder-gray-500 hover:border-gray-200 hover:bg-gray-200 focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400",
outline:
"border border-gray-300 bg-white placeholder-gray-500 hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400",
disabled: [
"border bg-gray-50 placeholder-gray-400",
props.variant === "outline" ? "border-gray-300" : "border-transparent",
],
}[variant];
return [
sizeClasses,
paddingClasses,
variantClasses,
textColor.value,
"transition-colors w-full",
];
});
defineExpose({ query });
</script>

View File

@@ -0,0 +1,32 @@
<template>
<Select
:model-value="agentStatusStore.myStatus"
:options="dropdownOptions"
:placeholder="__('Set status')"
variant="subtle"
@update:model-value="agentStatusStore.setMyStatus"
>
<template #item-prefix="{ item }">
<div
class="size-2 rounded-full flex-shrink-0"
:class="agentStatusStore.statusColor(item.value)"
/>
</template>
</Select>
</template>
<script setup lang="ts">
import { useAgentStatusStore } from "@/stores/agentStatus";
import { __ } from "@/translation";
import { Select } from "frappe-ui";
import { computed } from "vue";
const agentStatusStore = useAgentStatusStore();
const dropdownOptions = computed(() =>
agentStatusStore.statusOptions.map((statusOption: string) => ({
label: __(statusOption),
value: statusOption,
}))
);
</script>

View File

@@ -0,0 +1,26 @@
<template>
<Button
variant="ghost"
icon-left="lucide-chevron-left"
:label="label"
:size="size"
class="!justify-start truncate font-medium text-ink-gray-8 hover:bg-transparent hover:opacity-70 focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 active:bg-transparent active:outline-none active:ring-0 rtl:flex-row-reverse ps-1"
@click="$emit('back')"
/>
</template>
<script setup lang="ts">
import { Button } from "frappe-ui";
interface P {
label: string;
size?: string;
}
interface E {
(event: "back"): void;
}
withDefaults(defineProps<P>(), { size: "md" });
defineEmits<E>();
</script>

View File

@@ -0,0 +1,283 @@
<template>
<div class="w-full h-full overflow-hidden">
<CardBase
:title="title"
:text="chartData.text"
:percentageChange="chartData.percentageChange"
:chartConfig="hasData ? chartConfig : placeholderChartConfig"
:currentDuration="currentDuration"
:orientation="orientation"
:timelineFilter="timelineFilter"
@changeDuration="changeDuration"
>
<template v-if="$slots.text" #text>
<slot name="text" v-bind="{ text: chartData.text }" />
</template>
</CardBase>
</div>
</template>
<script setup lang="ts">
import { __ } from "@/translation";
import { buildPercentageChange } from "@/utils";
import { EChartsOption } from "echarts";
import { createResource } from "frappe-ui";
import { computed, onMounted, ref, type PropType } from "vue";
import CardBase from "./ChartCardBase.vue";
interface BarChartData {
percentage_change?: number;
total?: number;
average?: number;
data: { date: string; count: number }[] | Record<number, number>;
total_reviews?: number;
}
const props = defineProps({
title: {
type: String,
required: true,
},
apiUrl: {
type: String,
required: false,
},
data: {
type: Object as PropType<BarChartData>,
required: true,
},
barColor: {
type: String,
default: "#3BBDE5",
},
dt: {
type: String,
required: false,
},
dn: {
type: String,
required: false,
},
orientation: {
type: String as PropType<"vertical" | "horizontal">,
default: "vertical",
},
measure: {
type: String as PropType<"total" | "average">,
default: "total",
},
negativeIsBetter: {
type: Boolean,
default: true,
},
timelineFilter: {
type: Boolean,
default: true,
},
});
// the untranslated key: it is lowercased straight into the API period
const currentDuration = ref("Last month");
const chartData = computed(() => {
const isDataFetched = resource.fetched;
const _data: BarChartData = isDataFetched ? resource.data : props.data;
// Star-distribution shape: data is { 1: n, 2: n, ... }
if (!props.timelineFilter && _data?.data && !Array.isArray(_data.data)) {
const distribution = _data.data as Record<number, number>;
const values = [1, 2, 3, 4, 5].map((s) => distribution[s] ?? 0);
return {
labels: ["1", "2", "3", "4", "5"],
counts: values,
percentageChange: buildPercentageChange(0, props.negativeIsBetter),
text: _data?.average ?? 0,
isStarDistribution: true,
};
}
// Time-series shape: data is [{ date, count }, ...]
const timeData = Array.isArray(_data?.data)
? (_data.data as { date: string; count: number }[])
: [];
const labels = timeData.map((item) => item.date);
const counts = timeData.map((item) => item.count);
const _percentageChange = _data?.percentage_change || 0;
const percentageChange = buildPercentageChange(
_percentageChange,
props.negativeIsBetter
);
return {
labels,
counts,
percentageChange,
text: props.measure === "average" ? _data?.average ?? 0 : _data?.total ?? 0,
isStarDistribution: false,
};
});
const hasData = computed(() => {
const counts = chartData.value.counts as number[];
return counts.length > 0 && counts.some((c) => c > 0);
});
const chartConfig = computed<EChartsOption>(() => {
const color = props.barColor;
if (chartData.value.isStarDistribution) {
const values = chartData.value.counts as number[];
// rank by value descending: index 0 = highest, 4 = lowest
const opacities = [1, 0.7, 0.5, 0.3, 0.2];
const baseColor = props.barColor; // #E79913
// unique sorted counts descending (for tie-aware rank lookup)
const uniqueSorted = [...new Set(values)].sort((a, b) => b - a);
const opacityByIndex: number[] = values.map((v) => {
const rank = uniqueSorted.indexOf(v); // same count → same rank
return opacities[Math.min(rank, opacities.length - 1)];
});
// helper: hex + opacity → rgba
const hexToRgba = (hex: string, alpha: number) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
};
return {
grid: { left: 4, right: 4, top: 16, bottom: 20, containLabel: false },
xAxis: {
type: "category",
data: ["1", "2", "3", "4", "5"],
axisLine: { show: true, lineStyle: { color: "#ededed" } },
axisTick: { show: false },
axisLabel: { color: "#6b7280", fontSize: 11 },
},
yAxis: { type: "value", show: false, min: 0 },
series: [
{
type: "bar",
data: values.map((value, i) => ({
value: value === 0 ? 0 : value,
itemStyle: {
color: hexToRgba(baseColor, opacityByIndex[i]),
borderRadius: [4, 4, 0, 0],
},
label: {
show: true,
position: "top" as const,
formatter: value > 0 ? String(value) : "",
color: "#6b7280",
fontSize: 11,
},
})),
barWidth: 16,
emphasis: { disabled: true },
},
],
tooltip: { show: false },
};
}
return {
xAxis: {
type: "category",
data: chartData.value.labels,
show: false,
boundaryGap: true,
},
yAxis: {
type: "value",
show: false,
},
series: [
{
data: (chartData.value.counts as number[]).map((value) => ({
value,
itemStyle: {
color: value === 0 ? "#e5e7eb" : color,
borderRadius: [3, 3, 0, 0],
},
})),
type: "bar",
barMaxWidth: 12,
barMinHeight: 4,
emphasis: { disabled: true },
label: { show: false },
},
],
grid: {
left: 2,
right: 2,
top: 2,
bottom: 2,
},
tooltip: {
show: true,
trigger: "axis",
appendToBody: true,
position: (point: number[]) => [point[0] + 12, point[1] - 50],
axisPointer: { type: "none" },
formatter: (params: any) => {
const p = Array.isArray(params) ? params[0] : params;
if (!p.value) return "";
return `<span style="font-size:12px;color:#6b7280">${p.name}: <b style="color:#374151">${p.value}</b></span>`;
},
},
};
});
const placeholderChartConfig = computed<EChartsOption>(() => {
const placeholderValues = Array.from(
{ length: 7 },
() => Math.floor(Math.random() * 8) + 3
);
return {
xAxis: { type: "category", show: false, boundaryGap: true },
yAxis: { type: "value", show: false },
series: [
{
data: placeholderValues.map((value) => ({
value,
itemStyle: { color: "#f1f1f1", borderRadius: [3, 3, 0, 0] },
})),
type: "bar",
barMaxWidth: 12,
emphasis: { disabled: true },
},
],
grid: { left: 2, right: 2, top: 2, bottom: 2 },
tooltip: { show: false },
animation: false,
};
});
const resource = createResource({
url: props.apiUrl || "",
type: "GET",
makeParams: () => {
const params: Record<string, any> = {
period: currentDuration.value.toLowerCase(),
};
if (props.dt && props.dn) {
params.dt = props.dt;
params.dn = props.dn;
}
return params;
},
});
const changeDuration = (period: string) => {
currentDuration.value = period;
if (props.apiUrl) resource.submit();
};
onMounted(() => {
if (props.timelineFilter && props.apiUrl && !props.data?.data) {
resource.submit();
}
});
</script>

View File

@@ -5,7 +5,7 @@
alt="Brand Logo"
class="h-8 w-8 shrink-0 object-cover"
/>
<HDLogo v-else class="h-8 w-8 shrink-0 rounded" />
<HDLogo v-else class="h-8 w-8 shrink-0 rounded-4" />
</template>
<script setup lang="ts">

View File

@@ -7,7 +7,7 @@
:label="activity?._caller?.label"
size="md"
/>
<span class="font-medium text-ink-gray-8 ml-1">
<span class="font-medium text-ink-gray-8 ms-1">
{{ activity?._caller?.label }}
</span>
<span>{{
@@ -16,7 +16,7 @@
: __("has made a call")
}}</span>
</div>
<div class="ml-auto whitespace-nowrap">
<div class="ms-auto whitespace-nowrap">
<Tooltip :text="dateFormat(activity.creation, 'MMM D, dddd')">
<div class="text-sm text-ink-gray-5">
{{ __(timeAgo(activity.creation)) }}
@@ -26,10 +26,10 @@
</div>
<div
@click="showCallLogDetailModal = true"
class="flex flex-col gap-2 cursor-pointer border-transparent shadow rounded-md bg-surface-cards px-3 py-2.5 text-ink-gray-9"
class="flex flex-col gap-2 cursor-pointer border-transparent shadow rounded-5 bg-surface-elevation-1 px-3 py-2.5 text-ink-gray-9"
>
<div class="flex items-center justify-between">
<div class="inline-flex gap-2 items-center text-base font-medium">
<div class="inline-flex gap-2 items-center text-base-medium">
<div>
{{
activity.call_type == "Incoming"

View File

@@ -1,106 +0,0 @@
<template>
<Dialog
v-model="show"
:options="{
title: 'Canned Responses',
size: '4xl',
}"
>
<template #body-content>
<TextInput
ref="searchInput"
v-model="search"
type="text"
:placeholder="'Site Down'"
>
<template #prefix>
<FeatherIcon name="search" class="h-4 w-4 text-gray-500" />
</template>
</TextInput>
<div
v-if="filteredTemplates.length"
class="mt-2 grid max-h-[560px] grid-cols-1 md:grid-cols-3 gap-2 overflow-y-auto"
>
<div
v-for="template in filteredTemplates"
:key="template.name"
class="flex h-56 cursor-pointer flex-col gap-2 rounded-lg border p-3 hover:bg-gray-100"
@click="emit('apply', template)"
>
<div class="border-b pb-2 text-base font-semibold">
{{ template.title }}
</div>
<TextEditor
v-if="template.message"
:content="template.message"
:editable="false"
editor-class="!prose-sm max-w-none !text-sm text-gray-600 focus:outline-none"
class="flex-1 overflow-hidden"
/>
</div>
</div>
<div v-else class="mt-2">
<div class="flex h-56 flex-col items-center justify-center">
<div class="text-lg text-gray-500">
{{ "No templates found" }}
</div>
</div>
</div>
<div class="flex justify-end mt-4">
<Button
label="New Canned Response"
@click="
() => {
$router.push('/canned-responses#new');
templates.data = null;
}
"
/>
</div>
</template>
</Dialog>
</template>
<script setup>
import { TextEditor, createListResource } from "frappe-ui";
import { ref, computed, nextTick, watch, onMounted } from "vue";
const props = defineProps({
doctype: {
type: String,
default: "",
},
});
const show = defineModel();
const searchInput = ref("");
const emit = defineEmits(["apply"]);
const search = ref("");
const templates = createListResource({
type: "list",
doctype: "HD Canned Response",
cache: ["cannedResponses", props.doctype],
fields: ["title", "message", "modified"],
orderBy: "modified desc",
pageLength: 99999,
});
onMounted(() => {
if (templates.data == null) {
templates.fetch();
}
});
const filteredTemplates = computed(() => {
return (
templates.data?.filter((template) => {
return template.title.toLowerCase().includes(search.value.toLowerCase());
}) ?? []
);
});
watch(show, (value) => value && nextTick(() => searchInput.value?.el?.focus()));
</script>

View File

@@ -0,0 +1,173 @@
<template>
<div
class="flex flex-col w-full h-full rounded-5 p-4"
:class="[orientation == 'horizontal' && 'pt-3']"
>
<slot name="title">
<div class="text-ink-gray-5 text-base mb-2">
{{ title }}
</div>
</slot>
<div
class="flex flex-col gap-2 h-full w-full"
v-if="orientation === 'vertical'"
>
<div class="flex items-end w-full gap-2">
<slot name="text">
<div
class="text-2xl-medium text-center text-ink-gray-8 whitespace-nowrap"
>
{{ text }}
</div>
</slot>
<div v-if="timelineFilter" class="flex items-center text-sm gap-1">
<div class="flex items-center gap-1" :class="percentageChange.color">
<span
v-if="percentageChange.icon"
:class="percentageChange.icon"
class="size-4"
/>
<div>{{ percentageChange.value }}%</div>
</div>
<Dropdown :options="durationOptions">
<div
class="flex items-center gap-0.5 text-ink-gray-5 hover:text-ink-gray-6 cursor-pointer shrink-0"
>
<div class="flex gap-1">
<span>vs</span>
<span>{{ __(currentDuration).toLowerCase() }}</span>
</div>
<LucideChevronDown class="size-4" />
</div>
<template #item-label="{ item }">
<div
class="data-[disabled]:cursor-not-allowed group flex w-full items-center rounded-4 px-2 text-base focus:outline-none focus:bg-surface-gray-3 data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3 whitespace-nowrap text-ink-gray-7 cursor-pointer justify-between"
>
<span>
{{ item.label }}
</span>
</div>
</template>
<template #item-suffix="{ item }">
<LucideCheck
v-if="item.label == __(currentDuration)"
class="size-4"
/>
</template>
</Dropdown>
</div>
</div>
<slot name="chart">
<div v-if="chartConfig" class="w-full h-full">
<ECharts :options="chartConfig" class="w-full h-full" />
</div>
</slot>
</div>
<div class="flex flex-col gap-2 h-full w-full" v-else>
<div class="flex items-end w-full gap-2 justify-between flex-1">
<slot name="text">
<span
class="text-2xl-medium text-center text-ink-gray-8 whitespace-nowrap"
>
{{ text }}
</span>
</slot>
<slot name="chart">
<div v-if="chartConfig" class="h-full max-w-[50%] w-[50%]">
<ECharts :options="chartConfig" class="w-full h-full" />
</div>
</slot>
</div>
<div v-if="timelineFilter" class="flex items-center text-sm gap-1">
<div class="flex items-center gap-1" :class="percentageChange.color">
<span
v-if="percentageChange.icon"
:class="percentageChange.icon"
class="size-4"
/>
<div>{{ percentageChange.value }}%</div>
</div>
<Dropdown :options="durationOptions">
<div
class="flex items-center gap-0.5 text-ink-gray-5 hover:text-ink-gray-6 cursor-pointer shrink-0"
>
vs {{ __(currentDuration).toLowerCase() }}
<LucideChevronDown class="size-4" />
</div>
<template #item-label="{ item }">
<div
class="data-[disabled]:cursor-not-allowed group flex w-full items-center rounded-4 px-2 text-base focus:outline-none focus:bg-surface-gray-3 data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3 whitespace-nowrap text-ink-gray-7 cursor-pointer justify-between"
>
<span>
{{ item.label }}
</span>
</div>
</template>
<template #item-suffix="{ item }">
<LucideCheck
v-if="item.label == __(currentDuration)"
class="size-4"
/>
</template>
</Dropdown>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import LucideCheck from "~icons/lucide/check";
import LucideChevronDown from "~icons/lucide/chevron-down";
import { __ } from "@/translation";
import { EChartsOption } from "echarts";
import { Dropdown } from "frappe-ui";
import { ECharts } from "frappe-ui/experimental";
import { computed, type PropType } from "vue";
const props = defineProps({
title: {
type: String,
},
text: {
type: [Number, String] as PropType<number | string>,
default: "",
},
percentageChange: {
type: Object,
required: true,
},
chartConfig: {
type: Object as PropType<EChartsOption>,
required: true,
},
currentDuration: {
type: String,
default: __("Last month"),
},
orientation: {
type: String as PropType<"vertical" | "horizontal">,
default: "vertical",
},
timelineFilter: {
type: Boolean,
default: true,
},
});
const chartConfig = computed<EChartsOption>(() => props.chartConfig);
const currentDuration = computed(() => props.currentDuration);
const emit = defineEmits(["changeDuration"]);
// the emitted value is the untranslated key: it travels to the API as the
// period, so only the label goes through __()
const DURATIONS = ["Last week", "Last month", "Last 3 months"];
const durationOptions = DURATIONS.map((duration) => ({
label: __(duration),
onClick: () => {
if (currentDuration.value !== duration) emit("changeDuration", duration);
},
}));
</script>

View File

@@ -1,199 +0,0 @@
<template>
<div class="flex-col text-base flex-1" ref="commentBoxRef">
<div class="mb-1 ml-0.5 flex items-center justify-between">
<div class="text-gray-600 flex items-center gap-2">
<Avatar
size="md"
:label="commenter"
:image="getUser(commentedBy).user_image"
/>
<p>
<span class="font-medium text-gray-800">
{{ commenter }}
</span>
<span> added a</span>
<span class="max-w-xs truncate font-medium text-gray-800">
comment
</span>
</p>
</div>
<div class="flex items-center gap-1">
<Tooltip :text="dateFormat(creation, dateTooltipFormat)">
<span class="pl-0.5 text-sm text-gray-600">
{{ timeAgo(creation) }}
</span>
</Tooltip>
<div v-if="authStore.userId === commentedBy && !editable">
<Dropdown
:placement="'right'"
:options="[
{
label: 'Edit',
onClick: () => handleEditMode(),
icon: 'edit-2',
condition: () => !isTicketMergedComment,
},
{
label: 'Delete',
onClick: () => (showDialog = true),
icon: 'trash-2',
},
]"
>
<Button
icon="more-horizontal"
class="text-gray-600"
variant="ghost"
/>
</Dropdown>
</div>
</div>
</div>
<div class="rounded bg-gray-50 px-4 py-3">
<TextEditor
ref="editorRef"
:editor-class="[
'prose-f shrink text-p-sm transition-all duration-300 ease-in-out block w-full content',
getFontFamily(_content),
]"
:content="_content"
:editable="editable"
:bubble-menu="textEditorMenuButtons"
@change="(event:string) => {_content = event}"
>
<template #bottom v-if="editable">
<div class="flex flex-row-reverse gap-2">
<Button label="Save" @click="handleSaveComment" variant="solid" />
<Button label="Discard" @click="handleDiscard" />
</div>
</template>
</TextEditor>
<div class="flex flex-wrap gap-2" v-if="!editable">
<AttachmentItem
v-for="a in attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
</div>
<Dialog
v-model="showDialog"
:options="{
title: 'Delete Comment',
message: 'Are you sure you want to confirm this action?',
actions: [
{ label: 'Cancel', onClick: () => (showDialog = false) },
{
label: 'Delete',
onClick: () => deleteComment.submit(),
variant: 'solid',
},
],
}"
/>
</template>
<script setup lang="ts">
import { AttachmentItem } from "@/components";
import { useAuthStore } from "@/stores/auth";
import { updateRes as updateComment } from "@/stores/knowledgeBase";
import { useUserStore } from "@/stores/user";
import { CommentActivity } from "@/types";
import {
dateFormat,
dateTooltipFormat,
getFontFamily,
isContentEmpty,
textEditorMenuButtons,
timeAgo,
} from "@/utils";
import {
Avatar,
Dialog,
Dropdown,
TextEditor,
createResource,
toast,
} from "frappe-ui";
import { PropType, computed, onMounted, ref } from "vue";
const authStore = useAuthStore();
const props = defineProps({
activity: {
type: Object as PropType<CommentActivity>,
required: true,
},
});
const { getUser } = useUserStore();
const { name, creation, content, commenter, commentedBy, attachments } =
props.activity;
const isTicketMergedComment = computed(() => {
const regex = /has been merged with ticket #\d+/;
return regex.test(content);
});
const emit = defineEmits(["update"]);
const showDialog = ref(false);
const editable = ref(false);
const _content = ref(content);
// HTML refs
const commentBoxRef = ref(null);
const editorRef = ref(null);
function handleEditMode() {
editable.value = true;
editorRef.value.editor.chain().focus("start");
}
function handleDiscard() {
_content.value = content;
editable.value = false;
}
const deleteComment = createResource({
url: "frappe.client.delete",
makeParams: () => ({
doctype: "HD Ticket Comment",
name: name,
}),
onSuccess() {
emit("update");
toast.success("Comment deleted");
},
});
function handleSaveComment() {
if (content === _content.value) {
editable.value = false;
return;
}
if (isContentEmpty(_content.value)) {
toast.error("Comment cannot be empty");
return;
}
updateComment.submit(
{
doctype: "HD Ticket Comment",
name: name,
fieldname: "content",
value: _content.value,
},
{
onSuccess: () => {
editable.value = false;
emit("update");
toast.success("Comment updated");
},
}
);
}
onMounted(() => {
commentBoxRef.value.style.width = "0px";
});
</script>

View File

@@ -1,141 +1,142 @@
<template>
<TextEditor
v-if="agentsList.data"
<Editor
ref="editorRef"
:editor-class="[
'prose-sm max-w-none',
editable &&
'min-h-[7rem] mx-6 md:ml-10 md:mr-9 max-h-[50vh] overflow-y-auto border-t py-3',
getFontFamily(newComment),
]"
:content="newComment"
:starterkit-options="{ heading: { levels: [2, 3, 4, 5, 6] } }"
:placeholder="placeholder"
v-model="newComment"
:extensions="extensions"
:editable="editable"
:mentions="agents"
@change="editable ? (newComment = $event) : null"
:extensions="[PreserveVideoControls]"
:uploadFunction="(file:any)=>uploadFunction(file, doctype, ticketId)"
:placeholder="placeholder"
:upload-function="
(file: any, options: any) =>
track(uploadFunction(file, doctype, ticketId, true, options))
"
>
<template #bottom>
<div v-if="editable" class="flex flex-col gap-2 px-6 md:pl-10 md:pr-9">
<!-- Attachments -->
<div class="flex flex-wrap gap-2">
<AttachmentItem
v-for="a in attachments"
:key="a.file_url"
:label="a.file_name"
:url="!['MOV', 'MP4'].includes(a.file_type) ? a.file_url : null"
>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeAttachment(a)"
/>
</template>
</AttachmentItem>
</div>
<!-- Fixed Menu -->
<div class="flex justify-between overflow-hidden border-t py-2.5">
<div class="flex items-center overflow-x-auto w-[60%]">
<TextEditorFixedMenu
class="-ml-1"
:buttons="textEditorMenuButtons"
/>
<FileUploader
:upload-args="{
doctype: doctype,
docname: ticketId,
private: true,
}"
@success="(f) => attachments.push(f)"
>
<template #default="{ openFileSelector }">
<Button
theme="gray"
variant="ghost"
@click="openFileSelector()"
<template #default="{ isEmpty }">
<!-- Scroll here so selected nodes aren't clipped. -->
<div :class="editable && 'max-h-[44vh] overflow-y-auto'">
<EditorContent
:class="[
'prose-sm max-w-none',
editable && 'min-h-[7rem] mx-5 border-t py-3',
getFontFamily(newComment),
]"
/>
</div>
<!-- Attachments -->
<AttachmentList
class="my-2 ms-5"
:attachments="attachments"
@remove="removeAttachment"
/>
<div v-if="editable" class="flex flex-col gap-2 border-t">
<div class="px-4">
<!-- Fixed Menu -->
<div class="flex justify-between gap-2 overflow-hidden py-2.5">
<div class="flex min-w-0 flex-1 items-center overflow-x-auto">
<div class="inline-flex items-center gap-1.5 p-1">
<FileUploader
:doctype="doctype"
:docname="ticketId"
private
@success="(f) => attachments.push(f)"
>
<template #icon>
<AttachmentIcon
class="h-4"
style="color: #000000; stroke-width: 1.5 !important"
/>
<template #default="{ openFileSelector, uploading }">
{{ void (loading = uploading) }}
<button
class="flex rounded-4 p-1 text-ink-gray-8 transition-colors focus-within:ring-0 hover:bg-surface-gray-3"
@click="openFileSelector()"
:disabled="uploading"
>
<LoadingIndicator v-if="uploading" class="h-4 w-4" />
<AttachmentIcon
v-else
class="h-4 w-4"
style="stroke-width: 1.5 !important"
/>
</button>
</template>
</Button>
</template>
</FileUploader>
</div>
<div class="flex items-center justify-end space-x-2 w-[40%]">
<Button
label="Discard"
@click="
() => {
newComment = '';
attachments = [];
emit('discard');
}
"
/>
<Button
variant="solid"
:label="label"
:disabled="commentEmpty"
:loading="loading"
@click="
() => {
loading = true;
submitComment();
newComment = '';
}
"
/>
</FileUploader>
<div class="h-4 w-[2px] border-s ml-1" />
</div>
<EditorFixedMenu :items="fullToolbar" />
<EditorTableMenu />
</div>
<div class="flex shrink-0 items-center justify-end gap-x-2">
<Button
label="Discard"
@click="
() => {
newComment = '';
attachments = [];
dropUnused(null);
emit('discard');
}
"
/>
<!-- A disabled button fires no pointer events, so the span
carries the hover for the tooltip -->
<Tooltip
:text="isUploading ? __('Please wait, media is uploading') : ''"
>
<span class="inline-flex">
<Button
variant="solid"
:label="label"
:disabled="isDisabled"
:loading="loading"
@click="submitComment()"
/>
</span>
</Tooltip>
</div>
</div>
</div>
</div>
</template>
</TextEditor>
</Editor>
</template>
<script setup lang="ts">
import {
FileUploader,
TextEditor,
TextEditorFixedMenu,
LoadingIndicator,
Tooltip,
createResource,
dayjs,
toast,
} from "frappe-ui";
import { useOnboarding } from "frappe-ui/frappe";
import {
Editor,
EditorContent,
EditorFixedMenu,
EditorTableMenu,
} from "frappe-ui/editor";
import { addPendingActivity } from "@framework/ui/ActivityTimeline";
import { useOnboarding } from "@framework/ui";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { AttachmentItem } from "@/components/";
import { AttachmentList } from "@/components/";
import { buildEditorExtensions, fullToolbar } from "@/components/editor/config";
import { AttachmentIcon } from "@/components/icons/";
import { useTyping } from "@/composables/realtime";
import { useUploadTracker } from "@/composables/useUploadTracker";
import { useAgentStore } from "@/stores/agent";
import { useAuthStore } from "@/stores/auth";
import { PreserveVideoControls } from "@/tiptap-extensions";
import { useUserStore } from "@/stores/user";
import { __ } from "@/translation";
import { capture } from "@/telemetry";
import {
getFontFamily,
isContentEmpty,
removeAttachmentFromServer,
textEditorMenuButtons,
uploadFunction,
} from "@/utils";
import { useStorage } from "@vueuse/core";
import { storeToRefs } from "pinia";
const { updateOnboardingStep } = useOnboarding("helpdesk");
const { agents: agentsList } = useAgentStore();
const { isManager } = useAuthStore();
onMounted(() => {
if (
agentsList.loading ||
agentsList.data?.length ||
agentsList.list.promise
) {
return;
}
agentsList.fetch();
});
const { updateOnboardingStep } = useOnboarding("helpdesk") ?? {};
const { agents: agentsList, dropdown } = storeToRefs(useAgentStore());
const authStore = useAuthStore();
const { isManager } = authStore;
const { getUser } = useUserStore();
const props = defineProps({
ticketId: {
@@ -160,43 +161,25 @@ const props = defineProps({
},
});
const emit = defineEmits(["submit", "discard"]);
const emit = defineEmits(["submit", "discard", "sending", "restore"]);
const newComment = useStorage("commentBoxContent" + props.ticketId, null);
// Mentions as a reactive getter so the `@` list stays in sync as agents load.
const extensions = buildEditorExtensions({
mentions: () => dropdown.value ?? [],
});
// Initialize typing composable
const { onUserType, cleanup } = useTyping(props.ticketId);
const attachments = ref([]);
const commentEmpty = computed(() => {
return isContentEmpty(newComment.value);
const { isUploading, track, dropUnused } = useUploadTracker();
const isDisabled = computed(() => {
return isContentEmpty(newComment.value) || loading.value || isUploading.value;
});
const loading = ref(false);
// Watch for changes in comment content to trigger typing events
watch(newComment, (newValue, oldValue) => {
if (newValue !== oldValue && newValue) {
onUserType();
}
});
onBeforeUnmount(() => {
cleanup();
});
const label = computed(() => {
return loading.value ? "Sending..." : props.label;
});
const agents = computed(() => {
return (
agentsList.data?.map((agent) => ({
label: agent.agent_name.trimEnd(),
value: agent.name,
})) || []
);
});
function removeAttachment(attachment) {
attachments.value = attachments.value.filter((a) => a !== attachment);
removeAttachmentFromServer(attachment.name);
@@ -206,28 +189,58 @@ async function submitComment() {
if (isContentEmpty(newComment.value)) {
return false;
}
// The keyboard shortcut reaches here without passing the disabled button
if (isUploading.value || loading.value) return false;
const content = newComment.value;
const sentAttachments = attachments.value;
const pendingRow = addPendingActivity(props.doctype, props.ticketId, {
type: "comment",
timestamp: dayjs().format("YYYY-MM-DD HH:mm:ss"),
author: {
email: getUser(authStore.userId).email,
// the users list may still be a stub with a guessed name and no avatar
fullname: authStore.userName,
image: authStore.userImage,
},
data: { name: "", content, attachments: sentAttachments },
});
// drop before clearing, or an unmount mid-send deletes what the comment carries
dropUnused(content);
newComment.value = null;
attachments.value = [];
loading.value = true;
emit("sending");
const comment = createResource({
url: "run_doc_method",
makeParams: () => ({
dt: props.doctype,
dn: props.ticketId,
method: "new_comment",
args: {
content: newComment.value,
attachments: attachments.value,
},
args: { content, attachments: sentAttachments },
}),
onSuccess: () => {
onSuccess: (res: { message?: string } | string) => {
// run_doc_method answers with the whole body, since it always carries `docs`
const name = typeof res === "string" ? res : res?.message;
// the real row replaces the pending one the moment it arrives; unkeyed,
// it would outlive it
name ? pendingRow.resolve(`comment:${name}`) : pendingRow.drop();
capture("comment_added");
if (isManager) {
updateOnboardingStep("comment_on_ticket");
updateOnboardingStep?.("comment_on_ticket");
}
emit("submit");
loading.value = false;
attachments.value = [];
newComment.value = null;
},
onError: () => {
toast.error(__("Could not add the comment"));
pendingRow.drop();
newComment.value = content;
attachments.value = sentAttachments;
loading.value = false;
emit("restore");
},
});
@@ -237,6 +250,33 @@ async function submitComment() {
const editorRef = ref(null);
const editor = computed(() => editorRef.value?.editor);
// Watch for changes in comment content to trigger typing events
watch(newComment, (newValue, oldValue) => {
if (newValue !== oldValue && newValue) {
onUserType();
}
});
onMounted(() => {
if (
agentsList.value.loading ||
agentsList.value.data?.length ||
agentsList.value.list.promise
) {
return;
}
agentsList.value.fetch();
});
onBeforeUnmount(() => {
cleanup();
// the saved draft still references what it shows, so only what it dropped goes
dropUnused(newComment.value);
if (isContentEmpty(newComment.value)) {
localStorage.removeItem("commentBoxContent" + props.ticketId);
}
});
defineExpose({
submitComment,
editor,

View File

@@ -1,14 +1,16 @@
<template>
<div class="comm-area">
<div class="comm-area" resizable="true">
<div
class="flex justify-between gap-3 border-t px-6 md:px-10 py-4 md:py-2.5"
class="flex justify-between gap-3 border-t px-6 md:px-5 py-4 md:py-2.5"
>
<div class="flex gap-1.5 items-center">
<Button
ref="sendEmailRef"
variant="ghost"
label="Reply"
:class="[showEmailBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
:class="[
showEmailBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
]"
@click="toggleEmailBox()"
>
<template #prefix>
@@ -18,7 +20,9 @@
<Button
variant="ghost"
label="Comment"
:class="[showCommentBox ? '!bg-gray-300 hover:!bg-gray-200' : '']"
:class="[
showCommentBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
]"
@click="toggleCommentBox()"
>
<template #prefix>
@@ -28,67 +32,87 @@
<TypingIndicator :ticketId="ticketId" />
</div>
</div>
<div
v-show="showEmailBox"
class="flex gap-1.5 flex-1"
@keydown.ctrl.enter.capture.stop="submitEmail"
@keydown.meta.enter.capture.stop="submitEmail"
>
<EmailEditor
ref="emailEditorRef"
:label="
isMobileView ? 'Send' : isMac ? 'Send (⌘ + ⏎)' : 'Send (Ctrl + ⏎)'
"
v-model:content="content"
placeholder="Hi John, we are looking into this issue."
:ticketId="ticketId"
:to-emails="toEmails"
:cc-emails="ccEmails"
:bcc-emails="bccEmails"
@submit="
() => {
showEmailBox = false;
emit('update');
}
"
@discard="
() => {
showEmailBox = false;
}
"
/>
</div>
<div
v-show="showCommentBox"
@keydown.ctrl.enter.capture.stop="submitComment"
@keydown.meta.enter.capture.stop="submitComment"
>
<CommentTextEditor
ref="commentTextEditorRef"
:label="
isMobileView
? 'Comment'
: isMac
? 'Comment (⌘ + ⏎)'
: 'Comment (Ctrl + ⏎)'
"
:ticketId="ticketId"
:editable="showCommentBox"
:doctype="doctype"
placeholder="@John could you please look into this?"
@submit="
() => {
showCommentBox = false;
emit('update');
}
"
@discard="
() => {
showCommentBox = false;
}
"
/>
</div>
<Transition name="slide">
<div
v-show="showEmailBox"
ref="emailBoxRef"
@keydown.ctrl.enter.capture.stop="submitEmail"
@keydown.meta.enter.capture.stop="submitEmail"
@keydown.esc.capture.stop="showEmailBox = false"
>
<div class="overflow-hidden">
<EmailEditor
ref="emailEditorRef"
:label="
isMobileView ? 'Send' : isMac ? 'Send (⌘ + ⏎)' : 'Send (Ctrl + ⏎)'
"
placeholder="Hi John, we are looking into this issue."
:ticketId="ticketId"
:to-emails="toEmails"
:cc-emails="ccEmails"
:bcc-emails="bccEmails"
@sending="
() => {
showEmailBox = false;
}
"
@submit="() => emit('update')"
@restore="
() => {
showEmailBox = true;
}
"
@discard="
() => {
showEmailBox = false;
}
"
/>
</div>
</div>
</Transition>
<Transition name="slide">
<div
v-show="showCommentBox"
ref="commentBoxRef"
@keydown.ctrl.enter.capture.stop="submitComment"
@keydown.meta.enter.capture.stop="submitComment"
@keydown.esc.capture.stop="showCommentBox = false"
>
<div class="overflow-hidden">
<CommentTextEditor
ref="commentTextEditorRef"
:label="
isMobileView
? 'Comment'
: isMac
? 'Comment (⌘ + ⏎)'
: 'Comment (Ctrl + ⏎)'
"
:ticketId="ticketId"
:editable="showCommentBox"
:doctype="doctype"
placeholder="@John could you please look into this?"
@sending="
() => {
showCommentBox = false;
}
"
@submit="() => emit('update')"
@restore="
() => {
showCommentBox = true;
}
"
@discard="
() => {
showCommentBox = false;
}
"
/>
</div>
</div>
</Transition>
</div>
</template>
@@ -97,7 +121,9 @@ import { CommentTextEditor, EmailEditor, TypingIndicator } from "@/components";
import { CommentIcon, EmailIcon } from "@/components/icons/";
import { useDevice } from "@/composables";
import { useScreenSize } from "@/composables/screen";
import { useShortcut } from "@/composables/shortcuts";
import { showCommentBox, showEmailBox } from "@/pages/ticket/modalStates";
import { onClickOutside } from "@vueuse/core";
import { ref, watch } from "vue";
const emit = defineEmits(["update"]);
@@ -108,6 +134,8 @@ let doc = defineModel();
// let doc = inject(TicketSymbol)?.value.doc
const emailEditorRef = ref(null);
const commentTextEditorRef = ref(null);
const emailBoxRef = ref(null);
const commentBoxRef = ref(null);
function toggleEmailBox() {
if (showCommentBox.value) {
@@ -123,25 +151,31 @@ function toggleCommentBox() {
showCommentBox.value = !showCommentBox.value;
}
// the editors emit `submit` once the write lands; reloading here too would
// fetch twice, and the guards' `false` never reaches this side anyway
function submitEmail() {
if (emailEditorRef.value.submitMail()) {
emit("update");
}
emailEditorRef.value.submitMail();
}
function submitComment() {
if (commentTextEditorRef.value.submitComment()) {
emit("update");
commentTextEditorRef.value.submitComment();
}
function splitIfString(str: string | string[]) {
if (typeof str === "string") {
return str.split(",");
}
return str;
}
function replyToEmail(data: object) {
showEmailBox.value = true;
emailEditorRef.value.addToReply(
data.content,
data.to?.split(","),
data.cc?.split(","),
data.bcc?.split(",")
splitIfString(data.to),
splitIfString(data.cc),
splitIfString(data.bcc)
);
}
@@ -172,7 +206,7 @@ watch(
() => showEmailBox.value,
(value) => {
if (value) {
emailEditorRef.value?.editor?.commands?.focus();
emailEditorRef.value?.editor?.commands?.focus("start");
}
}
);
@@ -186,12 +220,64 @@ watch(
}
);
useShortcut("r", () => {
toggleEmailBox();
});
useShortcut("c", () => {
toggleCommentBox();
});
defineExpose({
replyToEmail,
toggleEmailBox,
toggleCommentBox,
editor: emailEditorRef,
});
const IGNORED_SELECTORS = [
".tippy-box",
".tippy-content",
".PopoverContent",
'[role="dialog"]',
'[role="presentation"]',
'[role="menu"]',
".dialog-overlay",
// Grammarly suggestions appear outside the box, allow them to stop collapsing.
"grammarly-extension",
"grammarly-popups",
"[data-grammarly-part]",
];
// `ignore` is only consulted on pointerdown, which dialogs stop, so the click
// through has to be checked too. Without this a dialog button closes the box.
function isIgnored(event: Event): boolean {
const target = event.target as HTMLElement | null;
return Boolean(target?.closest?.(IGNORED_SELECTORS.join(", ")));
}
onClickOutside(
emailBoxRef,
(event) => {
if (showEmailBox.value && !isIgnored(event)) {
showEmailBox.value = false;
}
},
{
ignore: IGNORED_SELECTORS,
}
);
onClickOutside(
commentBoxRef,
(event) => {
if (showCommentBox.value && !isIgnored(event)) {
showCommentBox.value = false;
}
},
{
ignore: IGNORED_SELECTORS,
}
);
</script>
<style>
@@ -200,4 +286,18 @@ defineExpose({
width: 100vw;
}
}
.slide-enter-active,
.slide-leave-active {
display: grid;
transition: grid-template-rows 0.25s ease;
}
.slide-enter-from,
.slide-leave-to {
grid-template-rows: 0fr;
}
.slide-enter-to,
.slide-leave-from {
grid-template-rows: 1fr;
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div class="flex flex-col">
<Editor
ref="editorRef"
v-model="internalContent"
:extensions="extensions"
v-bind="uploadFn ? { uploadFunction: uploadFn } : {}"
:placeholder="placeholder"
>
<template #default>
<EditorBubbleMenu :items="commentToolbar" />
<div
class="flex items-center overflow-x-auto rounded-t-4 border border-b-0 border-outline-gray-1 bg-surface-gray-1 px-1 py-1"
>
<div
v-if="showAttachments"
class="inline-flex items-center gap-1.5 pe-1"
>
<FileUploader private @success="addAttachment">
<template #default="{ openFileSelector, uploading }">
{{ syncUploadingState(uploading) }}
<button
class="flex rounded-4 p-1 text-ink-gray-8 transition-colors hover:bg-surface-gray-3 disabled:opacity-40"
:disabled="uploading"
@click="openFileSelector()"
>
<LoadingIndicator v-if="uploading" class="h-4 w-4" />
<AttachmentIcon
v-else
class="h-4 w-4"
style="stroke-width: 1.5 !important"
/>
</button>
</template>
</FileUploader>
<div class="h-4 w-[2px] border-s ml-1" />
</div>
<EditorFixedMenu :items="fullToolbar" />
<EditorTableMenu />
</div>
<EditorContent :class="editorClass" />
</template>
</Editor>
<AttachmentList
v-if="showAttachments && attachments?.length"
class="mt-2"
:attachments="attachments ?? []"
@remove="removeAttachment"
/>
</div>
</template>
<script setup lang="ts">
import { AttachmentList } from "@/components";
import {
buildEditorExtensions,
commentToolbar,
fullToolbar,
} from "@/components/editor/config";
import { AttachmentIcon } from "@/components/icons";
import { getUserEmailInfo } from "@/composables/useUserEmailInfo";
import { isContentEmpty } from "@/utils";
import { FileUploader, LoadingIndicator, type UploadedFile } from "frappe-ui";
import {
Editor,
EditorBubbleMenu,
EditorContent,
EditorFixedMenu,
EditorTableMenu,
} from "frappe-ui/editor";
import { computed, nextTick, onMounted, ref, watch } from "vue";
type UploadFunction = (file: File) => Promise<UploadedFile>;
const props = withDefaults(
defineProps<{
placeholder?: string;
minHeight?: string;
maxHeight?: string;
extensions?: any[];
showSignature?: boolean;
showAttachments?: boolean;
uploadFn?: UploadFunction;
}>(),
{
placeholder: "",
minHeight: "min-h-[180px]",
maxHeight: "max-h-80",
extensions: () => [],
showSignature: false,
showAttachments: false,
}
);
const internalContent = defineModel<string>({ default: "" });
const editorRef = ref<any>(null);
const attachments = defineModel<UploadedFile[] | null>("attachments", {
default: () => [],
});
const isUploading = ref(false);
const extensions = buildEditorExtensions({ extra: props.extensions });
const contentClass = [
"!prose-sm max-w-full overflow-auto py-1.5 px-3",
"rounded-b-4 border border-outline-gray-1 bg-surface-base",
"placeholder-ink-gray-4 text-ink-gray-8 -mt-0.5",
];
const editorClass = computed(() => [
...contentClass,
props.minHeight,
props.maxHeight,
]);
const userResource = getUserEmailInfo();
function getDefaultContent(signature: string): string {
return `<p></p><p></p>${signature}`;
}
function applySignature(signature: string) {
if (!isContentEmpty(internalContent.value)) return;
const commands = editorRef.value?.editor?.commands;
if (commands) {
commands.setContent(signature);
setTimeout(() => {
editorRef.value?.editor?.commands?.focus("start");
}, 0);
} else {
internalContent.value = signature;
}
}
function addAttachment(file: UploadedFile) {
attachments.value = [...(attachments.value ?? []), file];
}
function removeAttachment(attachment: UploadedFile) {
attachments.value = (attachments.value ?? []).filter((a) => a !== attachment);
}
// FileUploader exposes `uploading` only via slot scope, so mirror it into a
// ref that the parent can read through the exposed `isUploading`.
function syncUploadingState(uploading: boolean): string {
isUploading.value = uploading;
return "";
}
function getContent(): string {
return editorRef.value?.editor?.getHTML() ?? "";
}
function isEmpty(): boolean {
return !editorRef.value?.editor?.state?.doc?.textContent?.trim()?.length;
}
function reset() {
const commands = editorRef.value?.editor?.commands;
if (!commands) {
internalContent.value = "";
return;
}
const data = userResource?.data as { email_signature?: string } | null;
const sig = data?.email_signature
? getDefaultContent(data.email_signature)
: "";
commands.setContent(sig || "<p></p>");
internalContent.value = sig;
attachments.value = null;
setTimeout(() => {
editorRef.value?.editor?.commands?.focus("start");
}, 0);
}
if (props.showSignature) {
// Late arrival: resource resolves after mount
watch(
() => userResource.data,
(data: { email_signature?: string } | null) => {
if (!data?.email_signature) return;
applySignature(getDefaultContent(data.email_signature));
}
);
}
onMounted(() => {
if (!props.showSignature) return;
nextTick(() => {
const data = userResource.data as { email_signature?: string } | null;
if (data?.email_signature) {
applySignature(getDefaultContent(data.email_signature));
}
});
});
defineExpose({
getContent,
isEmpty,
reset,
editorRef,
isUploading,
});
</script>

View File

@@ -1,8 +1,12 @@
<template>
<Dialog v-model="showDialog" :options="{ title }" @close="closeDialog">
<template #body-content>
<Dialog v-model:open="isOpen" :title="title" @close="closeDialog">
<template #default>
<div class="space-y-4">
<p class="text-p-base text-gray-800" v-if="message" v-html="message" />
<p
class="text-p-base text-ink-gray-8"
v-if="message"
v-html="message"
/>
</div>
</template>
<template #actions>
@@ -28,6 +32,12 @@ export default {
message: {
type: String,
},
// Optional v-model; when absent, visibility falls back to the internal
// state (open on mount), preserving the `v-if` + `show()`/`hide()` mode.
modelValue: {
type: Boolean,
default: undefined,
},
onConfirm: {
type: Function,
default: null,
@@ -37,6 +47,7 @@ export default {
default: null,
},
},
emits: ["update:modelValue"],
expose: ["show", "hide"],
components: {
Dialog,
@@ -48,6 +59,19 @@ export default {
isLoading: false,
};
},
computed: {
isOpen: {
get() {
return this.modelValue !== undefined
? this.modelValue
: this.showDialog;
},
set(value) {
this.showDialog = value;
this.$emit("update:modelValue", value);
},
},
},
methods: {
async handleConfirmation() {
if (this.isLoading) return;
@@ -66,14 +90,14 @@ export default {
}
},
show() {
this.showDialog = true;
this.isOpen = true;
},
closeDialog() {
this.hide();
this.onCancel?.();
},
hide() {
this.showDialog = false;
this.isOpen = false;
},
},
};

View File

@@ -1,13 +1,14 @@
<template>
<div v-if="normalActions.length" class="flex gap-2">
<!-- onClick rides along in `action`; binding it twice would run it twice -->
<Button
v-for="action in normalActions"
:key="action.label"
:label="action.label"
@click="action.onClick()"
v-bind="action"
>
<template v-if="action.icon" #prefix>
<FeatherIcon :name="action.icon" class="h-4 w-4" />
<Icon :icon="action.icon" class="h-4 w-4" />
</template>
</Button>
</div>
@@ -16,8 +17,8 @@
<Dropdown v-slot="{ open }" :options="g.action">
<Button :label="g.label">
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
<component
:is="open ? LucideChevronUp : LucideChevronDown"
class="h-4"
/>
</template>
@@ -26,11 +27,14 @@
</div>
</div>
<Dropdown v-if="groupedActions.length" :options="groupedActions">
<Button icon="more-horizontal" variant="ghost" />
<Button icon="lucide-more-horizontal" variant="ghost" />
</Dropdown>
</template>
<script setup>
import LucideChevronUp from "~icons/lucide/chevron-up";
import LucideChevronDown from "~icons/lucide/chevron-down";
import Icon from "@/components/Icon.vue";
import { Dropdown } from "frappe-ui";
import { computed } from "vue";

View File

@@ -0,0 +1,89 @@
<template>
<Dialog v-model:open="open" :title="dialogTitle" size="md">
<template #default>
<div class="flex flex-col gap-4">
<p class="text-p-base text-ink-gray-7">{{ message }}</p>
<Checkbox
v-if="count"
size="sm"
v-model="deleteLinkedTickets"
:label="__('Delete {0} ticket(s)', [count])"
/>
</div>
</template>
<template #actions>
<div class="flex justify-end">
<Button
variant="solid"
theme="red"
icon-left="lucide-trash-2"
:label="__('Delete')"
:loading="isDeleting"
@click="confirmDelete"
/>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { __ } from "@/translation";
import { Button, Checkbox, createResource, Dialog } from "frappe-ui";
import { computed, ref, watch } from "vue";
const props = defineProps<{
/** Name of the record being deleted. */
name: string;
/** HD Ticket link field that ties tickets to this record. */
linkField: "customer" | "contact";
/** Confirmation copy shown above the checkbox. */
message: string;
title?: string;
/** Async delete handler; the button stays in a loading state until it settles. */
onDelete: (payload: { deleteLinkedTickets: boolean }) => Promise<void> | void;
}>();
const open = defineModel<boolean>({ default: false });
const resource = createResource({
url: "frappe.client.get_count",
params: {
doctype: "HD Ticket",
filters: { [props.linkField]: props.name },
},
auto: true,
});
const count = computed<number>(() => {
if (resource.loading) return 0;
return resource.data ?? 0;
});
const deleteLinkedTickets = ref(false);
const isDeleting = ref(false);
const dialogTitle = computed(() => props.title ?? __("Delete"));
async function confirmDelete() {
if (isDeleting.value) return;
isDeleting.value = true;
try {
await props.onDelete({ deleteLinkedTickets: deleteLinkedTickets.value });
open.value = false;
} catch {
// Keep the dialog open on failure; the handler surfaces the error.
} finally {
isDeleting.value = false;
}
}
watch(
open,
(isOpen) => {
if (isOpen) {
resource.reload();
}
},
{ immediate: true }
);
</script>

View File

@@ -34,9 +34,11 @@ function handleDiscard() {
},
actions: [
{
label: "Confirm",
label: "Delete",
theme: "red",
iconLeft: "lucide-trash-2",
variant: "solid",
onClick(close: Function) {
onClick({ close }: { close: () => void }) {
emit("discard");
close();
},

View File

@@ -1,7 +1,10 @@
<template>
<Tooltip text="View documentation">
<a :href="url" target="_blank">
<lucide-info class="h-4 w-4 text-ink-gray-6" :class="color" />
<lucide-circle-question-mark
class="h-4 w-4 text-ink-gray-6"
:class="color"
/>
</a>
</Tooltip>
</template>

View File

@@ -1,240 +0,0 @@
<template>
<div
v-bind="$attrs"
class="grow cursor-pointer border-transparent bg-white rounded-md shadow text-base leading-6 transition-all duration-300 ease-in-out"
>
<div
class="flex items-center justify-between gap-2"
:class="isMobileView && 'items-start'"
>
<!-- email design for mobile -->
<div v-if="isMobileView" class="flex items-center gap-2 text-sm">
<div class="leading-tight">
<p>{{ sender.full_name || "No name found" }}</p>
<Tooltip :text="dateFormat(creation, dateTooltipFormat)">
<p class="text-xs md:text-sm text-gray-600">
{{ timeAgo(creation) }}
</p>
</Tooltip>
<p class="sm:flex hidden text-sm text-gray-600" v-if="sender.name">
{{ "<" + sender.name + ">" }}
</p>
</div>
</div>
<!-- email design for desktop -->
<div v-else class="flex items-center gap-1">
<span>{{ sender.full_name || "No name found" }}</span>
<span class="sm:flex hidden text-sm text-gray-600" v-if="sender.name">{{
"<" + sender.name + ">"
}}</span>
</div>
<div class="flex gap-0.5 items-center">
<Badge
v-if="status.label"
:label="__(status.label)"
variant="subtle"
:theme="status.color"
class="mr-1.5"
/>
<Tooltip
:text="dateFormat(creation, dateTooltipFormat)"
v-if="!isMobileView"
>
<p class="text-xs md:text-sm text-gray-600">
{{ timeAgo(creation) }}
</p>
</Tooltip>
<Button
variant="ghost"
class="text-gray-700"
@click="
emit('reply', {
content: content,
to: sender?.name ?? to,
})
"
>
<ReplyIcon class="h-4 w-4" />
</Button>
<Button
variant="ghost"
class="text-gray-700"
@click="
emit('reply', {
content: content,
to: to ?? sender.name,
cc: cc ? cc : [],
bcc: bcc ? bcc : [],
})
"
>
<ReplyAllIcon class="h-4 w-4" />
</Button>
<Dropdown
v-if="showSplitOption"
:placement="'right'"
:options="[
{
label: 'Split Ticket',
icon: LucideSplit,
onClick: () => (showSplitModal = true),
},
]"
>
<Button
icon="more-horizontal"
class="text-gray-600"
variant="ghost"
/>
</Dropdown>
</div>
</div>
<!-- <div class="text-sm leading-5 text-gray-600">
{{ subject }}
</div> -->
<div class="text-sm leading-5 text-gray-600">
<span v-if="to" class="text-2xs mr-1 font-bold text-gray-500">TO:</span>
<span v-if="to"> {{ to }} </span>
<span v-if="cc">, </span>
<span v-if="cc" class="text-2xs mr-1 font-bold text-gray-500"> CC: </span>
<span v-if="cc">{{ cc }}</span>
<span v-if="bcc">, </span>
<span v-if="bcc" class="text-2xs mr-1 font-bold text-gray-500">
BCC:
</span>
<span v-if="bcc">{{ bcc }}</span>
</div>
<div class="border-0 border-t my-3 border-outline-gray-modals" />
<EmailContent :content="content" />
<div class="flex flex-wrap gap-2">
<AttachmentItem
v-for="a in attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
<TicketSplitModal
v-model="showSplitModal"
:ticket_id="name"
:communication_id="name"
/>
</template>
<script setup lang="ts">
import { AttachmentItem } from "@/components";
import { useScreenSize } from "@/composables/screen";
import { dateFormat, dateTooltipFormat, timeAgo } from "@/utils";
import { Dropdown } from "frappe-ui";
import { computed, ref } from "vue";
import LucideSplit from "~icons/lucide/split";
import { ReplyAllIcon, ReplyIcon } from "./icons";
import TicketSplitModal from "./ticket/TicketSplitModal.vue";
const props = defineProps({
activity: {
type: Object,
required: true,
},
showSplitOption: {
type: Boolean,
default: false,
},
});
const {
sender,
to,
cc,
bcc,
creation,
subject,
attachments,
content,
name,
deliveryStatus,
} = props.activity;
const emit = defineEmits(["reply"]);
const { isMobileView } = useScreenSize();
const showSplitModal = ref(false);
const status = computed(() => {
let _status = deliveryStatus;
let indicator_color = "red";
if (["Sent", "Clicked"].includes(_status)) {
indicator_color = "green";
} else if (["Sending", "Scheduled"].includes(_status)) {
indicator_color = "orange";
} else if (["Opened", "Read"].includes(_status)) {
indicator_color = "blue";
} else if (_status == "Error") {
indicator_color = "red";
}
return { label: _status, color: indicator_color };
});
// TODO: Implement reply functionality using this way instead of emit drillup
// function reply(email, reply_all = false) {
// emailBox.toggleEmailBox();
// let editor = emailBox.editor;
// let message = email.content;
// let recipients = sender.name;
// editor.toEmails = [email.sender];
// editor.cc = editor.bcc = false;
// editor.ccEmails = [];
// editor.bccEmails = [];
// console.log(recipients);
// if (!email.subject.startsWith("Re:")) {
// editor.subject = `Re: ${email.subject}`;
// } else {
// editor.subject = email.subject;
// }
// if (reply_all) {
// let cc = email.cc?.split(",").map((r) => r.trim());
// let bcc = email.bcc?.split(",").map((r) => r.trim());
// if (cc?.length) {
// recipients = recipients.filter((r) => !cc?.includes(r));
// cc.push(...recipients);
// } else {
// cc = recipients;
// }
// editor.cc = cc ? true : false;
// editor.bcc = bcc ? true : false;
// editor.ccEmails = cc;
// editor.bccEmails = bcc;
// }
// let repliedMessage = `<blockquote>${message}</blockquote>`;
// editor.editor
// .chain()
// .clearContent()
// .insertContent("<p>.</p>")
// .updateAttributes("paragraph", { class: "reply-to-content" })
// .insertContent(repliedMessage)
// .focus("all")
// .insertContentAt(0, { type: "paragraph" })
// .focus("start")
// .run();
// }
</script>
<style>
.email-content {
max-width: 100%;
}
.email-content > * {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
</style>

View File

@@ -6,9 +6,9 @@
/>
</template>
<script setup>
import { ref, watch } from "vue";
import { getFontFamily } from "@/utils";
<script setup lang="ts">
import { dataTheme, getFontFamily, stripEmailColors } from "@/utils";
import { computed, ref, watch } from "vue";
const props = defineProps({
content: {
@@ -17,12 +17,30 @@ const props = defineProps({
},
});
const files = import.meta.globEager("/src/index.css", { query: "?inline" });
const css = files["/src/index.css"].default;
const iframeRef = ref(null);
const iframeRef = ref<HTMLIFrameElement | null>(null);
const _content = ref(props.content);
const themedContent = computed(() =>
dataTheme.value === "dark" ? stripEmailColors(_content.value) : _content.value
);
// Get CSS path - in dev Vite serves it directly, in prod we need the built path
const cssHref = computed(() => {
if (import.meta.env.DEV) {
return "/src/index.css";
}
// In production, find the built CSS file from the document
const links = document.querySelectorAll('link[rel="stylesheet"]');
for (const link of links) {
const href = link.getAttribute("href");
if (href?.includes("/assets/helpdesk/desk/") && href.endsWith(".css")) {
return href;
}
}
// Fallback to a reasonable path
return "/assets/helpdesk/desk/index.css";
});
const parser = new DOMParser();
const doc = parser.parseFromString(_content.value, "text/html");
@@ -30,7 +48,11 @@ const gmailReplyToContent = doc.querySelectorAll("div.gmail_quote");
const outlookReplyToContent = doc.querySelectorAll("div#appendonsend");
const replyToContent = doc.querySelectorAll("p.reply-to-content");
if (gmailReplyToContent.length) {
if (doc.body.children.length === 0) {
// Plain-text emails (e.g. from Thunderbird) arrive as text with newlines and no spacing losing formatting.
const plainText = doc.body.innerHTML.replace(/\n\n/g, "\n");
_content.value = `<div class="whitespace-pre-wrap">${plainText}</div>`;
} else if (gmailReplyToContent.length) {
_content.value = parseReplyToContent(doc, "div.gmail_quote", true);
} else if (outlookReplyToContent.length) {
_content.value = parseReplyToContent(doc, "div#appendonsend");
@@ -38,8 +60,12 @@ if (gmailReplyToContent.length) {
_content.value = parseReplyToContent(doc, "p.reply-to-content");
}
function parseReplyToContent(doc, selector, forGmail = false) {
function handleAllInstances(doc) {
function parseReplyToContent(
doc: Document,
selector: string,
forGmail = false
) {
function handleAllInstances(doc: Document) {
const replyToContentElements = doc.querySelectorAll(selector);
if (replyToContentElements.length === 0) return;
const replyToContentElement = replyToContentElements[0];
@@ -48,13 +74,15 @@ function parseReplyToContent(doc, selector, forGmail = false) {
}
handleAllInstances(doc);
return doc.body.innerHTML;
}
function replaceReplyToContent(replyToContentElement, forGmail) {
function replaceReplyToContent(
replyToContentElement: Element,
forGmail: boolean
) {
if (!replyToContentElement) return;
let randomId = Math.random().toString(36).substring(2, 7);
const randomId = Math.random().toString(36).substring(2, 7);
const wrapper = doc.createElement("div");
wrapper.classList.add("replied-content");
@@ -75,166 +103,153 @@ function replaceReplyToContent(replyToContentElement, forGmail) {
if (prevSibling && prevSibling.tagName === "BR") {
prevSibling.remove();
}
let cloned = replyToContentElement.cloneNode(true);
const cloned = replyToContentElement.cloneNode(true) as Element;
cloned.classList.remove("gmail_quote");
wrapper.appendChild(cloned);
} else {
const allSiblings = Array.from(
replyToContentElement.parentElement.children
replyToContentElement.parentElement?.children || []
);
const replyToContentIndex = allSiblings.indexOf(replyToContentElement);
const followingSiblings = allSiblings.slice(replyToContentIndex + 1);
if (followingSiblings.length === 0) return;
let clonedFollowingSiblings = followingSiblings.map((sibling) =>
const clonedFollowingSiblings = followingSiblings.map((sibling) =>
sibling.cloneNode(true)
);
const div = doc.createElement("div");
div.append(...clonedFollowingSiblings);
wrapper.append(div);
// Remove all siblings after the reply-to-content element
for (let i = replyToContentIndex + 1; i < allSiblings.length; i++) {
replyToContentElement.parentElement.removeChild(allSiblings[i]);
replyToContentElement.parentElement?.removeChild(allSiblings[i]);
}
}
replyToContentElement.parentElement.replaceChild(
replyToContentElement.parentElement?.replaceChild(
wrapper,
replyToContentElement
);
}
const htmlContent = `
const htmlContent = computed(
() => `
<!DOCTYPE html>
<html>
<head>
<style>
${css}
.replied-content .collapse {
margin: 10px 0 10px 0;
visibility: visible;
cursor: pointer;
display: flex;
font-size: larger;
font-weight: 700;
height: 12px;
line-height: 0.1;
background: #e8eaed;
width: 23px;
justify-content: center;
border-radius: 5px;
}
.replied-content .collapse:hover {
background: #dadce0;
}
.replied-content .collapse + input {
display: none;
}
.replied-content .collapse + input + div {
display: none;
}
.replied-content .collapse + input:checked + div {
display: block;
}
.email-content {
word-break: break-word;
}
.email-content
:is(:where(table):not(:where([class~='not-prose'], [class~='not-prose']
*))) {
table-layout: auto;
}
.email-content
:where(table):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
width: unset;
table-layout: auto;
text-align: unset;
margin-top: unset;
margin-bottom: unset;
font-size: unset;
line-height: unset;
}
/* tr */
.email-content
:where(tbody tr):not(:where([class~='not-prose'], [class~='not-prose']
*)) {
border-bottom-width: 0;
border-bottom-color: transparent;
}
/* td */
.email-content
:is(:where(td):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
position: unset;
border-width: 0;
border-color: transparent;
padding: 0;
}
.email-content
:where(tbody td):not(:where([class~='not-prose'], [class~='not-prose']
*)) {
vertical-align: revert;
}
/* image */
.email-content
:is(:where(img):not(:where([class~='not-prose'], [class~='not-prose']
*))) {
border-width: 0;
}
.email-content
:where(img):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
margin: 0;
}
/* before & after */
.email-content
:where(blockquote
p:first-of-type):not(:where([class~='not-prose'], [class~='not-prose']
*))::before {
content: none;
}
.email-content
:where(blockquote
p:last-of-type):not(:where([class~='not-prose'], [class~='not-prose']
*))::after {
content: none;
}
</style>
<link rel="stylesheet" href="${cssHref.value}" />
<base target="_blank" />
<style>
:root {
--bg-surface-gray-3: #ededed;
--bg-surface-gray-4: #e2e2e2;
}
[data-theme='dark'] {
--bg-surface-gray-3: #343434;
--bg-surface-gray-4: #424242;
}
.replied-content .collapse {
margin: 10px 0 10px 0;
visibility: visible;
cursor: pointer;
display: flex;
font-size: larger;
font-weight: 700;
height: 12px;
line-height: 0.1;
color: #383838;
background: #e8eaed;
width: 23px;
justify-content: center;
border-radius: 5px;
}
.replied-content .collapse:hover {
background: #dadce0;
}
[data-theme='dark'] .replied-content .collapse {
color: #e2e2e2;
background: #424242;
}
[data-theme='dark'] .replied-content .collapse:hover {
background: #525252;
}
.replied-content .collapse + input {
display: none;
}
.replied-content .collapse + input + div {
display: none;
}
.replied-content .collapse + input:checked + div {
display: block;
}
.email-content {
word-break: break-word;
}
.email-content :is(:where(img):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
border-width: 0;
}
.email-content :where(img):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
margin: 0;
}
.email-content :where(blockquote p:first-of-type):not(:where([class~='not-prose'], [class~='not-prose'] *))::before {
content: none;
}
.email-content :where(blockquote p:last-of-type):not(:where([class~='not-prose'], [class~='not-prose'] *))::after {
content: none;
}
</style>
</head>
<body>
<div ref="emailContentRef" class="email-content prose-f">${_content.value}</div>
<div class="email-content prose-f">${themedContent.value}</div>
</body>
<base target="_blank" />
</html>
`;
`
);
watch(iframeRef, (iframe) => {
if (iframe) {
iframe.onload = () => {
const emailContent =
iframe.contentWindow.document.querySelector(".email-content");
let parent = emailContent.closest("html");
emailContent.classList.add(getFontFamily(_content.value));
iframe.contentWindow?.document.querySelector(".email-content");
if (!emailContent) return;
const parent = emailContent.closest("html");
if (!parent) return;
parent.setAttribute("data-theme", dataTheme.value);
const font = getFontFamily(_content.value);
if (font) emailContent.classList.add(font);
iframe.style.height = parent.offsetHeight + 1 + "px";
let replyCollapsers = emailContent.querySelectorAll(".replyCollapser");
// Clicks inside the iframe don't bubble to the parent document, popovers/dropdowns that close on outside-click never fire.
iframe.contentDocument?.addEventListener("pointerdown", () => {
document.dispatchEvent(
new PointerEvent("pointerdown", { bubbles: true })
);
});
// This is to ensure that keyboard shortcuts work even when the iframe is focused. For example, pressing "r" to reply to an email should work even if the user has clicked inside the email content.
iframe.contentDocument?.addEventListener("keydown", (e) => {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: e.key,
code: e.code,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
bubbles: true,
})
);
});
const replyCollapsers = emailContent.querySelectorAll(".replyCollapser");
if (replyCollapsers.length) {
replyCollapsers.forEach((replyCollapser) => {
replyCollapser.addEventListener("change", () => {
@@ -245,4 +260,9 @@ watch(iframeRef, (iframe) => {
};
}
});
watch(dataTheme, (theme) => {
const html = iframeRef.value?.contentDocument?.documentElement;
if (html) html.setAttribute("data-theme", theme);
});
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,397 @@
<template>
<div>
<div ref="pills" class="flex flex-wrap gap-1">
<Button
v-for="value in values"
:key="value"
:label="value"
theme="gray"
variant="subtle"
:tooltip="copyOnClick ? __('Click to copy') : undefined"
:class="[
{
'rounded-4 !bg-surface-base hover:!bg-surface-gray-1 focus-visible:ring-outline-gray-4':
variant === 'subtle',
},
copyOnClick
? 'cursor-pointer transition-transform active:scale-[0.98]'
: '',
]"
@click="copyOnClick ? copy(value) : null"
@keydown.delete.capture.stop="removeLastValue"
>
<template #suffix>
<LucideX class="size-3.5" @click.stop="removeValue(value)" />
</template>
</Button>
<div class="flex-1 min-w-32">
<ComboboxRoot
:model-value="tempSelection"
:open="showOptions"
@update:open="(o) => (showOptions = o)"
@update:modelValue="onSelect"
:ignore-filter="true"
highlight-on-hover
>
<ComboboxAnchor
class="flex h-7 max-w-full w-auto items-center gap-2 rounded-4 px-2 py-1 border border-transparent"
:class="[
variant == 'ghost'
? 'bg-surface-base hover:bg-surface-base'
: 'bg-surface-gray-2 hover:bg-surface-gray-3',
inputClass,
]"
>
<ComboboxInput
ref="search"
:value="query"
autocomplete="off"
class="bg-transparent p-0 outline-none border-0 text-base text-ink-gray-8 h-full placeholder:text-ink-gray-4 w-full focus:outline-none focus:ring-0 focus:border-0"
:placeholder="placeholder"
@focus="showOptions = true"
@input="onInput"
@keydown.delete.capture.stop="removeLastValue"
@keydown="onKeydown"
@paste="onPaste"
/>
</ComboboxAnchor>
<ComboboxPortal>
<ComboboxContent
class="z-[100] mt-1 min-w-48 w-auto max-w-96 bg-surface-elevation-2 overflow-hidden rounded-6 shadow-2xl ring-1 ring-black ring-opacity-5"
position="popper"
:align="'start'"
@openAutoFocus.prevent
@closeAutoFocus.prevent
>
<ComboboxViewport class="max-h-60 overflow-auto p-1.5">
<ComboboxEmpty
class="flex gap-2 rounded-4 px-2 py-1 text-base text-ink-gray-5"
>
<LucideSearch class="size-4" />
{{ __(emptyPlaceholder) }}
</ComboboxEmpty>
<ComboboxItem
v-for="option in options"
:key="option.value"
:value="option.value"
class="text-base leading-none text-ink-gray-7 rounded-4 flex items-center px-2 py-1 relative select-none data-[highlighted]:outline-none data-[highlighted]:bg-surface-gray-3 cursor-pointer"
@mousedown.prevent="onSelect(option.value)"
>
<UserAvatar
class="mr-0.5 shrink-0"
:name="getUsernameLabel(option.label)"
size="lg"
/>
<div class="flex min-w-0 flex-col gap-1 p-1 text-ink-gray-8">
<div class="truncate text-base-medium">
{{ getUsernameLabel(option.label) }}
</div>
<div class="truncate text-sm text-ink-gray-5">
{{ option.isCustom ? customEmailLabel : option.value }}
</div>
</div>
</ComboboxItem>
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</ComboboxRoot>
</div>
</div>
<ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" />
<div
v-if="info"
class="whitespace-pre-line text-sm text-ink-blue-6 mt-2 pl-2"
>
{{ info }}
</div>
</div>
</template>
<script setup>
import LucideSearch from "~icons/lucide/search";
import LucideX from "~icons/lucide/x";
import UserAvatar from "@/components/UserAvatar.vue";
import { useUserStore } from "@/stores/user";
import { copy } from "@/utils";
import { watchDebounced } from "@vueuse/core";
import { createResource } from "frappe-ui";
import {
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxPortal,
ComboboxRoot,
ComboboxViewport,
} from "reka-ui";
import { computed, nextTick, onMounted, ref, watch } from "vue";
const props = defineProps({
// "contact" searches HD contacts; "agents" picks from the user store.
scope: { type: String, default: "contact" },
validate: { type: Function, default: null },
emptyPlaceholder: { type: String, default: __("No results found") },
variant: { type: String, default: "subtle" },
placeholder: { type: String, default: "" },
inputClass: { type: String, default: "" },
existingUsers: { type: Array, default: () => [] },
allowCustomEmail: { type: Boolean, default: false },
additionalFilters: { type: Array, default: () => [] },
copyOnClick: { type: Boolean, default: false },
customEmailLabel: { type: String, default: __("Invite via email") },
});
const errorMessage = (value) => __("{0} is an Invalid Email Address", [value]);
// v-model values
const values = defineModel();
// Common state
const pills = ref(null);
const search = ref(null);
const error = ref(null);
const info = ref(null);
const query = ref("");
const showOptions = ref(false);
const tempSelection = ref(null);
const { users } = useUserStore();
// --- Contacts resource (used when scope = "contact") ---
const filterContacts = createResource({
url: "helpdesk.api.contact.search_contacts",
method: "GET",
params: contactSearchParams(""),
transform: (data) =>
data.map(({ full_name, email_id, name }) => ({
label: full_name || email_id || name,
value: email_id || name,
email: email_id,
name,
})),
});
watchDebounced(
query,
(newVal) => {
if (props.scope === "agents") return;
filterContacts.update({ params: contactSearchParams(newVal) });
filterContacts.reload();
},
{ debounce: 250, immediate: true }
);
function contactSearchParams(txt) {
const params = { txt: txt || "" };
if (props.additionalFilters?.length) {
params.additional_filters = JSON.stringify(props.additionalFilters);
}
return params;
}
// --- Options ---
const options = computed(() => {
if (props.scope === "agents") {
let list = (users?.data || []).map((u) => ({
label: u.full_name || u.name,
value: u.name,
}));
if (values.value?.length) {
list = list.filter((o) => !values.value.includes(o.value));
}
if (props.existingUsers?.length) {
list = list.filter((o) => !props.existingUsers.includes(o.value));
}
return list.filter(matchesQuery);
}
// contacts mode; the search is debounced, so narrow the last results to the
// typed text, or a quick Enter picks a contact that no longer matches
let list = (filterContacts.data || []).filter(matchesQuery);
if (values.value?.length) {
list = list.filter((o) => !values.value.includes(o.value));
}
if (props.existingUsers?.length) {
list = list.filter((o) => !isExistingUser(o.email));
}
// offer the typed text itself when nothing matches, like the email To field
if (
props.allowCustomEmail &&
query.value &&
!list.length &&
!isExistingUser(query.value)
) {
list = [{ label: query.value, value: query.value, isCustom: true }];
}
return list;
});
function matchesQuery(option) {
const q = query.value.toLowerCase();
return [option.label, option.value, option.name].some((field) =>
field?.toLowerCase().includes(q)
);
}
function isExistingUser(email) {
if (!email) return false;
return props.existingUsers?.some(
(user) => user?.toLowerCase?.() === email.toLowerCase()
);
}
function addValue(input) {
if (!input) return;
error.value = null;
info.value = null;
const parts = splitRecipients(input);
for (const email of parts) {
if (values.value?.includes(email)) continue;
const inList = options.value.some((o) => o.value === email && !o.isCustom);
if (!inList && !canAddCustomValue(email)) break;
if (!values.value) values.value = [email];
else values.value.push(email);
}
}
function canAddCustomValue(email) {
if (!props.allowCustomEmail) {
error.value =
props.scope === "agents"
? __("{0} is not an existing user", [email])
: __("{0} is not an existing contact", [email]);
query.value = email;
return false;
}
if (isExistingUser(email)) {
error.value = __("{0} is already added", [email]);
query.value = email;
return false;
}
if (props.validate && !props.validate(email)) {
error.value = errorMessage(email);
query.value = email;
return false;
}
return true;
}
// Split on commas/semicolons/newlines while keeping a quoted display name
// intact, so '"Doe, John" <a@b.com>' stays a single token.
function splitRecipients(input) {
const parts = [];
let current = "";
let inQuotes = false;
for (const char of input) {
if (char === '"') {
inQuotes = !inQuotes;
current += char;
} else if ((char === "," || char === ";" || char === "\n") && !inQuotes) {
if (current.trim()) parts.push(current.trim());
current = "";
} else {
current += char;
}
}
if (current.trim()) parts.push(current.trim());
return parts;
}
// Reduce a "Name <email>" value to just the display name for rendering.
function getUsernameLabel(value) {
if (!value) return "";
return value.split("<")[0].replace(/"/g, "").trim();
}
function removeValue(value) {
values.value = values.value.filter((v) => v !== value);
}
// Read the pills from the DOM: a Button with a tooltip renders extra root
// nodes, so its `$el` is not the <button>.
function lastPill() {
const buttons = pills.value?.querySelectorAll(":scope > button");
return buttons?.[buttons.length - 1] ?? null;
}
function removeLastValue() {
if (query.value) return;
const pill = lastPill();
if (document.activeElement === pill) {
values.value.pop();
nextTick(() => {
if (values.value.length) {
lastPill()?.focus();
} else {
setFocus();
}
});
} else {
pill?.focus();
}
}
// TODO: use single contact component. This refocus also fires when focus has
// already moved on, so a click elsewhere within 200ms is pulled back here.
function setFocus() {
setTimeout(() => {
search.value?.$el?.focus();
}, 200);
}
function onInput(e) {
query.value = e.target.value;
error.value = null;
info.value = null;
showOptions.value = true;
}
function onSelect(val) {
if (!val) return;
addValue(val);
if (error.value) {
showOptions.value = false;
return;
}
query.value = "";
tempSelection.value = null;
showOptions.value = true;
nextTick(() => setFocus());
}
// Commit the current query as a pill on Enter, comma or space.
function onKeydown(e) {
const isCommitKey = e.key === "Enter" || e.key === "," || e.key === " ";
if (!isCommitKey || !query.value.trim()) return;
e.preventDefault();
onSelect(query.value);
}
// Split a pasted, comma/space/newline-separated list into multiple pills.
function onPaste(e) {
const text = e.clipboardData?.getData("text") ?? "";
if (!/[\s,;]/.test(text)) return;
e.preventDefault();
addValue(text);
if (!error.value) {
query.value = "";
tempSelection.value = null;
nextTick(() => setFocus());
}
}
watch(options, (list) => {
tempSelection.value = list[0]?.value ?? null;
});
onMounted(() => {
// reka-ui hardcodes autocomplete="off", which Chrome ignores for fields it
// reads as email/address. "new-password" is the value Chrome won't autofill,
// so its native address dropdown stops covering our options. (No password UI:
// the input is type=text and not inside a form.)
search.value?.$el?.setAttribute("autocomplete", "new-password");
});
defineExpose({ setFocus });
</script>

View File

@@ -1,33 +1,94 @@
<template>
<div class="flex h-full items-center justify-center">
<div
v-if="variant === 'badge'"
class="flex flex-col items-center justify-center gap-4 absolute inset-x-0 top-5.5 bottom-0 pointer-events-none"
>
<div
class="flex flex-col items-center gap-3 text-xl font-medium text-ink-gray-4"
class="p-4 size-14.5 rounded-full bg-surface-gray-1 flex justify-center items-center"
>
<component v-if="icon" :is="icon" class="size-6 text-ink-gray-6" />
</div>
<div class="flex flex-col items-center gap-1">
<div class="text-base-medium text-ink-gray-6">
{{ __(title) }}
</div>
<div
v-if="descriptionText"
class="text-p-sm text-ink-gray-5 max-w-60 text-center"
>
{{ __(descriptionText) }}
</div>
</div>
</div>
<div
v-else
class="flex h-full items-center justify-center absolute inset-x-0 top-0 pointer-events-none"
>
<div
class="flex flex-col items-center gap-2 text-xl-medium text-ink-gray-4 w-9/12 md:w-4/12"
>
<!-- overlay variant (for charts) -->
<div
v-if="variant === 'overlay'"
class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -z-10"
:style="{
backgroundImage:
'radial-gradient(ellipse at center, var(--surface-base) 10%, color-mix(in srgb, var(--surface-base) 90%, transparent) 25%, transparent 70%)',
}"
/>
<!-- Icon -->
<component v-if="icon" :is="icon" class="h-10 w-10" />
<!-- title -->
<span>{{ title }}</span>
<!-- Button which emits Empty State Action -->
<Button label="Create" @click="emit('emptyStateAction')" variant="subtle">
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
</Button>
<div class="flex flex-col items-center justify-center gap-0.5">
<span
:class="{
'text-sm font-medium text-ink-gray-8': text === 'sm',
'text-base font-medium text-ink-gray-8': text === 'md' || !text,
'text-md font-medium text-ink-gray-8': text === 'lg',
}"
>
{{ __(title) }}
</span>
<span
v-if="descriptionText"
:class="{
'text-center text-p-xs text-ink-gray-6 mt-1': text === 'sm',
'text-center text-p-sm text-ink-gray-6 mt-1':
text === 'md' || !text,
'text-center text-p-base text-ink-gray-6 mt-1': text === 'lg',
}"
>
{{ __(descriptionText) }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { VNode } from "vue";
import { VNode, computed } from "vue";
interface Props {
title: string;
icon?: VNode | string;
description?: string;
variant?: "default" | "overlay" | "badge";
text?: "sm" | "md" | "lg";
}
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
title: "No Data Found",
icon: "",
variant: "default",
text: "lg",
});
const emit = defineEmits(["emptyStateAction"]);
const descriptionText = computed(() =>
props.description !== undefined && props.description !== ""
? props.description
: `Create new ${props.title
.split(" ")[1]
.toLocaleLowerCase()} using the Create button.`
);
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="w-[106px] shrink-0 truncate text-base text-ink-gray-5">
<Tooltip :text="__(label)">
<span>{{ __(label) }}</span>
</Tooltip>
<span v-if="required" class="text-ink-red-6"> * </span>
</div>
</template>
<script setup lang="ts">
import { Tooltip } from "frappe-ui";
defineProps<{
label: string;
// Frappe fields carry 1 / 0, not true / false
required?: boolean | number;
}>();
</script>

View File

@@ -1,76 +0,0 @@
<template>
<div class="flex-1">
<div
class="mt-1.5 flex justify-between text-base items-start md:items-center"
>
<div
v-if="relatedActivities.length > 1"
class="inline-flex flex-wrap gap-1.5 text-ink-gray-8 font-medium w-4/5"
>
<span>{{ `${show_others ? "Hide " : "Show "}` }}</span>
<span>+{{ relatedActivities.length }} </span>
<span>changes from </span>
<span>{{ user }}</span>
<Button
class="!size-4"
variant="ghost"
@click="show_others = !show_others"
>
<template #icon>
<SelectIcon />
</template>
</Button>
</div>
<div class="text-gray-600 w-4/6" v-else>
<span class="font-medium text-gray-800">
{{ user }}
</span>
<span> {{ content }}</span>
</div>
<Tooltip :text="dateFormat(creation, dateTooltipFormat)">
<div class="text-gray-600 text-sm w-2/6 flex justify-end">
{{ timeAgo(creation) }}
</div>
</Tooltip>
</div>
<div v-if="show_others">
<div
v-for="relatedActivity in relatedActivities"
:key="relatedActivity.creation"
class="mt-2 flex justify-between text-base"
>
<div class="text-gray-600 w-4/6">
<span class="font-medium text-gray-800">
{{ relatedActivity.user }}
</span>
<span> {{ relatedActivity.content }}</span>
</div>
<Tooltip
:text="dateFormat(relatedActivity.creation, dateTooltipFormat)"
>
<div class="text-gray-600 text-sm w-2/6 flex justify-end">
{{ timeAgo(relatedActivity.creation) }}
</div>
</Tooltip>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { SelectIcon } from "@/components/icons";
import { dateFormat, dateTooltipFormat, timeAgo } from "@/utils";
import { ref } from "vue";
const props = defineProps({
activity: {
type: Object,
required: true,
},
});
const { user, content, creation, relatedActivities } = props.activity;
let show_others = ref(false);
</script>

View File

@@ -2,15 +2,18 @@
<div v-if="isEmoji(icon)" v-bind="$attrs">
{{ icon }}
</div>
<FeatherIcon
<SpriteIcon
v-else-if="typeof icon == 'string'"
:name="icon"
:name="icon.replace('lucide-', '')"
v-bind="$attrs"
/>
<component v-else :is="icon" v-bind="$attrs" />
</template>
<script setup>
import { isEmoji } from "@/utils";
// Sprite, not a `lucide-*` class: these names come from stored view icons, and
// Tailwind only emits CSS for class names it can see in source.
import { Icon as SpriteIcon } from "frappe-ui/experimental";
const props = defineProps({
icon: {

View File

@@ -1,118 +0,0 @@
<template>
<Popover transition="default">
<template #target="{ togglePopover, isOpen }">
<slot v-bind="{ isOpen, togglePopover }">
<span class="text-base"> {{ modelValue || "" }} </span>
</slot>
</template>
<template #body="{ togglePopover }">
<div
v-if="reaction"
class="px-2 py-1 flex items-center justify-center gap-2 rounded-full bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div
class="size-5 cursor-pointer rounded-full bg-surface-transparent text-xl"
v-for="r in reactionEmojis"
:key="r"
@click="() => (emoji = r) && togglePopover()"
>
<button>
{{ r }}
</button>
</div>
<Button
class="rounded-full"
icon="plus"
@click.stop="() => (reaction = false)"
/>
</div>
<div
v-else
class="my-3 max-w-max transform bg-surface-white px-4 sm:px-0"
>
<div
class="relative max-h-96 pb-3 overflow-y-auto min-w-40 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div class="flex gap-2 px-3 pb-1 pt-3">
<div class="flex-1">
<FormControl
type="text"
placeholder="Search by keyword"
v-model="search"
:debounce="300"
/>
</div>
<Button @click="setRandom">Random</Button>
</div>
<div class="w-96"></div>
<div class="px-3" v-for="(emojis, group) in emojiGroups" :key="group">
<div
class="sticky top-0 bg-surface-modal pb-2 pt-3 text-sm text-ink-gray-7"
>
{{ group }}
</div>
<div class="grid w-96 grid-cols-12 place-items-center">
<button
class="h-8 w-8 rounded-md p-1 text-2xl hover:bg-surface-gray-2 focus:outline-none focus:ring focus:ring-blue-200"
v-for="_emoji in emojis"
:key="_emoji.description"
@click="() => (emoji = _emoji.emoji) && togglePopover()"
:title="_emoji.description"
>
{{ _emoji.emoji }}
</button>
</div>
</div>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import { ref, computed } from "vue";
import { Popover } from "frappe-ui";
import { gemoji } from "gemoji";
const search = ref("");
const emoji = defineModel();
const reaction = defineModel("reaction");
const reactionEmojis = ref(["👍", "❤️", "😂", "😮", "😢", "🙏"]);
const emojiGroups = computed(() => {
let groups = {};
for (let _emoji of gemoji) {
if (search.value) {
let keywords = [_emoji.description, ..._emoji.names, ..._emoji.tags]
.join(" ")
.toLowerCase();
if (!keywords.includes(search.value.toLowerCase())) {
continue;
}
}
let group = groups[_emoji.category];
if (!group) {
groups[_emoji.category] = [];
group = groups[_emoji.category];
}
group.push(_emoji);
}
if (!Object.keys(groups).length) {
groups["No results"] = [];
}
return groups;
});
function setRandom() {
let total = gemoji.length;
let index = randomInt(0, total - 1);
emoji.value = gemoji[index].emoji;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
defineExpose({ setRandom });
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div class="flex items-center gap-3 mb-2">
<FileUploader :fileTypes="['image/*']" @success="onSuccess">
<template #default="{ openFileSelector, uploading }">
<!-- Filled: image with hover-to-replace + remove badge -->
<div v-if="image" class="group relative size-11.5 shrink-0">
<Avatar
:image="image"
:label="fallbackLabel"
:shape="shape"
size="3xl"
/>
<button
type="button"
class="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/40 text-white opacity-0 outline-none transition focus:outline-none focus-visible:outline-none group-hover:opacity-100"
:class="roundedClass"
:aria-label="__('Replace image')"
@click.prevent="openFileSelector()"
>
<LucidePencil class="size-4" />
</button>
<button
type="button"
class="absolute -right-2.5 -top-2.5 flex size-4.5 cursor-pointer items-center justify-center rounded-full bg-surface-base text-ink-gray-4 opacity-0 outline outline-black-overlay-50 duration-300 ease-in-out focus:outline-none focus-visible:outline-none hover:bg-surface-gray-2 group-hover:opacity-100 [@media(hover:none)]:opacity-100"
:aria-label="__('Remove image')"
@click.prevent="image = ''"
>
<LucideX class="size-2.5" />
</button>
</div>
<!-- Empty: upload box -->
<button
v-else
type="button"
class="flex size-11.5 shrink-0 cursor-pointer items-center justify-center border border-outline-gray-2 text-ink-gray-4 outline-none transition focus:outline-none focus-visible:outline-none hover:border-outline-gray-3"
:class="roundedClass"
:disabled="uploading"
:aria-label="label || __('Upload image')"
@click.prevent="openFileSelector()"
>
<LucideUpload class="size-4.5" />
</button>
</template>
</FileUploader>
<div v-if="label || description" class="flex flex-col gap-0.5">
<span v-if="label" class="text-p-sm-medium text-ink-gray-8">
{{ __(label) }}
</span>
<span v-if="description" class="text-p-sm text-ink-gray-5">
{{ __(description) }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { __ } from "@/translation";
import type { File as FileType } from "@/types";
import { Avatar, FileUploader } from "frappe-ui";
import { computed } from "vue";
import LucidePencil from "~icons/lucide/pencil";
import LucideUpload from "~icons/lucide/upload";
import LucideX from "~icons/lucide/x";
const image = defineModel<string>({ default: "" });
const props = withDefaults(
defineProps<{
label?: string;
description?: string;
fallbackLabel?: string;
shape?: "circle" | "square";
}>(),
{
label: "",
description: __("Upload an image in PNG or JPG format"),
fallbackLabel: "",
shape: "square",
}
);
const roundedClass = computed(() =>
props.shape === "circle" ? "rounded-full" : "rounded-[10px]"
);
function onSuccess(file: FileType) {
image.value = file.file_url;
}
</script>

View File

@@ -1,11 +1,13 @@
<template>
<Teleport to="#app-header" v-if="showHeader">
<slot>
<header class="flex h-10.5 items-center justify-between mx-4 md:mr-0">
<div class="flex items-center gap-2 max-w-[50%]">
<header
class="flex h-10.5 items-center justify-between mx-4 md:ms-5 md:me-0"
>
<div class="flex items-center gap-2 min-w-0 flex-1">
<slot name="left-header" />
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 shrink-0">
<slot name="right-header" class="flex items-center gap-2" />
</div>
</header>
@@ -13,7 +15,7 @@
</Teleport>
</template>
<script setup>
import { ref, nextTick } from "vue";
import { nextTick, ref } from "vue";
const showHeader = ref(false);
nextTick(() => {

View File

@@ -0,0 +1,210 @@
<template>
<div class="w-full h-full overflow-hidden">
<CardBase
:title="title"
:text="chartData.text"
:percentageChange="chartData.percentageChange"
:chartConfig="chartConfig"
:currentDuration="currentDuration"
:orientation="orientation"
:timelineFilter="timelineFilter"
@changeDuration="changeDuration"
/>
</div>
</template>
<script setup lang="ts">
import { __ } from "@/translation";
import { buildPercentageChange, formatTime } from "@/utils";
import { EChartsOption } from "echarts";
import { createResource } from "frappe-ui";
import { computed, onMounted, ref, type PropType } from "vue";
import CardBase from "./ChartCardBase.vue";
interface AverageResponseData {
percentage_change: number;
average?: number;
total?: number;
data: { date: string; avg_time?: number; count?: number }[];
}
const props = defineProps({
title: {
type: String,
required: true,
},
apiUrl: {
type: String,
required: false,
},
timelineFilter: {
type: Boolean,
default: true,
},
data: {
type: Object as PropType<AverageResponseData>,
required: true,
},
chartColor: {
type: Object as PropType<{
lineColor: string;
gradientColor: { start: string; end: string };
}>,
required: true,
},
dt: {
type: String,
required: false,
},
dn: {
type: String,
required: false,
},
orientation: {
type: String as PropType<"vertical" | "horizontal">,
default: "vertical",
},
type: {
type: String as PropType<"Time" | "Count">,
default: "Time",
},
});
// the untranslated key: it is lowercased straight into the API period
const currentDuration = ref("Last month");
const chartData = computed(() => {
const isDataFetched = resource.fetched;
const _data: AverageResponseData = isDataFetched ? resource.data : props.data;
const dates = _data?.data?.map((item) => item.date) || [];
const seriesData =
_data?.data?.map((item) =>
props.type === "Time" ? item.avg_time : item.count
) || [];
const _percentageChange = _data?.percentage_change || 0;
const percentageChange = buildPercentageChange(_percentageChange);
// for time take average and for count take total
const text =
props.type === "Time"
? formatTime(_data?.average || 0, {
day: true,
hour: true,
minute: true,
maxUnits: 2,
}) || "0m"
: _data?.total || 0;
return {
data: seriesData,
dates,
percentageChange,
text,
};
});
const chartConfig = computed<EChartsOption>(() => {
const color = props.chartColor.lineColor;
const gradientColor = props.chartColor.gradientColor;
return {
xAxis: {
type: "category",
data: chartData.value.dates,
show: false,
boundaryGap: false,
},
yAxis: {
type: "value",
show: false,
},
series: [
{
data: chartData.value.data,
type: "line",
symbol: "none",
lineStyle: {
width: 1.25,
},
areaStyle: {
opacity: 0.8,
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: gradientColor.start,
},
{
offset: 1,
color: gradientColor.end,
},
],
global: false,
},
},
},
],
color: color,
grid: {
left: 2,
right: 2,
top: 2,
bottom: 2,
},
tooltip: {
show: true,
trigger: "axis",
appendToBody: true,
// position: (point: number[]) => [point[0] + 12, point[1] - 50],
axisPointer: { type: "none" },
formatter: (params: any) => {
const p = Array.isArray(params) ? params[0] : params;
if (p.value === undefined || p.value === null) return "";
const value =
props.type === "Time"
? formatTime(Number(p.value) || 0, {
day: true,
hour: true,
minute: true,
maxUnits: 2,
}) || "0m"
: p.value;
return `<span style="font-size:12px;color:#6b7280">${p.name}: <b style="color:#374151">${value}</b></span>`;
},
},
};
});
const resource = createResource({
url: props.apiUrl || "",
type: "GET",
makeParams: () => {
const params: Record<string, any> = {
period: currentDuration.value.toLowerCase(),
};
if (props.dt && props.dn) {
params.dt = props.dt;
params.dn = props.dn;
}
return params;
},
});
const changeDuration = (period: string) => {
currentDuration.value = period;
if (props.apiUrl) resource.submit();
};
onMounted(() => {
if (props.timelineFilter && props.apiUrl && !props.data?.data) {
resource.submit();
}
});
</script>

View File

@@ -3,7 +3,7 @@
<div v-for="group in groupedRows" :key="group.group">
<ListGroupHeader :group="group">
<div
class="my-2 flex items-center gap-2 text-base font-medium text-ink-gray-8 justify-between w-full mr-1"
class="my-2 flex items-center gap-2 text-base-medium text-ink-gray-8 justify-between w-full me-1"
>
<div class="flex items-center gap-2 w-full">
<component v-if="group.icon" :is="group.icon" />
@@ -11,7 +11,7 @@
v-if="group.group.label != ''"
class="flex items-end gap-1 w-full"
>
<span>{{ group.group.label }}</span>
<span>{{ __(group.group.label) }}</span>
<span class="text-xs text-ink-gray-5"
>{{
group.rows.length +
@@ -57,14 +57,13 @@
</template>
<script setup>
import { Button, Dropdown } from "frappe-ui";
import {
Button,
Dropdown,
ListGroupHeader,
ListGroupRows,
ListRow,
ListRows,
} from "frappe-ui";
} from "frappe-ui/experimental";
import { computed, ref, watch } from "vue";
import IconMoreHorizontal from "~icons/lucide/more-horizontal";

View File

@@ -1,18 +1,25 @@
<template>
<!-- View Controls -->
<div
class="flex items-center justify-between gap-2 px-5 pb-4 pt-3 pl-6"
:class="[
'flex items-center justify-between gap-2 px-5 pb-4 pt-4',
list?.data?.data?.length > 0 ? 'relative' : 'absolute inset-x-0',
]"
v-if="showViewControls"
>
<QuickFilters v-if="!isMobileView" class="flex-1" />
<div class="flex items-start gap-2 justify-end h-full" v-if="!isMobileView">
<QuickFilters v-if="!isMobileView" />
<div v-if="!isMobileView" class="-ms-2 h-5 border-s"></div>
<div
class="flex items-start gap-2 justify-end h-full py-1 ps-0.5"
v-if="!isMobileView"
>
<Button
label="Save Changes"
:label="__('Save Changes')"
v-if="isViewUpdated && canSaveView"
@click="handleViewUpdate"
/>
<Reload @click="handleReload" :loading="list.loading" />
<Filter :default_filters="defaultParams.filters" />
<Filter />
<SortBy :hide-label="isMobileView" />
<ColumnSettings
:hide-label="isMobileView"
@@ -20,7 +27,7 @@
/>
</div>
<div v-else class="flex justify-between items-center w-full">
<Filter :default_filters="defaultParams.filters" />
<Filter />
<div class="flex items-center gap-2">
<Reload @click="handleReload" :loading="list.loading" />
<SortBy :hide-label="isMobileView" />
@@ -28,9 +35,16 @@
</div>
</div>
<!-- Loading State -->
<div
v-if="list.loading && !list.data?.data?.length"
class="flex items-center justify-center h-full w-full absolute top-0 z-100"
>
<LoadingIndicator class="size-6 text-ink-gray-4" />
</div>
<!-- List View -->
<ListView
v-if="list.data?.data.length > 0"
v-else-if="list.data?.data.length > 0"
class="flex-1"
:columns="columns"
:rows="rows"
@@ -38,7 +52,7 @@
:options="{
selectable: options.selectable,
showTooltip: false,
resizeColumn: false,
resizeColumn: true,
getRowRoute: (row) => ({
name: options.rowRoute?.name,
params: { [options.rowRoute?.prop]: row.name },
@@ -52,7 +66,7 @@
v-for="column in columns"
:key="column.key"
:item="column"
@columnWidthUpdated="(width) => console.log(width)"
@columnWidthUpdated="handleColumnResize"
/>
</ListHeader>
<ListRows
@@ -72,9 +86,19 @@
</ListRows>
<ListSelectBanner v-if="options.showSelectBanner">
<template #actions="{ selections, unselectAll }">
<Dropdown :options="selectBannerOptions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
<div class="flex items-center gap-1">
<Button
v-for="action in selectBannerOptions(selections, unselectAll, true)"
:key="action.label"
:label="action.label"
:icon-left="action.icon"
variant="ghost"
@click="action.onClick"
/>
<Dropdown :options="selectBannerOptions(selections, unselectAll)">
<Button icon="lucide-more-horizontal" variant="ghost" />
</Dropdown>
</div>
</template>
</ListSelectBanner>
</ListView>
@@ -97,21 +121,24 @@
handlePageLength(count);
}
"
/>
</div>
<!-- Loading State -->
<div
v-else-if="list.loading"
class="w-full h-full flex items-center justify-center -mt-48"
>
<LoadingIndicator :scale="10" />
>
<!-- frappe-ui's ListFooter still feeds TabButtons the removed `buttons`
prop, so its own page-length picker renders nothing. -->
<template #left>
<TabButtons
:model-value="defaultParams.page_length_count"
:options="[20, 50, 100].map((o) => ({ label: String(o), value: o }))"
@update:model-value="(count) => handlePageLength(count)"
/>
</template>
</ListFooter>
</div>
<!-- Empty State -->
<EmptyState
v-else
v-else-if="!list.loading"
:title="emptyState.title"
:icon="emptyState.icon"
@emptyStateAction="emit('emptyStateAction')"
:description="emptyState.description"
/>
</template>
@@ -119,11 +146,11 @@
import { MultipleAvatar, StarRating } from "@/components";
import {
ColumnSettings,
Filter,
QuickFilters,
Reload,
SortBy,
} from "@/components/view-controls";
import { Filter, normalizeFilters } from "@/components/view-controls/filter";
import { useScreenSize } from "@/composables/screen";
import {
currentView as headerView,
@@ -132,30 +159,35 @@ import {
} from "@/composables/useView";
import { useAuthStore } from "@/stores/auth";
import { globalStore } from "@/stores/globalStore";
import { capture } from "@/telemetry";
import { View, ViewType } from "@/types";
import { formatTimeShort, getIcon } from "@/utils";
import { useStorage } from "@vueuse/core";
import { useTicketStatusStore } from "@/stores/ticketStatus";
import { capture } from "@/telemetry";
import { __ } from "@/translation";
import { View, ViewType } from "@/types";
import { getIcon } from "@/utils";
import { useStorage } from "@vueuse/core";
import {
call,
createResource,
dayjsLocal,
Dropdown,
FeatherIcon,
frappeRequest,
LoadingIndicator,
TabButtons,
toast,
} from "frappe-ui";
import {
Icon as SpriteIcon,
ListFooter,
ListHeader,
ListHeaderItem,
ListRowItem,
ListSelectBanner,
ListView,
LoadingIndicator,
toast,
} from "frappe-ui";
} from "frappe-ui/experimental";
import {
computed,
h,
onMounted,
onUnmounted,
provide,
reactive,
ref,
@@ -163,7 +195,9 @@ import {
watch,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import EmptyState from "./EmptyState.vue";
import { listFilters } from "./listViewFilters";
import ListRows from "./ListRows.vue";
interface P {
@@ -175,6 +209,7 @@ interface P {
// type of a h componnt
icon?: string | VNode;
title: string;
description?: string;
};
hideViewControls?: boolean;
hideColumnSetting?: boolean;
@@ -190,7 +225,6 @@ interface P {
}
interface E {
(event: "emptyStateAction"): void;
(event: "rowClick", row: any): void;
}
const props = defineProps<P>();
@@ -198,7 +232,7 @@ const emit = defineEmits<E>();
const route = useRoute();
const router = useRouter();
const { isManager } = useAuthStore();
const { $dialog } = globalStore();
const { $dialog, $socket } = globalStore();
const { getStatus } = useTicketStatusStore();
const listSelections = ref(new Set());
@@ -221,16 +255,20 @@ const defaultOptions = reactive({
},
selectBannerActions: [
{
label: "Delete",
icon: "trash-2",
label: __("Delete"),
icon: "lucide-trash-2",
onClick: (selections: Set<string>) => {
$dialog({
title: "Delete",
message: `Are you sure you want to delete ${selections.size} item(s)?`,
title: __("Delete"),
message: __("Are you sure you want to delete {0} item(s)?", [
selections.size,
]),
actions: [
{
label: "Confirm",
label: __("Delete"),
variant: "solid",
theme: "red",
iconLeft: "lucide-trash-2",
onClick({ close }) {
handleBulkDelete(close, selections);
},
@@ -245,11 +283,69 @@ const defaultOptions = reactive({
function handleBulkDelete(hide: Function, selections: Set<string>) {
capture("bulk_delete" + props.options.doctype);
call("frappe.desk.reportview.delete_items", {
items: JSON.stringify(Array.from(selections)),
doctype: props.options.doctype,
}).then(() => {
toast.success("Item(s) deleted successfully");
const requested = Array.from(selections);
const requestedCount = requested.length;
const failureMessages: string[] = [];
let successMessage = "";
let failedCount = 0;
const onBulkResult = (data: { message: string; title: string }) => {
const isFailure =
data.title === __("Bulk Operation Failed") ||
data.title === "Bulk Operation Failed";
const isSuccess =
data.title === __("Bulk Operation Successful") ||
data.title === "Bulk Operation Successful";
if (!isFailure && !isSuccess) return;
if (isFailure) {
// Parse how many items failed from the message (Frappe includes the count)
const match = data.message.match(/Failed to delete (\d+) documents?/);
if (match) {
failedCount = parseInt(match[1], 10);
}
failureMessages.push(data.message);
} else {
successMessage = data.message;
}
};
$socket.on("msgprint", onBulkResult);
// Use frappeRequest (not `call`) so per-item delete errors surfaced in
// `_server_messages` get routed through the app's serverMessagesHandler.
// `call` silently drops them on 200 responses.
frappeRequest({
url: "frappe.desk.reportview.delete_items",
params: {
items: JSON.stringify(requested),
doctype: props.options.doctype,
},
}).finally(() => {
$socket.off("msgprint", onBulkResult);
const deletedCount = requestedCount - failedCount;
if (failureMessages.length > 0 && deletedCount > 0) {
// Partial success: some deleted, some failed — show both toasts
toast.success(__("{0} item(s) deleted successfully", [deletedCount]));
for (const msg of failureMessages) {
toast.error(msg);
}
} else if (failureMessages.length > 0) {
// All failed
for (const msg of failureMessages) {
toast.error(msg);
}
} else if (successMessage) {
// All succeeded
toast.success(successMessage);
} else {
// Fallback: no socket messages received (e.g. enqueued for >10 items)
toast.success(__("{0} item(s) queued for deletion", [requestedCount]));
}
hide();
reset();
});
@@ -271,16 +367,21 @@ const { isMobileView } = useScreenSize();
const defaultEmptyState = {
icon: "",
title: "No Data Found",
title: __("No Data Found"),
};
const pageLengthCount = useStorage(
`list_page_length_count+${props.options.doctype}`,
options.value.default_page_length
);
const defaultParams = reactive({
doctype: options.value.doctype,
filters: {},
default_filters: options.value.defaultFilters,
order_by: "modified desc",
page_length: options.value.default_page_length,
page_length_count: options.value.default_page_length,
page_length: pageLengthCount.value,
page_length_count: pageLengthCount.value,
view: options.value.view,
columns: [],
rows: [],
@@ -316,7 +417,12 @@ const exposeFunctions = {
unselectAll: () => {},
};
function selectBannerOptions(selections: Set<string>, unselectAll = () => {}) {
/** Banner actions. `inline` picks the ones shown as buttons; the rest fill the "..." menu. */
function selectBannerOptions(
selections: Set<string>,
unselectAll = () => {},
inline = false
) {
exposeFunctions["unselectAll"] = unselectAll;
// Get the user-provided actions
@@ -339,8 +445,10 @@ function selectBannerOptions(selections: Set<string>, unselectAll = () => {}) {
onClick: () => action.onClick?.(selections),
}));
// Return combined actions
return [...userActions, ...defaultActions];
return [...userActions, ...defaultActions].filter(
(action) =>
Boolean(action.inline) === inline && (action.condition?.() ?? true)
);
}
const rows = computed(() => {
@@ -370,7 +478,7 @@ function getGroupedByRows(listRows, groupByField) {
group: option || " ",
collapsed: false,
rows: filteredRows,
icon: h(FeatherIcon, {
icon: h(SpriteIcon, {
name: "folder",
class: "h-4 w-4 flex-shrink-0 text-ink-gray-6",
}),
@@ -453,15 +561,15 @@ function listCell(column: any, row: any, item: any, idx: number) {
}
if (column.type === "Datetime") {
return h("span", {
class: "text-p-xs",
textContent: formatTimeShort(item),
class: "text-base",
textContent: dayjsLocal(item).fromNow(),
});
}
if (column.type === "MultipleAvatar") {
return h(MultipleAvatar, {
avatars: item,
hideName: true,
class: "flex items-center truncate flex-1 flex-row-reverse justify-end",
hideName: false,
class: "flex items-center flex-1 min-w-0",
});
}
if (column.type === "Rating") {
@@ -493,19 +601,22 @@ function handleFieldClick(e: MouseEvent, column, row, item) {
}
if (column.type === "MultipleAvatar") {
if (item.length > 1) {
let target = e.target as HTMLElement;
target = target.closest(".user-avatar");
if (target) {
item = target.getAttribute("data-name");
}
} else {
item = item[0].name;
}
applyFilters({ ...defaultParams.filters, [column.key]: ["LIKE", item] });
const avatar = (e.target as HTMLElement).closest(".user-avatar");
// the gap between the faces, and the "+n" chip, name nobody to filter by
const name = avatar?.getAttribute("data-name");
if (!name) return;
applyColumnFilter(column.key, "LIKE", `%${name}%`);
return;
}
applyFilters({ ...defaultParams.filters, [column.key]: item });
applyColumnFilter(column.key, "=", item);
}
function applyColumnFilter(key: string, operator: string, value: any) {
const conditions = normalizeFilters(defaultParams.filters).filter(
(condition) => condition[0] !== key
);
conditions.push([key, operator, value]);
applyFilters(conditions);
}
const showViewControls = computed(() => {
@@ -533,9 +644,17 @@ provide("listViewActions", {
reload,
});
// Also published module-scope, for the command palette: it renders in the
// sidebar, outside this provide chain.
listFilters.value = {
current: () => normalizeFilters(list?.params?.filters),
apply: applyFilters,
};
onUnmounted(() => (listFilters.value = null));
function applyFilters(filters) {
isViewUpdated.value = true;
defaultParams.filters = { ...filters };
defaultParams.filters = normalizeFilters(filters);
list.submit({ ...defaultParams });
// automatically update filters for default view
@@ -548,6 +667,9 @@ function applySort(order_by: string) {
isViewUpdated.value = true;
defaultParams.order_by = order_by;
list.submit({ ...defaultParams, order_by });
if (!defaultParams.is_default) return;
handleViewUpdate();
isViewUpdated.value = false;
}
function updateColumns(obj) {
@@ -564,10 +686,11 @@ function updateColumns(obj) {
function reload(reset: boolean = false) {
if (reset) {
defaultParams.filters = options.value.defaultFilters || {};
defaultParams.filters = normalizeFilters(options.value.defaultFilters);
defaultParams.order_by = "modified desc";
defaultParams.page_length = options.value.default_page_length;
defaultParams.page_length_count = options.value.default_page_length;
pageLengthCount.value = options.value.default_page_length;
defaultParams.page_length_count = pageLengthCount.value;
defaultParams.columns = [];
defaultParams.rows = [];
defaultParams.is_default = true;
@@ -576,7 +699,8 @@ function reload(reset: boolean = false) {
}
function handlePageLength(count: number, loadMore: boolean = false) {
defaultParams.page_length_count = count;
pageLengthCount.value = count;
defaultParams.page_length_count = pageLengthCount.value;
if (loadMore) {
defaultParams.page_length += count;
} else {
@@ -603,15 +727,45 @@ function handleViewUpdate() {
route_name: route.name,
is_customer_portal: options.value.isCustomerPortal,
};
updateView(view, () => {
isViewUpdated.value = false;
});
const currentView = findView(route.query.view as string).value;
if (currentView && currentView.public) {
$dialog({
title: __("Confirm Changes"),
message: __(
"This view is public. Changes made will be visible to everyone."
),
actions: [
{
label: __("Save"),
variant: "solid",
onClick({ close }) {
updateView(view, () => {
isViewUpdated.value = false;
});
close();
},
},
{
label: __("Cancel"),
variant: "outline",
onClick({ close }) {
close();
},
},
],
});
} else {
updateView(view, () => {
isViewUpdated.value = false;
});
}
}
const { findView, updateView, defaultView } = useView(options.value.doctype);
const canSaveView = computed(() => {
let currentView: View = findView(route.query.view as string).value;
if (currentView?.is_standard) return false;
if (!currentView || !currentView.public) return true;
if (currentView.public && isManager) {
return true;
@@ -625,18 +779,66 @@ function handleReload() {
}
function handleViewChanges() {
let currentView: View = findCurrentView();
if (!currentView) {
router.push({ name: route.name });
reload(true);
if (!switchToView(route.query.view as string)) return;
applyUrlFilters();
list.submit({ ...defaultParams });
}
/** Base for URL filters, so repeated pushes layer on the view, not each other. */
let viewFilters = [];
/** Owns sort, columns and rows. False means a redirect is in flight. */
function switchToView(view: string): boolean {
defaultParams.view.name = view;
const currentView: View = findCurrentView();
if (currentView) {
// normalize so legacy dict-format saved views become list conditions
viewFilters = normalizeFilters(currentView.filters);
defaultParams.order_by = currentView.order_by || "modified desc";
defaultParams.columns = currentView.columns;
defaultParams.rows = currentView.rows;
return true;
}
if (view) {
// Stale ?view: drop it but keep the filters, unlike a push to the bare route.
router.replace({
name: route.name,
query: { ...route.query, view: undefined },
});
return false;
}
viewFilters = normalizeFilters(options.value.defaultFilters);
defaultParams.order_by = "modified desc";
defaultParams.columns = [];
defaultParams.rows = [];
defaultParams.is_default = true;
headerView.value.label = __("List");
headerView.value.icon = LucideAlignJustify;
return true;
}
/** Touches `filters` only, so filtering never resets the user's sort. */
function applyUrlFilters() {
const urlFilters = parseUrlFilters();
if (!urlFilters) {
defaultParams.filters = viewFilters;
return;
}
defaultParams.filters = currentView.filters;
defaultParams.order_by = currentView.order_by || "modified desc";
defaultParams.columns = currentView.columns;
defaultParams.rows = currentView.rows;
const overriddenFields = new Set(urlFilters.map((c) => c[0]));
defaultParams.filters = urlFilters.length
? [...viewFilters.filter((c) => !overriddenFields.has(c[0])), ...urlFilters]
: [];
}
list.submit({ ...defaultParams });
/** null when the URL carries no filters; [] means "clear them". */
function parseUrlFilters() {
if (!route.query.filters) return null;
try {
return normalizeFilters(JSON.parse(route.query.filters as string));
} catch (e) {
console.error("Failed to parse filters from URL", e);
return null;
}
}
function findCurrentView() {
@@ -651,15 +853,14 @@ function findCurrentView() {
return currentView;
}
// The view is re-read only when it changes; re-reading it on every filter
// change is what let a filter silently reset the sort.
watch(
() => route.query.view,
(val: string) => {
defaultParams.view.name = val;
handleViewChanges();
if (!val) {
headerView.value.label = "List";
headerView.value.icon = "lucide:align-justify";
}
[() => route.query.view as string, () => route.query.filters as string],
([view], [previousView]) => {
if (view !== previousView && !switchToView(view)) return;
applyUrlFilters();
list.submit({ ...defaultParams });
}
);
@@ -678,6 +879,17 @@ function handleScrollPosition() {
}, 200);
}
function handleColumnResize({ key, width, save } = {}) {
const column = columns.value.find((c) => c.key === key);
if (column) column.width = width;
if (!save) return;
isViewUpdated.value = true;
defaultParams.columns = columns.value;
if (!defaultParams.is_default) return;
handleViewUpdate();
isViewUpdated.value = false;
}
onMounted(async () => {
handleScrollPosition();
@@ -691,7 +903,7 @@ onMounted(async () => {
if (route.query.view) {
const currentView = findCurrentView();
if (!currentView) return;
headerView.value.label = currentView.label || "List";
headerView.value.label = currentView.label || __("List");
headerView.value.icon = getIcon(currentView.icon);
}
return;

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-wrap gap-2 rounded-lg bg-gray-100 p-2">
<div class="flex flex-wrap gap-2 rounded-6 bg-surface-gray-2 p-2">
<Pill
v-for="item in items"
:key="item.value"
@@ -7,7 +7,7 @@
@click="(i) => remove(i)"
/>
<Input
<TextInput
v-model="input"
class="w-full"
:placeholder="placeholder"
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import { Input, toast } from "frappe-ui";
import { TextInput, toast } from "frappe-ui";
import { ref, toRefs } from "vue";
import Pill from "./Pill.vue";

View File

@@ -1,233 +0,0 @@
<template>
<div>
<div class="flex flex-wrap gap-1">
<Button
v-for="value in values"
ref="emails"
:key="value"
:label="value"
theme="gray"
variant="subtle"
class="rounded-full"
@keydown.delete.capture.stop="removeLastValue"
>
<template #suffix>
<FeatherIcon
class="h-3.5"
name="x"
@click.stop="removeValue(value)"
/>
</template>
</Button>
<div class="flex-1">
<Combobox v-model="selectedValue" nullable>
<Popover v-model:show="showOptions" class="w-full">
<template #target="{ togglePopover }">
<ComboboxInput
ref="search"
class="search-input form-input w-full border-none bg-white hover:bg-white focus:border-none focus:!shadow-none focus-visible:!ring-0"
type="text"
:value="query"
autocomplete="off"
@change="
(e) => {
query = e.target.value;
showOptions = true;
}
"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl">
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
>
<ComboboxOption
v-for="option in options"
:key="option.value"
v-slot="{ active }"
:value="option"
>
<li
:class="[
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
{ 'bg-gray-100': active },
]"
>
<UserAvatar
class="mr-2"
:name="option.value"
size="lg"
/>
<div class="flex flex-col gap-1 p-1 text-gray-800">
<div class="text-base font-medium">
{{ option.label }}
</div>
<div class="text-sm text-gray-600">
{{ option.value }}
</div>
</div>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</div>
</template>
</Popover>
</Combobox>
</div>
</div>
<ErrorMessage v-if="error" class="mt-2 pl-2" :message="error" />
</div>
</template>
<script setup lang="ts">
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from "@headlessui/vue";
import { UserAvatar } from "@/components/";
import { Popover, createResource } from "frappe-ui";
import { ref, computed, nextTick } from "vue";
import { watchDebounced } from "@vueuse/core";
const props = defineProps({
validate: {
type: Function,
default: null,
},
errorMessage: {
type: Function,
default: (value) => `${value} is an Invalid value`,
},
});
const values = defineModel();
const emails = ref([]);
const search = ref(null);
const error = ref(null);
const query = ref("");
const text = ref("");
const showOptions = ref(false);
const selectedValue = computed({
get: () => query.value || "",
set: (val) => {
query.value = "";
if (val) {
showOptions.value = false;
}
val?.value && addValue(val.value);
},
});
watchDebounced(
query,
(val) => {
val = val || "";
if (text.value === val) return;
text.value = val;
reload(val);
},
{ debounce: 300, immediate: true }
);
const filterOptions = createResource({
url: "helpdesk.api.contact.search_contacts",
method: "GET",
cache: ["Contact", text.value],
params: {
txt: text.value,
},
transform: (data: Record<"full_name" | "name" | "email_id", string>[]) => {
return data.map(({ full_name, email_id, name }) => ({
label: full_name || name || email_id,
value: email_id,
}));
},
});
const options = computed(() => {
let searchedContacts = filterOptions.data || [];
if (!searchedContacts.length && query.value) {
searchedContacts.push({
label: query.value,
value: query.value,
});
}
return searchedContacts;
});
function reload(val) {
filterOptions.update({
params: {
txt: val,
},
});
filterOptions.reload();
}
const addValue = (value) => {
error.value = null;
if (value) {
const splitValues = value.split(",");
splitValues.forEach((value) => {
value = value.trim();
if (value) {
// check if value is not already in the values array
if (!values.value?.includes(value)) {
// check if value is valid
if (value && props.validate && !props.validate(value)) {
error.value = props.errorMessage(value);
return;
}
// add value to values array
if (!values.value) {
values.value = [value];
} else {
values.value.push(value);
}
value = value.replace(value, "");
}
}
});
!error.value && (value = "");
}
};
const removeValue = (value) => {
values.value = values.value.filter((v) => v !== value);
};
const removeLastValue = () => {
if (query.value) return;
let emailRef = emails.value[emails.value.length - 1]?.$el;
if (document.activeElement === emailRef) {
values.value.pop();
nextTick(() => {
if (values.value.length) {
emailRef = emails.value[emails.value.length - 1].$el;
emailRef?.focus();
} else {
setFocus();
}
});
} else {
emailRef?.focus();
}
};
function setFocus() {
search.value.$el.focus();
}
defineExpose({ setFocus });
</script>

View File

@@ -1,6 +1,13 @@
<template>
<div v-if="_avatars?.length" class="mr-1.5 flex cursor-pointer items-center">
<div v-if="_avatars?.length == 1" class="flex items-center gap-2 text-base">
<div
v-if="_avatars?.length"
class="isolate me-1.5 flex min-w-0 cursor-pointer items-center"
>
<div
v-if="_avatars?.length == 1"
class="user-avatar flex min-w-0 items-center gap-2 text-base line-clamp-1"
:data-name="_avatars[0].name"
>
<Tooltip :text="_avatars[0].name">
<Avatar
shape="circle"
@@ -8,24 +15,34 @@
:label="_avatars[0].label"
size="sm"
/>
<div class="truncate" v-if="!hideName">{{ _avatars[0].label }}</div>
<div class="min-w-0 truncate" v-if="!hideName">
{{ _avatars[0].label }}
</div>
</Tooltip>
</div>
<Tooltip
v-for="avatar in _avatars"
v-else
:key="avatar.name"
:text="avatar.name"
>
<Avatar
class="user-avatar -mr-1.5 ring-2 ring-white transition hover:z-10 hover:scale-110"
shape="circle"
:image="avatar.image"
:label="avatar.label"
:size="size"
:data-name="avatar.name"
/>
</Tooltip>
<template v-else>
<Tooltip
v-for="avatar in visibleAvatars"
:key="avatar.name"
:text="avatar.name"
>
<Avatar
class="user-avatar -mr-1.5 ring-2 ring-[var(--surface-base)] transition hover:z-20 hover:scale-110"
shape="circle"
:image="avatar.image"
:label="avatar.label"
:size="size"
:data-name="avatar.name"
/>
</Tooltip>
<Tooltip v-if="overflowCount" :text="overflowNames">
<div
class="user-avatar relative z-10 -mr-1.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-surface-gray-3 text-xs text-ink-gray-7 ring-2 ring-[var(--surface-base)]"
>
+{{ overflowCount }}
</div>
</Tooltip>
</template>
</div>
</template>
<script setup lang="ts">
@@ -34,17 +51,25 @@ import { Avatar, Tooltip } from "frappe-ui";
import { computed } from "vue";
const props = defineProps({
// Frappe hands `_assign` over as a JSON string; callers with the list
// already parsed pass the array itself.
avatars: {
type: String,
type: [String, Array],
},
size: {
type: String,
default: "md",
default: "sm",
},
hideName: {
type: Boolean,
default: false,
},
// Cap the number of avatars shown; the rest collapse into a "+n" chip.
// 0 (default) shows all, so existing callers are unaffected.
max: {
type: Number,
default: 0,
},
});
const { getUser } = useUserStore();
const _avatars = computed(() => {
@@ -68,4 +93,25 @@ const _avatars = computed(() => {
});
return result;
});
const capped = computed(
() => props.max > 0 && (_avatars.value?.length || 0) > props.max
);
const visibleAvatars = computed(() =>
capped.value ? _avatars.value.slice(0, props.max) : _avatars.value
);
const overflowCount = computed(() =>
capped.value ? _avatars.value.length - props.max : 0
);
const overflowNames = computed(() =>
capped.value
? _avatars.value
.slice(props.max)
.map((a: { name: string }) => a.name)
.join(", ")
: ""
);
</script>

View File

@@ -1,60 +0,0 @@
<template>
<Popover v-slot="{ open }">
<PopoverButton
ref="reference"
v-slot="{ open: o }"
as="div"
@click="updatePosition"
@focusin="updatePosition"
@keydown="updatePosition"
>
<slot name="target" :open="o" />
</PopoverButton>
<div v-show="open">
<PopoverPanel
v-slot="{ open: o, close }"
ref="popover"
static
class="z-[100]"
>
<slot name="body" :open="o" :close="close" />
</PopoverPanel>
</div>
</Popover>
</template>
<script setup>
import { nextTick, ref, onBeforeUnmount } from "vue";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { createPopper } from "@popperjs/core";
const props = defineProps({
placement: {
type: String,
default: "bottom-start",
},
});
const reference = ref(null);
const popover = ref(null);
let popper = ref(null);
function setupPopper() {
if (!popper.value) {
popper.value = createPopper(reference.value.el, popover.value.el, {
placement: props.placement,
});
} else {
popper.value.update();
}
}
function updatePosition() {
nextTick(() => setupPopper());
}
onBeforeUnmount(() => {
popper.value?.destroy();
});
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div class="flex items-center justify-between p-5 pb-0">
<div class="gap-x-3 flex h-full">
<Avatar
:size="avatar.size ?? '3xl'"
:shape="avatar.shape ?? 'square'"
:label="avatar.label"
:image="avatar.image"
class="h-[52px] w-[52px]"
/>
<div
class="flex flex-col h-full gap-1.5"
:class="[
(isMobileView || !hasDocInfo) && 'flex-1 items-center justify-center',
]"
>
<div class="flex gap-2 items-center">
<p class="text-ink-gray-8 text-xl-medium">
{{ avatar.label }}
</p>
<Tooltip v-if="badge" :text="badge.tooltip ?? ''">
<Badge :label="badge.label" :theme="badge.theme ?? 'amber'" />
</Tooltip>
</div>
<div v-if="!isMobileView" class="flex items-center gap-x-1.5">
<template v-for="(item, index) in docInfo" :key="index">
<template v-if="item.condition !== false">
<span
v-if="
docInfo.slice(0, index).some((i) => i.condition !== false)
"
class="text-ink-gray-4"
>
•
</span>
<div class="flex items-center gap-x-1">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 text-ink-gray-6"
/>
<span v-if="item.value" class="text-sm text-ink-gray-8">{{
item.value
}}</span>
<template v-else-if="item.component">
<component :is="item.component" />
</template>
</div>
</template>
</template>
</div>
</div>
</div>
<slot name="actions" />
</div>
</template>
<script setup lang="ts">
import { useScreenSize } from "@/composables/screen";
import { Avatar, Badge, Tooltip, type AvatarProps } from "frappe-ui";
import { computed } from "vue";
interface DocInfoItem {
icon?: any;
value?: string;
component?: any;
condition?: boolean;
}
interface BadgeInfo {
label: string;
theme?: "gray" | "blue" | "green" | "amber" | "red";
tooltip?: string;
}
const props = withDefaults(
defineProps<{
avatar: AvatarProps;
docInfo?: DocInfoItem[];
badge?: BadgeInfo | null;
}>(),
{
docInfo: () => [],
badge: null,
}
);
const { isMobileView } = useScreenSize();
const hasDocInfo = computed(() => {
return props.docInfo?.some((item) => item.value) ?? false;
});
</script>

View File

@@ -2,7 +2,7 @@
<header class="border-b px-5 py-2.5">
<div class="flex items-center justify-between">
<slot name="title">
<div v-if="title" class="text-lg font-medium text-gray-900">
<div v-if="title" class="text-md-medium text-ink-gray-9">
{{ title }}
</div>
</slot>

View File

@@ -10,10 +10,10 @@
<slot name="prefix" />
</template>
<template #suffix>
<Tooltip>
<template #body>
<Tooltip bare>
<template #content>
<div
class="rounded bg-surface-gray-7 py-1.5 px-2 text-xs text-ink-white shadow-xl"
class="rounded-4 bg-surface-gray-10 py-1.5 px-2 text-xs text-ink-base shadow-xl"
>
<span class="flex items-center gap-1">
{{ show ? "Hide Password" : "Show Password" }}
@@ -24,7 +24,7 @@
<component
v-show="showEye"
:is="show ? LucideEyeOff : LucideEye"
class="h-3 cursor-pointer mr-1"
class="h-3 cursor-pointer me-1"
@click="show = !show"
/>
</div>

View File

@@ -1,5 +1,6 @@
<template>
<Button
ref="buttonRef"
:key="label"
theme="gray"
variant="outline"
@@ -13,7 +14,9 @@
</template>
<script setup>
import { Button } from "frappe-ui";
import { computed, ref } from "vue";
import IconX from "~icons/lucide/x";
defineProps({
label: {
type: String,
@@ -28,7 +31,13 @@ defineProps({
const emit = defineEmits(["click"]);
const buttonRef = ref(null);
const handleClick = (label) => {
emit("click", label);
};
defineExpose({
rootRef: computed(() => buttonRef.value?.$el),
});
</script>

View File

@@ -0,0 +1,35 @@
export interface Option {
label: string;
value: string;
}
// A dropdown rendered beneath a text question (e.g. org name + company size).
export interface DropdownConfig {
key: string;
label: string;
placeholder: string;
options: Option[];
}
export interface TextQuestion {
key: string;
title: string;
type: "text";
label?: string;
placeholder?: string;
multiline?: boolean;
required?: boolean;
dropdown?: DropdownConfig;
}
export interface ChoiceQuestion {
key: string;
title: string;
type: "choice";
options: Option[];
multiple?: boolean;
// Placeholder for the free-text box the "Other" option reveals.
otherPlaceholder?: string;
}
export type Question = TextQuestion | ChoiceQuestion;

View File

@@ -0,0 +1,279 @@
<template>
<div class="relative mx-auto w-full max-w-md flex flex-col gap-5">
<HDLogo class="size-8" />
<!-- Fixed height keeps the panel from resizing as questions vary in length. -->
<div class="relative">
<Button
v-if="current > 0"
variant="ghost"
icon="lucide-chevron-left"
class="!size-7 shrink-0 !rounded-5 absolute -left-11 mt-4"
@click="back"
/>
<div
:key="question.key"
class="mx-0 min-w-0 border-0 p-0 flex gap-5 flex-col"
>
<div>
<p class="p-0 text-p-xl font-bold text-ink-gray-9">
{{ question.title
}}<span v-if="titleRequired" class="ms-0.5 text-ink-red-6">*</span>
</p>
<p class="text-base pt-1.5">
{{ __("This helps us personalize your Helpdesk experience.") }}
</p>
</div>
<template v-if="question.type === 'text'">
<div class="flex flex-col gap-3">
<!-- Cap textarea resize: vertical only, between one row block and ~2x default. -->
<div
class="[&_textarea]:resize-y [&_textarea]:min-h-16 [&_textarea]:max-h-40"
>
<span
v-if="question.label"
class="mb-1.5 block text-p-sm text-ink-gray-5"
>
{{ question.label
}}<span v-if="question.required" class="ms-0.5 text-ink-red-6"
>*</span
>
</span>
<Textarea
v-if="question.multiline"
v-model="answers[question.key]"
size="sm"
variant="outline"
:rows="3"
:placeholder="question.placeholder"
/>
<TextInput
v-else
v-model="answers[question.key]"
size="sm"
type="text"
variant="outline"
:placeholder="question.placeholder"
@keyup.enter="proceed"
/>
</div>
<div v-if="dropdown">
<span class="mb-1.5 block text-p-sm text-ink-gray-5">
{{ dropdown.label }}
</span>
<Select
v-model="answers[dropdown.key]"
class="w-full"
size="sm"
variant="outline"
:placeholder="dropdown.placeholder"
:options="dropdown.options"
/>
</div>
</div>
</template>
<template v-else>
<div class="flex flex-wrap gap-3 !text-ink-gray-7 text-p-base">
<Button
v-for="option in choiceOptions"
:key="option.value"
:label="option.label"
:variant="isSelected(option) ? 'subtle' : 'outline'"
size="sm"
class="!rounded-5 border border-solid"
:class="
isSelected(option)
? '!bg-surface-gray-3 hover:!bg-surface-gray-4 !border-outline-gray-3'
: 'hover:!bg-surface-alpha-gray-2'
"
@click="select(option)"
/>
</div>
<div
class="-mt-5 grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
:class="
isOtherSelected
? 'grid-rows-[1fr] opacity-100'
: 'grid-rows-[0fr] opacity-0'
"
>
<div class="overflow-hidden">
<div
class="transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] [&_textarea]:resize-y [&_textarea]:min-h-16 [&_textarea]:max-h-40"
:class="isOtherSelected ? 'translate-y-0' : '-translate-y-full'"
>
<Textarea
ref="otherInput"
v-model="answers[otherKey]"
class="mt-5"
variant="outline"
:rows="3"
:placeholder="otherPlaceholder"
:tabindex="isOtherSelected ? 0 : -1"
:error="otherError"
@update:model-value="otherError = ''"
/>
</div>
</div>
</div>
</template>
</div>
</div>
<div class="flex flex-col items-center gap-2">
<Button
class="w-full"
variant="solid"
size="sm"
:label="isLast ? __('Finish') : __('Next')"
:disabled="!canProceed"
@click="proceed"
/>
</div>
</div>
<Button
class="mt-auto !bg-transparent !text-ink-gray-5 hover:!text-ink-gray-8"
variant="ghost"
:label="__('Skip for now')"
@click="finish"
/>
</template>
<script setup lang="ts">
import HDLogo from "@/assets/logos/HDLogo.vue";
import { __ } from "@/translation";
import { Button, Select, Textarea, TextInput } from "frappe-ui";
import { computed, nextTick, reactive, ref, watch } from "vue";
import type { Option, Question } from "./Questionnaire.types";
type Answers = Record<string, string | string[]>;
const props = defineProps<{ questions: Question[] }>();
const emit = defineEmits<{ submit: [answers: Answers] }>();
const current = ref(0);
const completed = ref(false);
const answers = reactive<Answers>({});
const otherInput = ref<InstanceType<typeof Textarea>>();
const total = computed(() => props.questions.length);
const question = computed(() => props.questions[current.value] as Question);
const isLast = computed(() => current.value === total.value - 1);
// Next stays disabled until the current question has an answer.
const canProceed = computed(() => {
const q = question.value;
const value = answers[q.key];
if (q.type === "text")
return !q.required || (typeof value === "string" && value.trim() !== "");
if (q.multiple) return Array.isArray(value) && value.length > 0;
return typeof value === "string" && value !== "";
});
// Narrow the discriminated union so the template stays declarative: choice
// questions render pills, text questions may carry a dropdown beneath them.
const choiceOptions = computed(() =>
question.value.type === "choice" ? question.value.options : []
);
const dropdown = computed(() =>
question.value.type === "text" ? question.value.dropdown : undefined
);
// "Other" free-text is stored under a `<key>_other` sibling answer key.
const OTHER_SUFFIX = "_other";
const otherKey = computed(() => `${question.value.key}${OTHER_SUFFIX}`);
const isOtherSelected = computed(() => {
const otherOption = choiceOptions.value.find((o) => o.value === "other");
return otherOption ? isSelected(otherOption) : false;
});
const otherPlaceholder = computed(
() =>
(question.value.type === "choice" && question.value.otherPlaceholder) ||
__("Tell us more")
);
// The asterisk normally rides on the field label; questions whose title is
// itself the prompt have no label to hang it on.
const titleRequired = computed(
() =>
question.value.type === "text" &&
!!question.value.required &&
!question.value.label
);
const isOtherFilled = computed(() => {
const other = answers[otherKey.value];
return typeof other === "string" && other.trim() !== "";
});
// Cleared on navigation and on toggling "Other" off, so re-opening the box
// never shows a stale error the user hasn't earned yet.
const otherError = ref("");
watch([current, isOtherSelected], () => (otherError.value = ""));
function isSelected(option: Option) {
const q = question.value;
if (q.type !== "choice") return false;
const value = answers[q.key];
if (q.multiple) {
return Array.isArray(value) && value.includes(option.value);
}
return value === option.value;
}
function select(option: Option) {
if (completed.value) return;
const q = question.value;
if (q.type !== "choice") return;
const key = q.key;
if (q.multiple) {
const value = Array.isArray(answers[key])
? [...(answers[key] as string[])]
: [];
const index = value.indexOf(option.value);
if (index === -1) {
value.push(option.value);
} else {
value.splice(index, 1);
}
answers[key] = value;
} else {
// A click only sets the selection; advancing is always via the Next button.
answers[key] = option.value;
}
if (!isOtherSelected.value) {
delete answers[otherKey.value];
} else if (option.value === "other") {
nextTick(() => otherInput.value?.focus({ preventScroll: true }));
}
}
function advance() {
if (current.value < total.value - 1) current.value += 1;
}
function back() {
if (current.value > 0) current.value -= 1;
}
function proceed() {
if (!canProceed.value) return;
// "Other" stays selectable with Next enabled; the ask happens on click.
if (isOtherSelected.value && !isOtherFilled.value) {
otherError.value = __("Add a few words so we can continue");
otherInput.value?.focus({ preventScroll: true });
return;
}
if (isLast.value) finish();
else advance();
}
function finish() {
if (completed.value) return;
completed.value = true;
setTimeout(() => emit("submit", { ...answers }), 700);
}
</script>

View File

@@ -2,7 +2,7 @@
<div class="relative" :style="{ width: `${sidebarWidth}px` }">
<slot v-bind="{ sidebarResizing, sidebarWidth }" />
<div
class="absolute z-1 h-full w-1 cursor-col-resize bg-surface-gray-4 opacity-0 transition-opacity hover:opacity-100"
class="absolute z-10 h-full w-1 cursor-col-resize bg-surface-gray-4 opacity-0 transition-opacity hover:opacity-100"
:class="{
'opacity-100': sidebarResizing,
'left-0': side == 'right',

View File

@@ -0,0 +1,373 @@
<template>
<Dialog v-model:open="show" size="4xl" bare @after-leave="onAfterLeave">
<template #default>
<div class="max-h-[575px]" :style="{ height: 'calc(100vh - 8rem)' }">
<div class="flex items-center justify-between w-full p-4 pb-2">
<div class="text-2xl-semibold">{{ __("Saved Replies") }}</div>
<Button
icon-left="lucide-plus"
:label="__('New')"
@click="onNewSavedReplyClick"
/>
</div>
<div class="p-4">
<div class="flex items-center gap-2">
<div class="relative w-full">
<TextInput
ref="searchInput"
:model-value="search"
@update:model-value="search = $event"
:placeholder="__('Search')"
type="text"
class="focus:ring-0 border-outline-gray-2"
:debounce="300"
autofocus
>
<template #prefix>
<LucideSearch class="size-4" />
</template>
</TextInput>
<Button
v-if="search"
icon="lucide-x"
variant="ghost"
@click="search = ''"
class="absolute end-1 top-1/2 -translate-y-1/2"
/>
</div>
<Dropdown :options="filters" align="end">
<Button :label="activeFilterLabel">
<template #prefix><FilterIcon class="h-4" /></template>
</Button>
</Dropdown>
</div>
</div>
<div class="px-4 h-full overflow-y-auto">
<div
v-if="savedReplyListResource?.list?.loading"
class="flex items-center justify-center mt-24"
>
<LoadingIndicator class="size-4" />
</div>
<div
v-if="
!savedReplyListResource?.list?.loading &&
savedReplyListResource?.data?.length
"
class="grid grid-cols-1 md:grid-cols-3 gap-3 pb-36"
>
<div
v-for="template in savedReplyListResource?.data"
:key="template.name"
class="flex h-56 cursor-pointer flex-col gap-2 rounded-6 border border-outline-gray-1 bg-surface-base p-3 hover:border-outline-gray-3 relative"
@click="onTemplateSelect(template)"
>
<div class="flex items-center gap-2 border-b pb-2">
<div class="text-base-semibold truncate max-w-[75%]">
{{ template.title }}
</div>
<Tooltip
v-if="actionCounts[template.name]"
:text="actionTooltip(actionCounts[template.name])"
>
<Badge
class="ms-auto shrink-0"
theme="gray"
variant="subtle"
:label="actionLabel(actionCounts[template.name])"
/>
</Tooltip>
</div>
<div
v-if="template.message"
class="flex-1 overflow-hidden pointer-events-none [mask-image:linear-gradient(to_bottom,black_80%,transparent)]"
>
<Editor
:model-value="template.message"
:extensions="extensions"
:editable="false"
>
<template #default>
<EditorContent
class="max-w-none text-p-sm text-ink-gray-5 focus:outline-none"
/>
</template>
</Editor>
</div>
<div
v-if="
selectedTemplate.name === template.name &&
selectedTemplate.isLoading
"
class="flex items-center justify-center absolute top-0 start-0 w-full h-full rounded-6"
>
<LoadingIndicator class="size-4" />
</div>
</div>
<div class="col-span-full flex justify-center">
<Button
v-if="
!savedReplyListResource.list.loading &&
savedReplyListResource.hasNextPage
"
:label="__('Load More')"
icon-left="lucide-refresh-cw"
@click="savedReplyListResource.next()"
/>
</div>
</div>
<div
v-if="
!savedReplyListResource?.list?.loading &&
!savedReplyListResource?.data?.length
"
class="mt-2"
>
<div class="flex h-56 flex-col items-center justify-center">
<div class="text-p-sm text-ink-gray-4">
{{ __("No saved replies found") }}
</div>
</div>
</div>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { recordSavedReplyUse } from "@/components/command-palette/savedReplyCommands";
import { buildEditorExtensions } from "@/components/editor/config";
import FilterIcon from "@/components/icons/FilterIcon.vue";
import { useConfigStore } from "@/stores/config";
import { capture } from "@/telemetry";
import { __ } from "@/translation";
import { RenderedSavedReply, SavedReply } from "@/types";
import { useStorage } from "@vueuse/core";
import {
Badge,
Button,
createListResource,
createResource,
Dialog,
Dropdown,
LoadingIndicator,
TextInput,
Tooltip,
} from "frappe-ui";
import { Editor, EditorContent } from "frappe-ui/editor";
import { storeToRefs } from "pinia";
import { computed, h, nextTick, ref, watch } from "vue";
import {
setActiveSettingsTab,
showSettingsModal,
} from "./Settings/settingsModal";
const extensions = buildEditorExtensions();
const props = defineProps({
doctype: {
type: String,
default: "",
},
ticketId: {
type: String,
default: "",
},
});
const show = defineModel();
const activeFilter = useStorage("saved-replies-filter", "Personal");
const { disableGlobalScopeForSavedReplies, teamRestrictionApplied } =
storeToRefs(useConfigStore());
const filters = computed(() => {
const scopes = [
{ label: __("All"), value: "All" },
{ label: __("Personal"), value: "Personal" },
{ label: __("My Team(s)"), value: "Team" },
{ label: __("Global"), value: "Global" },
];
if (teamRestrictionApplied.value && disableGlobalScopeForSavedReplies.value) {
scopes.pop();
}
return scopes.map((scope) => ({
...scope,
selected: activeFilter.value === scope.value,
onClick: () => (activeFilter.value = scope.value),
slots: {
suffix: () =>
h(
"span",
{ class: "text-sm text-ink-gray-5" },
String(scopeCounts.data?.[scope.value] ?? 0)
),
},
}));
});
const activeFilterLabel = computed(() => {
return (
filters.value.find((filter) => filter.value === activeFilter.value)
?.label ?? activeFilter.value
);
});
// Older sessions stored the label instead of the value
if (activeFilter.value == "My Team") {
activeFilter.value = "Team";
}
// Set default filter to Personal if Global is disabled
if (
teamRestrictionApplied.value &&
disableGlobalScopeForSavedReplies.value &&
activeFilter.value == "Global"
) {
activeFilter.value = "Personal";
}
const emit = defineEmits(["apply"]);
const search = ref("");
const searchInput = ref<InstanceType<typeof TextInput>>();
const selectedTemplate = ref({
name: "",
isLoading: false,
});
const pendingTemplate = ref<RenderedSavedReply | null>(null);
function onAfterLeave() {
if (pendingTemplate.value !== null) {
emit("apply", pendingTemplate.value);
pendingTemplate.value = null;
}
}
const scope = computed(() => {
return filters.value.find((f) => f.value === activeFilter.value)?.value;
});
const savedReplyListResource = createListResource({
doctype: "HD Saved Reply",
fields: ["name", "title", "owner", "scope", "message", "actions"],
filters: {
scope: scope.value == "All" ? undefined : ["=", scope.value],
},
cache: ["SavedReplyListForModal"],
auto: true,
orderBy: "modified desc",
start: 0,
pageLength: 20,
});
// One grouped query answers every scope at once, so the menu can say where the
// replies live before an agent switches to find out
const scopeCounts = createResource({
url: "frappe.client.get_list",
params: {
doctype: "HD Saved Reply",
fields: ["scope", { COUNT: "name" }],
group_by: "scope",
limit_page_length: 0,
},
transform: (rows: { scope: string }[]) => {
const counts: Record<string, number> = { All: 0 };
for (const row of rows) {
const count = row["COUNT(`name`)"] ?? 0;
counts[row.scope] = count;
counts.All += count;
}
return counts;
},
});
const actionCounts = computed<Record<string, number>>(() => {
const counts: Record<string, number> = {};
for (const row of savedReplyListResource.data || []) {
counts[row.name] = JSON.parse(row.actions || "[]").length;
}
return counts;
});
const actionLabel = (count: number) =>
count === 1 ? __("1 action") : __("{0} actions", count);
const actionTooltip = (count: number) =>
count === 1
? __("Applies 1 action when sent")
: __("Applies {0} actions when sent", count);
const onTemplateSelect = (template: SavedReply) => {
if (selectedTemplate.value.isLoading) return;
selectedTemplate.value = {
name: template.name,
isLoading: true,
};
const renderResponse = createResource({
url: "helpdesk.api.saved_replies.get_rendered_saved_reply",
params: {
saved_reply_id: template.name,
ticket_id: props.ticketId,
},
onSuccess: (data: RenderedSavedReply) => {
selectedTemplate.value = {
name: "",
isLoading: false,
};
// If user cancelled (Escape/outside click) while API was in flight, discard
if (!show.value) return;
pendingTemplate.value = data;
show.value = false;
// Shared with the palette: modal picks count toward its most-used rows.
recordSavedReplyUse(template.name, template.title);
capture("saved_reply_applied", { data: { source: "composer" } });
},
});
renderResponse.submit().catch(() => {
selectedTemplate.value = {
name: "",
isLoading: false,
};
});
};
const onNewSavedReplyClick = () => {
show.value = false;
showSettingsModal.value = true;
setActiveSettingsTab("Saved Replies");
};
// A reply is as findable by its wording as by its name, now that only a page loads
watch(search, (query) => {
savedReplyListResource.orFilters = query
? { title: ["like", `%${query}%`], message: ["like", `%${query}%`] }
: undefined;
savedReplyListResource.start = 0;
savedReplyListResource.list.reload();
});
watch(activeFilter, () => {
savedReplyListResource.filters = {
...savedReplyListResource?.filters,
scope: scope.value == "All" ? undefined : ["=", scope.value],
};
savedReplyListResource.start = 0;
savedReplyListResource.list.reload();
});
watch(
show,
(newValue) => {
if (!newValue) return;
if (search.value) {
search.value = "";
} else {
savedReplyListResource.list.reload();
}
// Counts don't move with the selected scope, so this is the only refetch
scopeCounts.reload();
nextTick(() => searchInput.value?.focus());
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,132 @@
<template>
<component
:is="config.doctype ? Link : Combobox"
v-bind="controlProps"
:disabled="!isEditable"
:model-value="action.value"
:placeholder="__('Select {0}', label.toLowerCase())"
@update:model-value="pick(($event ?? '') as string)"
>
<template #trigger="{ setOpen }">
<!-- The anchor forwards its click to this element, so it has to be a plain
one: Tooltip and Combobox both drop inherited attrs -->
<div
class="inline-flex"
:role="isEditable ? 'button' : undefined"
:tabindex="isEditable ? 0 : undefined"
@keydown.enter.self.prevent="setOpen(true)"
>
<Tooltip :text="value ? `${label}: ${value}` : label">
<Badge
class="bg-surface-base transition-colors"
:class="isEditable ? 'cursor-pointer hover:bg-surface-gray-2' : ''"
theme="gray"
variant="outline"
size="lg"
>
<template #prefix>
<component
:is="config.icon"
class="size-3 shrink-0 text-ink-gray-6"
/>
</template>
<span class="flex min-w-0 items-center gap-1">
<span v-if="value" class="shrink-0 text-ink-gray-6">
{{ label }}
</span>
<span class="max-w-40 truncate text-ink-gray-8">
{{ value || label }}
</span>
</span>
<template #suffix>
<!-- Removing is not picking: the click stops here -->
<button
type="button"
:aria-label="__('Remove {0}', label)"
class="flex items-center rounded-1 text-ink-gray-6 transition-colors hover:text-ink-gray-9 focus-visible:ring-2 focus-visible:ring-outline-gray-3 mt-[1px]"
@click.stop="emit('remove')"
@pointerdown.stop
>
<LucideX class="size-3" />
</button>
</template>
</Badge>
</Tooltip>
</div>
</template>
</component>
</template>
<script setup lang="ts">
import { ACTION_TYPES } from "@/components/Settings/SavedReplies/components/actionTypes";
import { useSavedReplyActionOptions } from "@/composables/useSavedReplyActionOptions";
import { Link } from "@framework/ui";
import { __ } from "@/translation";
import { SavedReplyAction } from "@/types";
import { Badge, Combobox, Tooltip } from "frappe-ui";
import { computed } from "vue";
import LucideX from "~icons/lucide/x";
const props = defineProps<{
action: SavedReplyAction;
/** Values the other chips hold, so a repoint can't collide with them. */
taken?: string[];
}>();
const emit = defineEmits<{
update: [action: SavedReplyAction];
remove: [];
}>();
const { valueOptions, linkFilters } = useSavedReplyActionOptions();
const config = computed(() => ACTION_TYPES[props.action.action_type]);
const label = computed(() => config.value.chipLabel);
// Empty for no-value actions like "Assign to Me"; the noun then stands alone
const value = computed(() => props.action.label || props.action.value);
// A tag chip holds one tag, so it picks like the rest. Excluded rather than
// listed, so a new control type is editable by default instead of silently
// read-only: no-value actions have nothing to pick, and a comment is too long
// for a chip.
const isEditable = computed(
() => !["none", "editor"].includes(config.value.control)
);
// Combobox takes string values; the shared options allow numbers too
const pickerOptions = computed(() =>
valueOptions(props.action.action_type)
.filter((option) => !props.taken?.includes(String(option.value)))
.map((option) => ({
label: option.label,
value: String(option.value),
}))
);
// A #trigger slot puts Combobox in button mode, and Link forwards that slot,
// so the badge below serves both controls.
const controlProps = computed(() =>
config.value.doctype
? {
doctype: config.value.doctype,
filters: linkFilters(props.action.action_type),
title: value.value,
}
: { options: pickerOptions.value }
);
/** `label` carries the display name (an agent's, not their id), so both move. */
function pick(picked: string) {
// A link picker offers no option list to read a label from; for these
// doctypes the record name is the label.
if (config.value.doctype) {
emit("update", { ...props.action, value: picked, label: picked });
return;
}
const option = pickerOptions.value.find(
(candidate) => candidate.value === picked
);
if (!option) return;
emit("update", { ...props.action, value: option.value, label: option.label });
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<!-- Full-bleed white section, so the block reads as one box with a gray top
and a white bottom rather than a card floating inside another card -->
<div class="border-t border-outline-gray-1 bg-surface-base">
<div class="flex h-8 items-center gap-1.5 ps-3.5 pe-1.5 text-sm">
<LucideMessageSquare class="size-3 shrink-0 text-ink-gray-6" />
<span class="shrink-0 text-ink-gray-6">{{ __("Comment") }}</span>
<Button
class="ms-auto shrink-0"
variant="ghost"
size="xs"
icon="lucide-x"
:label="__('Remove comment')"
@click="emit('remove')"
/>
</div>
<Editor
:model-value="action.value"
:extensions="extensions"
:placeholder="__('Comment')"
@update:model-value="emit('update:value', $event ?? '')"
>
<template #default>
<EditorContent
class="prose prose-v3 [--prose-font-size:13px] max-w-none max-h-[140px] min-h-16 overflow-y-auto px-3.5 py-2"
/>
</template>
</Editor>
<!-- Fixed height so the reset button appearing doesn't grow the footer -->
<div
class="flex h-9 items-center gap-2 ps-3.5 pe-1.5 pb-2 text-xs text-ink-gray-5"
>
<span class="cursor-default">
{{ __("Visible to your team only, never sent to the customer.") }}
</span>
<Button
v-if="isEdited"
class="ms-auto"
variant="ghost"
size="sm"
:label="__('Reset to default')"
@click="emit('update:value', action.original_value ?? '')"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { buildEditorExtensions } from "@/components/editor/config";
import { useAgentStore } from "@/stores/agent";
import { __ } from "@/translation";
import { SavedReplyAction } from "@/types";
import { Button } from "frappe-ui";
import { Editor, EditorContent } from "frappe-ui/editor";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import LucideMessageSquare from "~icons/lucide/message-square";
const props = defineProps<{
action: SavedReplyAction;
}>();
const emit = defineEmits<{
"update:value": [value: string];
remove: [];
}>();
/** Only actions staged with the original text can report an edit. */
const isEdited = computed(
() =>
props.action.original_value !== undefined &&
props.action.original_value !== props.action.value
);
// Same editor the Comment tab uses: rich text with @-mentions
const { dropdown: agentDropdown } = storeToRefs(useAgentStore());
const extensions = buildEditorExtensions({
mentions: () => agentDropdown.value ?? [],
});
</script>

View File

@@ -0,0 +1,264 @@
<template>
<div
v-if="pendingActions.length"
ref="container"
class="overflow-hidden rounded-6 border border-outline-gray-1 bg-surface-gray-1"
>
<div class="flex h-8 items-center gap-1.5 ps-3.5 pe-1.5">
<LucideInfo class="size-3 shrink-0 text-ink-gray-6" />
<template v-if="sourceLabel">
<Tooltip :text="sourceLabel">
<span class="min-w-0 truncate text-sm-medium text-ink-gray-7">
{{ sourceLabel }}
</span>
</Tooltip>
<span class="shrink-0 text-ink-gray-3">·</span>
</template>
<span class="shrink-0 whitespace-nowrap text-sm text-ink-gray-5">
{{ countSentence }}
</span>
<div class="ms-auto flex shrink-0 items-center gap-0.5">
<Button
variant="ghost"
size="xs"
:aria-expanded="expanded"
:icon="expanded ? 'lucide-chevron-down' : 'lucide-chevron-right'"
:label="expanded ? __('Hide actions') : __('Show actions')"
@click="toggleExpanded"
/>
<Button
variant="ghost"
size="xs"
icon="lucide-x"
:label="__('Clear all actions')"
@click="clear"
:tooltip="__('Clear all actions')"
/>
</div>
</div>
<div v-if="expanded">
<div
v-if="chipActions.length"
class="flex flex-wrap items-center gap-1.5 px-1.5 pb-2"
>
<SavedReplyActionChip
v-for="action in visibleChips"
:key="actionKey(action)"
:action="action"
:taken="takenTags(action)"
@update="updateAction(action, $event)"
@remove="removeAction(action)"
/>
<button v-if="overflowCount" type="button" @click="showAllChips = true">
<Badge theme="gray" variant="outline" size="lg">
{{ __("+{0} more", overflowCount) }}
</Badge>
</button>
</div>
<SavedReplyActionComment
v-if="noteAction"
:action="noteAction"
@update:value="setNoteValue"
@remove="removeAction(noteAction)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { isTagAction } from "@/components/Settings/SavedReplies/components/actionTypes";
import { reloadTicket, reloadTicketFeed } from "@/composables/useTicket";
import { userStorage } from "@/composables/userStorage";
import { __ } from "@/translation";
import { RenderedSavedReply, SavedReplyAction } from "@/types";
import { useElementSize } from "@vueuse/core";
import { Badge, Button, Tooltip, createResource, toast } from "frappe-ui";
import { computed, ref } from "vue";
import LucideInfo from "~icons/lucide/info";
import SavedReplyActionChip from "./SavedReplyActionChip.vue";
import SavedReplyActionComment from "./SavedReplyActionComment.vue";
/** Chips beyond this stay hidden until the agent asks for the rest. */
const MAX_VISIBLE_CHIPS = 4;
/** Failures hold long enough to read: one asks for a decision, the other for work. */
const FAILURE_TOAST_DURATION = 15000;
/** Below this the long sentence squeezes the reply name to nothing. */
const NARROW_WIDTH = 470;
const props = defineProps<{
ticketId: string;
doctype: string;
}>();
// Persisted like the draft body, so a refresh keeps text and actions in sync.
// Scoped to the agent: a staged comment must not survive a shift handover.
const pendingActions = userStorage<SavedReplyAction[]>(
"pendingSavedReplyActions" + props.ticketId,
[]
);
const expanded = userStorage(
"pendingSavedReplyActionsExpanded" + props.ticketId,
false
);
const showAllChips = ref(false);
// Not reactive: it only gates the next failure's retry
let isRetry = false;
const container = ref<HTMLElement>();
const { width } = useElementSize(container);
const noteAction = computed(() =>
pendingActions.value.find((action) => action.action_type === "Add Comment")
);
const chipActions = computed(() =>
pendingActions.value.filter((action) => action.action_type !== "Add Comment")
);
const visibleChips = computed(() =>
showAllChips.value || chipActions.value.length <= MAX_VISIBLE_CHIPS + 1
? chipActions.value
: chipActions.value.slice(0, MAX_VISIBLE_CHIPS)
);
const overflowCount = computed(
() => chipActions.value.length - visibleChips.value.length
);
/** Collapsing puts the chip list back to its capped state. */
function toggleExpanded() {
expanded.value = !expanded.value;
if (!expanded.value) showAllChips.value = false;
}
// Only one reply is ever staged, so its title is the whole label
const sourceLabel = computed(() => pendingActions.value[0]?.source ?? "");
const countSentence = computed(() => {
const count = pendingActions.value.length;
if (width.value && width.value < NARROW_WIDTH) {
return __("{0} actions after sending", count);
}
return count === 1
? __("1 action will be applied after sending the email")
: __("{0} actions will be applied after sending the email", count);
});
/** Stage the actions of an applied saved reply, replacing whatever was staged. */
function add(reply: RenderedSavedReply) {
pendingActions.value = (reply.actions ?? []).map((action) => ({
...action,
source: reply.title,
// Kept so the staged comment can report edits and restore
...(action.action_type === "Add Comment"
? { original_value: action.value }
: {}),
}));
}
/** What is staged right now, for the replace confirmation. Null when nothing is. */
function stagedSummary(): { count: number; source: string } | null {
if (!pendingActions.value.length) return null;
return { count: pendingActions.value.length, source: sourceLabel.value };
}
// Tags share an action type, so they key on their value too
function actionKey(action: SavedReplyAction): string {
return isTagAction(action.action_type)
? `${action.action_type}:${action.value}`
: action.action_type;
}
// Dont re-use duplicate tags
function takenTags(action: SavedReplyAction): string[] {
if (!isTagAction(action.action_type)) return [];
return pendingActions.value
.filter((other) => other !== action && isTagAction(other.action_type))
.map((other) => other.value);
}
/** Repoint an action, e.g. an escalation routed to a different team. */
function updateAction(action: SavedReplyAction, updated: SavedReplyAction) {
const index = pendingActions.value.indexOf(action);
if (index !== -1) pendingActions.value[index] = updated;
}
function removeAction(action: SavedReplyAction) {
pendingActions.value = pendingActions.value.filter(
(pending) => actionKey(pending) !== actionKey(action)
);
}
// Whatever is in the editor at send time is what gets applied
function setNoteValue(value: string) {
if (noteAction.value) noteAction.value.value = value;
}
function reloadFeed() {
reloadTicket(props.ticketId);
reloadTicketFeed(props.ticketId);
}
const applyActions = createResource({
url: "helpdesk.api.saved_replies.apply_saved_reply_actions",
onSuccess: (result: {
applied: SavedReplyAction[];
skipped: SavedReplyAction[];
}) => {
if (result.applied?.length) {
toast.success(
__("Applied {0} saved reply action(s)", result.applied.length)
);
}
if (result.skipped?.length) {
toast.warning(
__(
"Skipped {0} saved reply action(s) that are no longer valid",
result.skipped.length
)
);
}
pendingActions.value = [];
reloadFeed();
},
onError: (error: { status?: number }) => {
const failed = [...pendingActions.value];
// Only a response proves it rolled back, and one retry is enough
const canRetry = Boolean(error?.status) && !isRetry;
// Unstaged always: a kept batch would ride the next, unrelated send
pendingActions.value = [];
toast.error(__("Could not apply actions from the saved reply."), {
duration: FAILURE_TOAST_DURATION,
...(canRetry
? { action: { label: __("Retry"), onClick: () => retry(failed) } }
: { description: __("Please apply the actions manually") }),
});
reloadFeed();
},
});
/** Re-send a batch the agent kept from the failure toast. */
function retry(actions: SavedReplyAction[]) {
isRetry = true;
applyActions.submit({ ticket_id: props.ticketId, actions });
}
/** Apply the staged actions to the ticket; call after the email is sent. */
function submit() {
if (!pendingActions.value.length || props.doctype !== "HD Ticket") return;
// Actions stay staged during the request, so guard the second call
if (applyActions.loading) return;
isRetry = false;
applyActions.submit({
ticket_id: props.ticketId,
actions: [...pendingActions.value],
});
}
/** Drop the staged actions without applying them. */
function clear() {
pendingActions.value = [];
}
defineExpose({ add, stagedSummary, submit, clear });
</script>

View File

@@ -1,12 +1,12 @@
<template>
<div
v-if="!isEmpty(articles.data) && query.length > 2"
class="rounded border p-4 text-base"
v-if="Boolean(articles.data?.length) && query.length > 2"
class="rounded-4 border p-4 text-base"
>
<div class="mb-2 font-medium pl-2" v-if="!hideViewAll">
<div class="mb-2 font-medium ps-2" v-if="!hideViewAll">
These articles may already cover what you are looking for
<RouterLink
class="group cursor-pointer space-x-1 hover:text-gray-900"
class="group cursor-pointer space-x-1 hover:text-ink-gray-9"
:to="{
name: 'CustomerKnowledgeBase',
}"
@@ -22,10 +22,10 @@
<div
v-for="a in articles.data"
:key="a.id"
class="rounded-md border-2 p-2 border-hidden hover:bg-surface-gray-2"
class="rounded-5 border-2 p-2 border-hidden hover:bg-surface-gray-2"
>
<RouterLink
class="group cursor-pointer hover:text-gray-900 flex flex-col gap-1"
class="group cursor-pointer hover:text-ink-gray-9 flex flex-col gap-1"
:to="{
name: 'ArticlePublic',
params: {
@@ -39,7 +39,7 @@
<dt class="font-base">{{ a.subject }} - {{ a.headings }}</dt>
<!-- eslint-disable-next-line vue/no-v-html -->
<dd
class="font-base text-p-sm text-gray-600 line-clamp-1"
class="font-base text-p-sm text-ink-gray-5 line-clamp-1"
v-html="a.description"
></dd>
</RouterLink>
@@ -50,24 +50,24 @@
v-else-if="
!articles.loading && articles.data?.length === 0 && query.length > 2
"
class="flex flex-col items-center justify-center h-[240px] gap-2 rounded border"
class="flex flex-col items-center justify-center h-[240px] gap-2 rounded-4 border"
>
<Icon icon="heroicons-outline:search" class="h-8 w-8 text-gray-400" />
<LucideSearch class="size-8 text-ink-gray-3" />
<div class="flex items-center flex-col justify-center">
<p class="font-base">No answers found</p>
<span class="font-base text-p-sm text-gray-600 text-center"
<span class="font-base text-p-sm text-ink-gray-5 text-center"
>Rephrase the question and try again with some keywords</span
>
</div>
</div>
<div
v-else-if="articles.loading"
class="flex flex-col items-center justify-center h-[240px] gap-2 rounded border"
class="flex flex-col items-center justify-center h-[240px] gap-2 rounded-4 border"
>
<Icon icon="heroicons-outline:search" class="h-8 w-8 text-gray-400" />
<LucideSearch class="size-8 text-ink-gray-3" />
<div class="flex items-center flex-col justify-center">
<p class="font-base">Searching...</p>
<span class="font-base text-p-sm text-gray-600 text-center"
<span class="font-base text-p-sm text-ink-gray-5 text-center"
>Please wait while we search for the answers</span
>
</div>
@@ -75,11 +75,9 @@
</template>
<script setup lang="ts">
import { watch } from "vue";
import { createResource } from "frappe-ui";
import { isEmpty } from "lodash";
import { Icon } from "@iconify/vue";
import { capture } from "@/telemetry";
import { createResource } from "frappe-ui";
import { watch } from "vue";
interface P {
query: string;
hideViewAll?: boolean;
@@ -101,7 +99,9 @@ watch(
},
});
articles.reload();
}
},
// Mounted after the third character is typed.
{ immediate: true }
);
function handleSearchArticleClick(article) {

View File

@@ -1,101 +0,0 @@
<template>
<Autocomplete
placeholder="Select an option"
:options="options"
:value="selection"
@update:query="(q) => onUpdateQuery(q)"
@change="
(v) => {
if (!resetInput) {
selection = v;
}
emit('change', v);
}
"
/>
</template>
<script setup lang="ts">
import { Autocomplete } from "@/components";
import { createListResource } from "frappe-ui";
import { computed, ref } from "vue";
const emit = defineEmits(["change"]);
const props = defineProps({
value: {
type: String,
required: false,
default: "",
},
doctype: {
type: String,
required: true,
},
searchField: {
type: String,
required: false,
default: "name",
},
labelField: {
type: String,
required: false,
default: "name",
},
valueField: {
type: String,
required: false,
default: "name",
},
pageLength: {
type: Number,
required: false,
default: 100,
},
resetInput: {
type: Boolean,
required: false,
default: false,
},
customFilters: {
type: Object,
required: false,
default: {},
},
});
const r = createListResource({
doctype: props.doctype,
pageLength: props.pageLength,
auto: true,
fields: [props.labelField, props.searchField, props.valueField],
filters: {
[props.searchField]: ["like", `%${props.value}%`],
...props.customFilters,
},
onSuccess: () => {
selection.value = props.value
? options.value.find((o) => o.value === props.value)
: null;
},
});
const options = computed(
() =>
r.data?.map((result) => ({
label: result[props.labelField],
value: result[props.valueField],
})) || []
);
const selection = ref(null);
function onUpdateQuery(query: string) {
r.update({
filters: {
[props.searchField]: ["like", `%${query}%`],
...props.customFilters,
},
});
r.reload();
}
</script>

View File

@@ -0,0 +1,409 @@
<template>
<div class="relative inline-block text-start">
<div class="flex">
<Button
variant="outline"
:class="{ 'ring-2 ring-outline-gray-2': isOpen }"
@click="toggleDropdown"
>
<div
class="flex items-center min-h-[20px]"
:class="{ 'gap-2': hasSelectedImages && selectedCount > 0 }"
>
<template v-if="selectedCount > 0">
<!-- Stacked Avatars -->
<div class="flex -space-x-2 rtl:space-x-reverse isolate">
<div
v-for="(option, index) in selectedOptions.slice(0, 3)"
:key="`avatar-${option.value}`"
class="relative flex"
:style="{ zIndex: 10 - index }"
>
<Avatar
v-if="option.image"
:image="option.image"
:label="option.label"
class="border-2 border-[var(--surface-base)] flex-shrink-0"
size="sm"
/>
</div>
</div>
<!-- Count Text -->
<span class="text-ink-gray-7">
{{
selectedCount === 1
? selectedOptions[0]?.label
: `${selectedCount} ${selectionText}`
}}
</span>
</template>
<span v-else class="text-ink-gray-6">{{ placeholder }}</span>
</div>
<template #suffix>
<LucideChevronDown
class="ms-2 h-4 w-4 transition-transform duration-200"
:class="{ 'rotate-180': isOpen }"
/>
</template>
</Button>
</div>
<div
v-if="isOpen"
class="absolute z-50 mt-2 w-64 divide-y divide-outline-elevation-2 rounded-6 bg-surface-elevation-2 shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none start-0 origin-top-left"
>
<!-- Header -->
<div class="py-1.5 px-1.5">
<div
class="flex h-7 items-center text-sm-medium text-ink-gray-6 justify-between"
>
<input
ref="inputRef"
v-model="filterText"
:placeholder="label"
class="px-2 flex-1 bg-transparent border-none outline-none text-sm focus:border-none focus:ring-0 text-ink-gray-6 placeholder-ink-gray-4"
@click.stop
@keydown="handleInputKeydown"
/>
<Button
variant="ghost"
size="sm"
v-if="selectedCount > 0"
@click="clearAll"
>
<template #icon>
<LucideX class="size-4" />
</template>
</Button>
</div>
</div>
<!-- Options List -->
<div class="px-1.5 pb-1.5 max-h-64 overflow-y-auto">
<!-- Individual Options (ungrouped) -->
<div class="pt-1.5" v-if="filteredIndividualOptions.length > 0">
<button
v-for="option in filteredIndividualOptions"
:key="`individual-${option.value}`"
class="group flex h-7 w-full items-center rounded-4 px-2 text-base hover:bg-surface-gray-3 text-ink-gray-6"
:class="{ 'bg-surface-gray-2': isHighlighted(option) }"
@click="toggleOption(option)"
>
<Checkbox
:modelValue="props.modelValue.includes(option.value)"
class="me-4 flex-shrink-0"
/>
<component v-if="option.icon" :is="renderIcon(option.icon)" />
<Avatar
v-else-if="hasAnyVisualElements"
:image="option.image"
:label="option.label"
class="me-2 flex-shrink-0"
size="sm"
/>
<span class="text-ink-gray-7 flex-1 text-start truncate">
{{ option.label }}
</span>
<span
v-if="(option.count ?? 0) > 0"
class="text-xs text-ink-gray-5 ms-auto"
>
{{ option.count }}
</span>
</button>
</div>
<!-- Grouped Options -->
<div v-for="group in filteredGroups" :key="`group-${group.group}`">
<!-- Group Header -->
<div
class="flex h-7 pt-1.5 sticky top-0 bg-surface-base items-center px-2 text-sm-medium text-ink-gray-6"
>
{{ group.group }}
</div>
<!-- Group Options -->
<button
v-for="option in group.items"
:key="`grouped-${option.value}`"
class="group flex h-7 w-full items-center rounded-4 px-2 text-base hover:bg-surface-gray-3 text-ink-gray-6"
:class="{ 'bg-surface-gray-2': isHighlighted(option) }"
@click="toggleOption(option)"
>
<Checkbox
:modelValue="props.modelValue.includes(option.value)"
class="me-4 flex-shrink-0"
/>
<component v-if="option.icon" :is="renderIcon(option.icon)" />
<Avatar
v-else-if="option.image"
:image="option.image"
:label="option.label"
class="me-2 flex-shrink-0"
size="sm"
/>
<!-- Spacer for alignment when other options have visual elements -->
<div
v-else-if="hasAnyVisualElements"
class="me-2 w-6 h-6 flex-shrink-0"
></div>
<span class="text-ink-gray-7 flex-1 text-start truncate">
{{ option.label }}
</span>
<span
v-if="(option.count ?? 0) > 0"
class="text-xs text-ink-gray-5 ms-auto"
>
{{ option.count }}
</span>
</button>
</div>
<!-- No options message -->
<div
v-if="filteredOptions.length === 0"
class="px-2 py-4 text-center text-sm text-ink-gray-5"
>
No options found
</div>
</div>
</div>
<!-- Overlay to close dropdown -->
<div v-if="isOpen" class="fixed inset-0 z-40" @click="closeDropdown"></div>
</div>
</template>
<script setup lang="ts">
import { Avatar, Button, Checkbox } from "frappe-ui";
import {
computed,
h,
nextTick,
ref,
useTemplateRef,
watch,
type Component,
} from "vue";
import LucideChevronDown from "~icons/lucide/chevron-down";
import LucideX from "~icons/lucide/x";
// Type Definitions
interface MultiSelectOption {
value: string;
label: string;
count?: number;
image?: string;
icon?: string | Component;
}
interface MultiSelectGroup {
group: string;
items: MultiSelectOption[];
}
// Support both flat options and grouped options in the same prop
type MultiSelectData = MultiSelectOption[] | MultiSelectGroup[];
interface Props {
options: MultiSelectData;
modelValue: string[];
placeholder?: string;
label?: string;
selectionText?: string;
}
const props = withDefaults(defineProps<Props>(), {
placeholder: "Select options...",
label: "Options",
selectionText: "items",
options: () => [],
});
const emit = defineEmits<{
"update:modelValue": [value: string[]];
}>();
// Reactive State
const isOpen = ref(false);
const filterText = ref("");
const highlightedIndex = ref(0);
const inputRef = useTemplateRef<HTMLInputElement>("inputRef");
// Core Computed Properties
const selectedCount = computed(() => props.modelValue.length);
// Detect if the options are grouped or flat
const isGrouped = computed(() => {
return (
props.options.length > 0 &&
"group" in props.options[0] &&
"items" in props.options[0]
);
});
// Extract individual options and groups from the unified options prop
const individualOptions = computed(() => {
if (isGrouped.value) return [];
return props.options as MultiSelectOption[];
});
const groups = computed(() => {
if (!isGrouped.value) return [];
return props.options as MultiSelectGroup[];
});
// Flatten all options for easier processing
const allOptions = computed(() => {
if (isGrouped.value) {
return (props.options as MultiSelectGroup[]).flatMap(
(group) => group.items
);
}
return props.options as MultiSelectOption[];
});
const selectedOptions = computed(() =>
allOptions.value.filter((option) => props.modelValue.includes(option.value))
);
// UI Helper Computed Properties
const hasSelectedImages = computed(() =>
selectedOptions.value.some((option) => option.image)
);
const hasAnyVisualElements = computed(() => {
return allOptions.value.some((option) => option.image || option.icon);
});
// Filtering Logic
const filteredGroups = computed(() => {
if (!isGrouped.value) return [];
const searchText = filterText.value.toLowerCase();
return groups.value
.map((group) => ({
...group,
items: group.items.filter((option) => {
// Show all items if group name matches the search
const groupMatches = group.group.toLowerCase().includes(searchText);
// Show individual items if their label matches the search
const itemMatches = option.label.toLowerCase().includes(searchText);
return groupMatches || itemMatches;
}),
}))
.filter((group) => group.items.length > 0);
});
const filteredIndividualOptions = computed(() =>
individualOptions.value.filter((option) =>
option.label.toLowerCase().includes(filterText.value.toLowerCase())
)
);
// All filtered options in a flat array for keyboard navigation
const filteredOptions = computed(() => {
const individual = filteredIndividualOptions.value;
const grouped = filteredGroups.value.flatMap((group) => group.items);
return [...individual, ...grouped];
});
const highlightedOption = computed(
() => filteredOptions.value[highlightedIndex.value] || null
);
// Main Interaction Methods
function toggleDropdown() {
isOpen.value = !isOpen.value;
if (isOpen.value) {
highlightedIndex.value = 0; // Reset to first item when opening
nextTick(() => {
inputRef.value?.focus();
});
}
}
function closeDropdown() {
isOpen.value = false;
}
function toggleOption(option: MultiSelectOption) {
const newValue = [...props.modelValue];
const index = newValue.indexOf(option.value);
if (index > -1) {
newValue.splice(index, 1);
} else {
newValue.push(option.value);
}
emit("update:modelValue", newValue);
}
function clearAll() {
filterText.value = "";
emit("update:modelValue", []);
}
// Keyboard Navigation
function handleInputKeydown(event: KeyboardEvent) {
if (!isOpen.value) return;
if (event.key === "Escape") {
event.preventDefault();
closeDropdown();
} else if (event.key === "Enter" && highlightedOption.value) {
event.preventDefault();
event.stopPropagation();
toggleOption(highlightedOption.value);
} else if (event.key === "ArrowDown") {
event.preventDefault();
highlightedIndex.value = Math.min(
highlightedIndex.value + 1,
filteredOptions.value.length - 1
);
} else if (event.key === "ArrowUp") {
event.preventDefault();
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0);
}
}
// Utility Functions
function renderIcon(icon: string | any) {
if (!icon) return null;
const iconContent =
typeof icon === "string"
? h("span", { class: "text-base" }, icon)
: h(icon, { class: "w-4 h-4" });
return h(
"span",
{
class:
"flex-shrink-0 w-4 h-4 inline-flex items-center justify-center me-2",
},
[iconContent]
);
}
function isHighlighted(option: MultiSelectOption) {
return highlightedOption.value?.value === option.value;
}
// Watchers
// Reset highlighted index when filter changes
watch(filterText, () => {
highlightedIndex.value = 0;
});
</script>

View File

@@ -1,33 +1,41 @@
<template>
<Popover
:popover-class="popoverClass.length > 0 ? popoverClass : ''"
class="flex w-full"
>
<template #target="{ open, close }">
<div class="flex flex-col gap-1 w-full">
<slot name="label"></slot>
<FormControl
type="text"
class="w-full focus:outline-none outline-none border-inherit shadow-none"
v-bind="$attrs"
v-model="query"
@update:model-value="(e:string)=>{
if (e.length >= 3) {
open();
} else {
close();
}
}"
>
<template #prefix>
<Icon icon="lucide:search" class="h-4 w-4 text-gray-500" />
</template>
</FormControl>
<Popover>
<template #trigger="{ close, setOpen }">
<!--
Typing opens this, not clicking. reka turns the trigger's root into the
toggle, so the field stops its own click before it reaches that root.
-->
<div>
<div class="flex flex-col gap-1 w-full" @click.stop>
<slot name="label"></slot>
<FormControl
type="text"
class="w-full focus:outline-none outline-none border-inherit shadow-none"
v-bind="$attrs"
v-model="query"
@update:model-value="
(e: string) => {
if (e.length >= 3) {
setOpen(true);
} else {
close();
}
}
"
>
<template #prefix>
<LucideSearch class="size-4 text-ink-gray-4" />
</template>
</FormControl>
</div>
</div>
</template>
<template #body-main>
<template #default>
<!-- Searched Articles -->
<div class="max-h-[320px] md:max-h-[420px] overflow-scroll flex flex-col">
<div
class="max-h-[320px] md:max-h-[420px] overflow-scroll flex flex-col"
:class="popoverClass"
>
<SearchArticles
:query="query"
:hideViewAll="true"
@@ -40,9 +48,8 @@
<script setup lang="ts">
import { FormControl, Popover } from "frappe-ui";
import SearchArticles from "./SearchArticles.vue";
import { ModelRef } from "vue";
import { Icon } from "@iconify/vue";
import SearchArticles from "./SearchArticles.vue";
interface P {
popoverClass: Array<string>;

View File

@@ -1,42 +1,45 @@
<template>
<div>
<!-- px-4 lives once here so the header and content share a single horizontal
padding instead of each declaring their own. -->
<div class="px-4">
<slot name="header" v-bind="{ opened, hide, open, close, toggle }">
<div
v-if="!hide"
class="section-header flex items-center justify-between"
class="section-header sticky top-0 z-10 flex cursor-pointer select-none items-center gap-1 bg-surface-base py-3.5"
:class="headerClass"
@click="collapsible && toggle()"
>
<div
class="flex text-ink-gray-9 max-w-fit cursor-pointer items-center gap-2 text-base"
:class="labelClass"
@click="collapsible && toggle()"
>
<FeatherIcon
v-if="collapsible && collapseIconPosition === 'left'"
name="chevron-right"
class="h-4 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
<span>
{{ label || "Untitled" }}
<!-- Figma: text/base/medium, ink-gray-8 -->
<Tooltip v-if="tooltip" :text="__(tooltip)">
<span
class="text-base font-medium text-ink-gray-8"
:class="labelClass"
>
{{ __(label) || "Untitled" }}
</span>
<FeatherIcon
v-if="collapsible && collapseIconPosition === 'right'"
name="chevron-right"
class="h-4 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
</div>
</Tooltip>
<span
v-else
class="text-base font-medium text-ink-gray-8"
:class="labelClass"
>
{{ __(label) || "Untitled" }}
</span>
<LucideChevronRight
v-if="collapsible"
class="size-3.5 text-ink-gray-5 transition-transform"
:class="{ 'rotate-90': opened }"
/>
<slot name="actions"></slot>
</div>
</slot>
<transition
enter-active-class="duration-300 ease-in"
leave-active-class="duration-300 ease-[cubic-bezier(0, 1, 0.5, 1)]"
enter-to-class="max-h-[200px] overflow-hidden"
leave-from-class="max-h-[200px] overflow-hidden"
enter-from-class="max-h-0 overflow-hidden"
leave-to-class="max-h-0 overflow-hidden"
enter-active-class="transition-all duration-300 ease-in"
leave-active-class="transition-all duration-300 ease-[cubic-bezier(0,1,0.5,1)]"
enter-to-class="max-h-[2000px] overflow-hidden"
leave-from-class="max-h-[2000px] overflow-hidden"
enter-from-class="max-h-0 opacity-0"
leave-to-class="max-h-0 opacity-0"
>
<div class="columns" v-bind="$attrs" v-show="opened">
<slot v-bind="{ opened, open, close, toggle }" />
@@ -45,13 +48,20 @@
</div>
</template>
<script setup>
import { ref } from "vue";
import { __ } from "@/translation";
import { Tooltip } from "frappe-ui";
import { ref, watch } from "vue";
import LucideChevronRight from "~icons/lucide/chevron-right";
const props = defineProps({
label: {
type: String,
default: "",
},
tooltip: {
type: String,
default: "",
},
hideLabel: {
type: Boolean,
default: false,
@@ -64,10 +74,6 @@ const props = defineProps({
type: Boolean,
default: true,
},
collapseIconPosition: {
type: String,
default: "left",
},
labelClass: {
type: [String, Object, Array],
default: "",
@@ -78,19 +84,33 @@ const props = defineProps({
},
});
const emit = defineEmits(["update:opened"]);
const hide = ref(props.hideLabel);
const opened = ref(props.opened);
watch(
() => props.opened,
(val) => {
opened.value = val;
}
);
function setOpened(value) {
opened.value = value;
emit("update:opened", value);
}
function toggle() {
opened.value = !opened.value;
setOpened(!opened.value);
}
function open() {
opened.value = true;
setOpened(true);
}
function close() {
opened.value = false;
setOpened(false);
}
</script>
<script>

View File

@@ -0,0 +1,84 @@
<template>
<Popover
bare
:side="splitPlacement(props.placement).side"
:align="splitPlacement(props.placement).align"
>
<template #trigger>
<Button
class="flex items-center justify-between min-w-36"
:class="targetClass"
icon-right="lucide-chevron-down"
>
<div class="w-full truncate">
{{
options?.find((option) => option.value == model)?.label || "Select"
}}
</div>
</Button>
</template>
<template #default="{ close: closePopover }">
<div
class="mt-1 p-1 text-ink-gray-6 w-[--reka-popper-anchor-width] bg-surface-base shadow-2xl rounded-4"
:class="bodyClass"
>
<div class="max-h-52 overflow-y-auto">
<div
v-for="option in options"
:key="option.value"
class="p-2 cursor-pointer hover:bg-surface-gray-3 text-base flex items-center justify-between rounded-4"
@click="
() => {
onChange(option.value);
closePopover();
}
"
>
<div class="w-full truncate">
{{ option.label }}
</div>
<LucideCheck v-if="model == option.value" class="size-4 ms-2" />
</div>
</div>
<hr class="my-1" />
<Button
variant="ghost"
:label="__('Reset')"
icon-left="lucide-refresh-ccw"
class="w-full focus-visible:ring-0"
@click="onReset(closePopover)"
/>
</div>
</template>
</Popover>
</template>
<script setup lang="ts">
import { splitPlacement } from "@/utils";
import LucideCheck from "~icons/lucide/check";
import { Button, Popover } from "frappe-ui";
const model = defineModel();
interface Props {
options: Array<{ value: string; label: string }>;
targetClass?: string;
bodyClass?: string;
placement?: string;
defaultValue?: string;
}
const props = withDefaults(defineProps<Props>(), {
placement: "bottom-start",
defaultValue: undefined,
});
const onReset = (closePopover: () => void) => {
model.value = props.defaultValue !== undefined ? props.defaultValue : null;
closePopover();
};
const onChange = (value: string) => {
model.value = value;
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex items-center justify-between py-2" v-bind:class="$attrs">
<div class="flex items-center space-x-3 w-4/5">
<div class="flex items-center gap-x-3 w-4/5">
<Avatar :image="agent.user_image" :label="agent.agent_name" size="xl" />
<div>
<div class="flex items-center gap-2">

View File

@@ -1,158 +1,191 @@
<template>
<div v-bind:class="$attrs.class" class="px-10 py-8">
<!-- Header -->
<SettingsLayoutHeader
:title="__('Agents')"
:description="__('Add, manage agents and assign roles to them.')"
>
<template #actions>
<Button
@click="() => (showNewAgentsDialog = !showNewAgentsDialog)"
label="New"
variant="solid"
>
<template #prefix>
<LucidePlus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</template>
<template #bottom-section>
<div class="flex items-center gap-2 justify-between">
<FormControl
v-model="search"
:placeholder="'Search'"
<SettingsLayoutBase
:title="__('Agents')"
:description="__('Add, manage agents and assign roles to them.')"
>
<template #header-actions>
<Button
@click="() => setActiveSettingsTab('Invite Agents')"
label="New"
variant="solid"
class="rtl:flex-row-reverse"
>
<template #prefix>
<LucidePlus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</template>
<template #header-bottom>
<div class="flex items-center gap-2 justify-between">
<div class="relative grow">
<TextInput
:model-value="search"
@update:model-value="search = $event"
:placeholder="__('Search')"
type="text"
class="focus:ring-0 border-outline-gray-2"
:debounce="300"
class="w-60"
>
<template #prefix>
<LucideSearch class="h-4 w-4 text-gray-500" />
<LucideSearch class="size-4" />
</template>
</FormControl>
<Dropdown :options="dropdownOptions" placement="right">
<template #default="{ open }">
<Button
:label="activeFilter"
class="flex items-center justify-between w-[90px]"
>
<template #suffix>
<FeatherIcon
:name="open ? 'chevron-up' : 'chevron-down'"
class="h-4"
/>
</template>
</Button>
</template>
<template #item="{ item, active }">
<button
class="group flex text-ink-gray-6 gap-4 h-7 w-full justify-between items-center rounded px-2 text-base"
:class="{ 'bg-surface-gray-3': active }"
@click="item.onClick"
>
<div class="flex items-center justify-between flex-1">
<span class="whitespace-nowrap">
{{ item.label }}
</span>
<FeatherIcon
v-if="activeFilter === item.label"
name="check"
class="size-4 text-ink-gray-7"
/>
</div>
</button>
</template>
</Dropdown>
</TextInput>
<Button
v-if="search"
icon="lucide-x"
variant="ghost"
@click="search = ''"
class="absolute end-1 top-1/2 -translate-y-1/2"
/>
</div>
</template>
</SettingsLayoutHeader>
<!-- loading state -->
<div v-if="agents.loading" class="flex mt-28 justify-between w-full h-full">
<Button
:loading="agents.loading"
variant="ghost"
class="w-full"
size="2xl"
/>
</div>
<!-- Empty State -->
<div
v-if="!agents.loading && !agents.data?.length"
class="flex mt-28 justify-between w-full h-full"
>
<p class="text-sm text-gray-500 w-full flex justify-center">
No agents found
</p>
</div>
<!-- Agent List -->
<div
class="overflow-y-auto w-full hide-scrollbar mt-4"
v-if="!agents.loading && Boolean(agents.data?.length)"
>
<div v-for="(agent, idx) in agents.data" :key="agent.agent_name">
<AgentCard
:agent="agent"
:show-status="true"
:class="idx !== agents.data.length - 1 && 'border-b '"
>
<template #right>
<Dropdown
v-if="isManager"
class="w-1/5 flex justify-end items-center"
:options="getRoles(agent.name)"
:label="getUserRole(agent.name)"
:button="{
label: getUserRole(agent.name),
iconRight: 'chevron-down',
}"
placement="right"
/>
<Dropdown
:options="getOptions(agent)"
placement="right"
:key="agent"
class="ml-2"
>
<Button>
<template #icon>
<IconMoreHorizontal class="h-4 w-4" />
</template>
</Button>
</Dropdown>
<Dropdown :options="dropdownOptions" align="end">
<template #default="{ open }">
<Button :label="activeFilter">
<template #suffix>
<component
:is="open ? LucideChevronUp : LucideChevronDown"
class="h-4"
/>
</template>
</Button>
</template>
</AgentCard>
<template #item-label="{ item }">
<button
class="group flex text-ink-gray-6 gap-4 w-full justify-between items-center rounded-4 text-base"
@click="item.onClick"
>
<div class="flex items-center justify-between flex-1">
<span class="whitespace-nowrap">
{{ item.label }}
</span>
<LucideCheck
v-if="activeFilter === item.label"
class="size-4 text-ink-gray-7"
/>
</div>
</button>
</template>
</Dropdown>
</div>
<!-- Load More Button -->
<div class="flex justify-center">
<Button
v-if="!agents.loading && agents.hasNextPage"
class="mt-3.5 p-2"
@click="() => agents.next()"
:loading="agents.loading"
label="Load More"
icon-left="refresh-cw"
</template>
<template #content>
<div class="grow">
<!-- loading state -->
<div
v-if="agents.loading"
class="flex mt-28 justify-between w-full h-full"
>
<Button
:loading="agents.loading"
variant="ghost"
class="w-full"
size="lg"
/>
</div>
<!-- Empty State -->
<EmptyState
v-if="!agents.loading && !agents.data?.length"
variant="badge"
:icon="AgentIcon"
title="No agent found"
:description="
activeFilter.length
? 'Change your search terms or filters'
: 'Add one to get started.'
"
/>
<!-- Agent List -->
<div
class="w-full"
v-if="!agents.loading && Boolean(agents.data?.length)"
>
<div
class="grid grid-cols-8 items-center gap-3 text-sm text-ink-gray-5"
>
<div class="col-span-6 text-p-sm">{{ __("Agent name") }}</div>
</div>
<hr class="mt-2" />
<div v-for="(agent, index) in agents.data" :key="agent.agent_name">
<div class="flex items-center justify-between h-14 group rounded-4">
<div class="flex items-center gap-x-3 grow">
<Avatar
:image="agent.user_image"
:label="agent.agent_name"
size="xl"
/>
<div>
<div class="flex items-center gap-2">
<p class="text-base">
{{ agent.agent_name }}
</p>
<Badge
:label="__('Inactive')"
:theme="'gray'"
:class="!agent.is_active ? 'opacity-100' : 'opacity-0'"
variant="subtle"
/>
</div>
<div class="text-base text-ink-gray-6 mt-1">
{{ agent.name }}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<Dropdown
v-if="isManager"
class="flex justify-end items-center"
:options="getRoles(agent.name)"
:label="getUserRole(agent.name)"
:button="{
label: getUserRole(agent.name),
iconRight: 'lucide-chevron-down',
iconLeft:
getUserRole(agent.name) === 'Agent'
? 'lucide-user'
: getUserRole(agent.name) === 'Manager'
? 'lucide-briefcase'
: null,
}"
align="end"
/>
<Dropdown :options="getOptions(agent)" :key="agent" align="end">
<Button icon="lucide-more-horizontal" variant="ghost" />
</Dropdown>
</div>
</div>
<hr v-if="index !== agents.data.length - 1" />
</div>
<!-- Load More Button -->
<div class="flex justify-center">
<Button
v-if="!agents.loading && agents.hasNextPage"
class="mt-3.5 p-2"
@click="() => agents.next()"
:loading="agents.loading"
:label="__('Load More')"
icon-left="lucide-refresh-cw"
/>
</div>
</div>
</div>
</div>
</div>
<AddNewAgentsDialog
:title="__('Add Agents')"
@close="showNewAgentsDialog = false"
:modelValue="showNewAgentsDialog"
:show="showNewAgentsDialog"
@update:modelValue="showNewAgentsDialog = $event"
/>
</template>
</SettingsLayoutBase>
</template>
<script setup lang="ts">
import LucideChevronUp from "~icons/lucide/chevron-up";
import LucideChevronDown from "~icons/lucide/chevron-down";
import LucideCheck from "~icons/lucide/check";
import EmptyState from "@/components/EmptyState.vue";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import { useAuthStore } from "@/stores/auth";
import { useUserStore } from "@/stores/user";
import { call, FormControl, toast } from "frappe-ui";
import { h } from "vue";
import LucideCheck from "~icons/lucide/check";
import IconMoreHorizontal from "~icons/lucide/more-horizontal";
import AgentCard from "./AgentCard.vue";
import { activeFilter, showNewAgentsDialog, useAgents } from "./agents";
import { __ } from "@/translation";
import { Avatar, Button, call, Dropdown, toast } from "frappe-ui";
import { h, onUnmounted } from "vue";
import AgentIcon from "../icons/AgentIcon.vue";
import { activeFilter, useAgents } from "./agents";
import { setActiveSettingsTab } from "./settingsModal";
const { getUserRole, updateUserRoleCache } = useUserStore();
const { isManager } = useAuthStore();
@@ -163,58 +196,20 @@ const agents = agentStore.agents;
function getRoles(agent: string) {
const agentRole = getUserRole(agent);
const roles = [
{
label: "Agent",
component: (props) =>
RoleOption({
role: "Agent",
active: props.active,
selected: agentRole === "Agent",
onClick: () => {
updateRole(agent, "Agent");
},
}),
},
];
const role = (label: string, icon: string) => ({
label,
icon,
selected: agentRole === label,
onClick: () => updateRole(agent, label),
});
const roles = [role("Agent", "lucide-user")];
if (isManager) {
roles.unshift({
label: "Manager",
component: (props) =>
RoleOption({
role: "Manager",
active: props.active,
selected: agentRole === "Manager",
onClick: () => {
updateRole(agent, "Manager");
},
}),
});
roles.unshift(role("Manager", "lucide-briefcase"));
}
return roles;
}
function RoleOption({ active, role, onClick, selected }) {
return h(
"button",
{
class: [
active ? "bg-surface-gray-2" : "text-ink-gray-7",
"group flex w-full justify-between items-center rounded-md px-2 py-2 text-base",
],
onClick: !selected ? onClick : null,
},
[
h("span", { class: "whitespace-nowrap" }, role),
selected
? h(LucideCheck, {
class: ["h-4 w-4 shrink-0 text-ink-gray-7"],
"aria-hidden": true,
})
: null,
]
);
}
function updateRole(agent: string, newRole: string) {
const currentRole = getUserRole(agent);
if (currentRole === newRole) {
@@ -226,7 +221,7 @@ function updateRole(agent: string, newRole: string) {
new_role: newRole,
}).then(() => {
updateUserRoleCache(agent, newRole);
toast.success(`Role updated to ${newRole}`);
toast.success(__(`Role updated to ${newRole} successfully.`));
});
}
@@ -235,7 +230,7 @@ function getOptions(agent) {
return [
{
label: "Disable Agent",
icon: "x-circle",
icon: "lucide-x-circle",
onClick: async () => {
await agentStore.updateAgent(agent.name, 0);
agents.reload({ ...filters, search: search.value });
@@ -244,7 +239,7 @@ function getOptions(agent) {
},
{
label: "Enable Agent",
icon: "check-circle",
icon: "lucide-check-circle",
onClick: async () => {
await agentStore.updateAgent(agent.name, 1);
agents.reload({ ...filters, search: search.value });
@@ -277,6 +272,8 @@ const dropdownOptions = [
},
},
];
</script>
<style scoped></style>
onUnmounted(() => {
agents.filters = {};
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
<span class="text-md-semibold text-ink-gray-8">{{
__("Assignee Rules")
}}</span>
<span class="text-p-sm text-ink-gray-6">
@@ -14,7 +14,7 @@
</div>
<div class="mt-8 flex items-center justify-between gap-2">
<div>
<div class="text-base font-medium text-ink-gray-8">
<div class="text-base-medium text-ink-gray-8">
{{ __("Ticket Routing") }}
</div>
<div class="text-p-sm text-ink-gray-6 mt-1">
@@ -24,11 +24,10 @@
</div>
</div>
<div>
<Popover placement="bottom-end">
<template #target="{ togglePopover }">
<Popover bare side="bottom" align="end">
<template #trigger>
<div
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] select-none min-w-40"
@click="togglePopover()"
class="flex items-center justify-between text-base rounded-4 h-7 py-1.5 ps-2 pe-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-elevation-2 hover:bg-surface-gray-3 focus:bg-surface-base focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] select-none min-w-44"
>
<div>
{{
@@ -37,30 +36,29 @@
)?.label
}}
</div>
<FeatherIcon name="chevron-down" class="size-4" />
<LucideChevronDown class="size-4" />
</div>
</template>
<template #body="{ togglePopover }">
<template #default="{ close: closePopover }">
<div
class="p-1 text-ink-gray-7 mt-1 w-48 bg-white shadow-xl rounded"
class="p-1 text-ink-gray-7 mt-1 bg-surface-base shadow-xl rounded-4 w-[--reka-popper-anchor-width]"
>
<div
v-for="option in ticketRoutingOptions"
:key="option.value"
class="p-2 cursor-pointer hover:bg-gray-50 text-sm flex items-center justify-between rounded"
class="p-2 cursor-pointer hover:bg-surface-gray-3 text-sm flex items-center justify-between rounded-4"
@click="
() => {
assignmentRuleData.rule = option.value;
togglePopover();
closePopover();
}
"
>
<span>
{{ option.label }}
</span>
<FeatherIcon
<LucideCheck
v-if="assignmentRuleData.rule == option.value"
name="check"
class="size-4"
/>
</div>
@@ -71,7 +69,7 @@
</div>
<div class="mt-7 flex items-center justify-between gap-2">
<div>
<div class="text-base font-medium text-ink-gray-8">
<div class="text-base-medium text-ink-gray-8">
{{ __("Assignees") }}
</div>
<div class="text-p-sm text-ink-gray-6 mt-1">
@@ -81,44 +79,56 @@
<AssigneeSearch @addAssignee="validateAssignmentRule('users')" />
</div>
<div class="mt-4 flex flex-wrap gap-2">
<div
<Button
v-for="user in users"
:key="user.name"
class="flex items-center gap-2 text-sm bg-surface-gray-2 rounded-md p-1 w-max px-2 select-none"
variant="outline"
size="sm"
:label="user.full_name"
>
<Avatar :image="user.user_image" :label="user.full_name" size="sm" />
<div class="text-ink-gray-7">
{{ user.full_name }}
</div>
<Tooltip
v-if="user.email == assignmentRuleData.lastUser"
:text="__('Last user assigned by this rule')"
:hover-delay="0.35"
:placement="'top'"
>
<div
class="text-xs rounded-full select-none bg-blue-600 text-white p-0.5 px-2"
<template #prefix>
<Avatar :image="user.user_image" :label="user.full_name" size="sm" />
</template>
<template #suffix>
<Tooltip
v-if="user.email == assignmentRuleData.lastUser"
:text="__('Last user assigned by this rule')"
:hover-delay="350"
side="top"
>
{{ __("Last") }}
</div>
</Tooltip>
<Button variant="ghost" icon="x" @click="removeAssignedUser(user)" />
</div>
<Badge theme="blue" variant="solid" :label="__('Last')" />
</Tooltip>
<LucideX
class="size-4 text-ink-gray-4 transition-colors hover:text-ink-gray-7"
@click.stop="removeAssignedUser(user)"
/>
</template>
</Button>
</div>
<ErrorMessage :message="assignmentRulesErrors.users" />
</div>
</template>
<script setup lang="ts">
import { Avatar, Button, ErrorMessage, Popover, Tooltip } from "frappe-ui";
import LucideCheck from "~icons/lucide/check";
import LucideChevronDown from "~icons/lucide/chevron-down";
import {
assignmentRuleData,
assignmentRulesErrors,
validateAssignmentRule,
} from "../../../stores/assignmentRules";
import AssigneeSearch from "./AssigneeSearch.vue";
import { computed } from "vue";
} from "@/stores/assignmentRules";
import { useUserStore } from "@/stores/user";
import {
Avatar,
Badge,
Button,
ErrorMessage,
Popover,
Tooltip,
} from "frappe-ui";
import LucideX from "~icons/lucide/x";
import { computed } from "vue";
import AssigneeSearch from "./AssigneeSearch.vue";
const { getUser } = useUserStore();

View File

@@ -1,20 +1,22 @@
<template>
<Combobox :multiple="true">
<Popover placement="bottom-end">
<template #target="{ togglePopover }">
<Popover bare side="bottom" align="end">
<template #trigger>
<Button
variant="subtle"
icon-left="plus"
@click="togglePopover()"
icon-left="lucide-plus"
:label="__('Add Assignee')"
class="rtl:flex-row-reverse"
/>
</template>
<template #body="{ togglePopover }">
<div class="mt-1 rounded-lg bg-white py-1 text-base shadow-2xl w-60">
<template #default>
<div
class="mt-1 rounded-6 bg-surface-base py-1 text-base shadow-2xl w-60"
>
<div class="relative px-1.5 pt-0.5">
<ComboboxInput
ref="search"
class="form-input w-full"
class="form-input w-full bg-transparent"
type="text"
@change="
(e) => {
@@ -26,17 +28,17 @@
:placeholder="__('Search')"
/>
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
class="absolute end-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="query = ''"
>
<FeatherIcon name="x" class="w-4" />
<LucideX class="w-4" />
</button>
</div>
<ComboboxOptions class="my-2 max-h-64 overflow-y-auto px-1.5" static>
<ComboboxOption
v-show="users.length > 0"
v-for="user in users"
:key="user.username"
:key="user.name"
:value="user"
as="template"
v-slot="{ active }"
@@ -48,28 +50,30 @@
"
>
<li
class="flex items-center rounded p-1.5 w-full text-base"
:class="{ 'bg-gray-100': active }"
class="flex items-center rounded-4 p-1.5 w-full text-base"
:class="{ 'bg-surface-gray-2': active }"
>
<div class="flex gap-2 items-center w-full select-none">
<Avatar
:shape="'circle'"
:image="user.user_image"
:label="user.full_name"
:label="user.agent_name"
size="lg"
/>
<div class="flex flex-col gap-1">
<div class="font-semibold text-ink-gray-7">
{{ user.full_name }}
<div class="flex flex-col gap-1 truncate">
<div class="font-semibold text-ink-gray-7 truncate">
{{ user.agent_name }}
</div>
<div class="text-ink-gray-6 truncate">
{{ user.user }}
</div>
<div class="text-ink-gray-6">{{ user.email }}</div>
</div>
</div>
</li>
</ComboboxOption>
<li
v-if="users.length == 0"
class="mt-1.5 rounded-md p-1.5 text-base text-gray-600"
class="mt-1.5 rounded-5 p-1.5 text-base text-ink-gray-5"
>
{{ __("No results found") }}
</li>
@@ -77,34 +81,26 @@
<div class="border-t p-1.5 pb-0.5">
<Button
variant="ghost"
class="w-full"
icon-left="plus"
class="w-full rtl:flex-row-reverse"
icon-left="lucide-plus"
:label="__('Invite agent')"
@click="
() => {
showNewAgentsDialog = true;
togglePopover();
}
"
@click="inviteAgents"
/>
</div>
</div>
</template>
</Popover>
</Combobox>
<AddNewAgentsDialog
:title="__('Add Agents')"
@close="showNewAgentsDialog = false"
:modelValue="showNewAgentsDialog"
:show="showNewAgentsDialog"
@update:modelValue="showNewAgentsDialog = $event"
@onAgentsInvited="addInvitedAgents"
<ConfirmDialog
v-model="showConfirmDialog.show"
:title="showConfirmDialog.title"
:message="showConfirmDialog.message"
:onConfirm="showConfirmDialog.onConfirm"
/>
</template>
<script setup lang="ts">
import AddNewAgentsDialog from "@/components/desk/global/AddNewAgentsDialog.vue";
import LucideX from "~icons/lucide/x";
import { assignmentRuleData } from "@/stores/assignmentRules";
import {
Combobox,
@@ -112,87 +108,76 @@ import {
ComboboxOption,
ComboboxOptions,
} from "@headlessui/vue";
import { watchDebounced } from "@vueuse/core";
import { Avatar, createListResource, Popover } from "frappe-ui";
import { computed, ref } from "vue";
import { Avatar, Popover } from "frappe-ui";
import { computed, inject, Ref, ref } from "vue";
import { setActiveSettingsTab } from "../settingsModal";
import { useAgentStore } from "@/stores/agent";
import { onMounted } from "vue";
import { __ } from "@/translation";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
const emit = defineEmits(["addAssignee"]);
const query = ref("");
const showNewAgentsDialog = ref(false);
watchDebounced(
() => query.value,
(val) => {
const filters = {
full_name: undefined,
email: undefined,
};
const isEmail = val.includes("@") || val.includes(".");
if (isEmail) {
filters.email = ["like", `%${val}%`];
} else {
filters.full_name = ["like", `%${val}%`];
}
usersList.update({
filters,
});
usersList.reload();
},
{ debounce: 500 }
const { agents } = useAgentStore();
const isAssignmentRuleFormDirty = inject<Ref<boolean>>(
"isAssignmentRuleFormDirty"
);
const usersList = createListResource({
doctype: "User",
fields: ["name", "email", "full_name", "user_image"],
start: 0,
pageLength: 5,
auto: true,
transform: (data) => {
return data
.map((user) => {
if (user.full_name == "Administrator") {
return {
...user,
email: "Administrator",
user: "Administrator",
};
}
return {
...user,
user: user.email,
};
})
.filter((user) => user.full_name != "Guest");
},
const showConfirmDialog = ref({
show: false,
title: "",
message: "",
onConfirm: () => {},
});
const users = computed(() => {
return (
usersList.data?.filter(
let filteredAgents =
agents.data?.filter(
(user) =>
!assignmentRuleData.value.users.some((u) => u.user === user.email)
) || []
);
!assignmentRuleData.value.users.some((u) => u.user === user.user)
) || [];
if (query.value) {
const val = query.value.toLowerCase();
const isEmail = val.includes("@") || val.includes(".");
filteredAgents = filteredAgents.filter((user) => {
if (isEmail) {
return user.user?.toLowerCase().includes(val);
} else {
return user.agent_name?.toLowerCase().includes(val);
}
});
}
return filteredAgents;
});
const addInvitedAgents = (users) => {
users.forEach((user) => {
addAssignee({ user });
const addAssignee = (user) => {
assignmentRuleData.value.users.push({
user: user.user,
});
emit("addAssignee", user);
};
const addAssignee = (user) => {
const userExists = assignmentRuleData.value.users.some(
(u) => u.user === user.user
);
if (!userExists) {
assignmentRuleData.value.users.push({
full_name: user.full_name,
email: user.email,
user_image: user.user_image,
user: user.email,
});
emit("addAssignee", user);
const inviteAgents = () => {
if (isAssignmentRuleFormDirty.value) {
showConfirmDialog.value = {
show: true,
title: __("Unsaved changes"),
message: __(
"Are you sure you want to leave? Unsaved changes will be lost."
),
onConfirm: () => {
setActiveSettingsTab("Invite Agents");
},
};
} else {
setActiveSettingsTab("Invite Agents");
}
};
onMounted(() => {
if (agents.loading || agents.data?.length || agents.list.promise) {
return;
}
agents.fetch();
});
</script>

View File

@@ -1,28 +1,36 @@
<template>
<div
class="grid grid-cols-11 items-center gap-4 cursor-pointer hover:bg-gray-50 rounded"
class="grid grid-cols-12 items-center gap-4 cursor-pointer hover:bg-surface-sidebar rounded-4"
>
<div
@click="assignmentRulesActiveScreen = { screen: 'view', data: data }"
class="w-full py-3 pl-2 col-span-7"
class="w-full ps-2 col-span-7 h-14 flex flex-col justify-center"
>
<div class="text-base text-ink-gray-7 font-medium">{{ data.name }}</div>
<div class="text-base-medium text-ink-gray-7">{{ data.name }}</div>
<div
v-if="data.description && data.description.length > 0"
class="text-sm w-full text-ink-gray-5 mt-1 whitespace-nowrap overflow-ellipsis overflow-hidden"
class="text-sm w-full text-ink-gray-5 mt-1 truncate"
>
{{ data.description }}
</div>
</div>
<div class="col-span-2">
<Select
class="w-max bg-transparent -ml-2 border-0 text-ink-gray-6 focus-visible:!ring-0 bg-none"
:options="priorityOptions"
<div class="col-span-3">
<select
class="w-full h-7 text-base hover:bg-surface-gray-3 rounded-5 p-0 ps-2 pe-5 bg-transparent -ms-2 border-0 text-ink-gray-8 focus-visible:!ring-0 bg-none truncate"
v-model="data.priority"
@update:modelValue="onPriorityChange"
/>
@change="onPriorityChange"
>
<option
v-for="option in priorityOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="flex justify-between items-center w-full pr-2 col-span-2">
<div class="flex justify-between items-center w-full pe-2 col-span-2">
<div>
<Switch
size="sm"
@@ -31,26 +39,26 @@
/>
</div>
<div>
<Dropdown placement="right" :options="dropdownOptions">
<Button
icon="more-horizontal"
variant="ghost"
@click="isConfirmingDelete = false"
/>
<Dropdown
align="end"
:options="dropdownOptions"
@update:open="(open) => open && (isConfirmingDelete = false)"
>
<Button icon="lucide-more-horizontal" variant="ghost" />
</Dropdown>
</div>
</div>
</div>
<Dialog
:options="{ title: __('Duplicate Assignment Rule') }"
v-model="duplicateDialog.show"
:title="__('Duplicate Assignment Rule')"
v-model:open="duplicateDialog.show"
>
<template #body-content>
<template #default>
<div class="flex flex-col gap-4">
<FormControl
:label="__('New Assignment Rule Name')"
type="text"
v-model="duplicateDialog.name"
v-model="duplicateDialog.newName"
/>
</div>
</template>
@@ -69,19 +77,22 @@
<script setup lang="ts">
import { assignmentRulesActiveScreen } from "@/stores/assignmentRules";
import { __ } from "@/translation";
import { AssignmentRuleListResourceSymbol } from "@/types";
import { AssignmentRule } from "@/types/doctypes";
import { ConfirmDelete } from "@/utils";
import {
Button,
createResource,
Dialog,
Dropdown,
FormControl,
Select,
Switch,
toast,
} from "frappe-ui";
import { inject, ref } from "vue";
const assignmentRulesList = inject<any>("assignmentRulesList");
const assignmentRulesListData = inject(AssignmentRuleListResourceSymbol);
const props = defineProps({
data: {
@@ -100,6 +111,7 @@ const priorityOptions = [
const duplicateDialog = ref({
show: false,
newName: "",
name: "",
});
@@ -113,9 +125,9 @@ const deleteAssignmentRule = () => {
name: props.data.name,
},
onSuccess: () => {
assignmentRulesList.reload();
assignmentRulesListData?.reload();
isConfirmingDelete.value = false;
toast.success("Assignment rule deleted");
toast.success(__("Assignment rule deleted successfully."));
},
auto: true,
});
@@ -123,14 +135,15 @@ const deleteAssignmentRule = () => {
const dropdownOptions = [
{
label: "Duplicate",
label: __("Duplicate"),
onClick: () => {
duplicateDialog.value = {
show: true,
name: props.data.name + " (Copy)",
newName: props.data.name + " (Copy)",
name: props.data.name,
};
},
icon: "copy",
icon: "lucide-copy",
},
...ConfirmDelete({
onConfirmDelete: () => deleteAssignmentRule(),
@@ -140,22 +153,37 @@ const dropdownOptions = [
const duplicate = () => {
createResource({
url: "helpdesk.api.assignment_rule.duplicate_assignment_rule",
url: "frappe.client.get",
params: {
docname: props.data.name,
new_name: duplicateDialog.value.name,
doctype: "Assignment Rule",
name: duplicateDialog.value.name,
},
onSuccess: (data) => {
assignmentRulesList.reload();
toast.success("Assignment rule duplicated");
duplicateDialog.value.show = false;
duplicateDialog.value.name = "";
assignmentRulesActiveScreen.value = {
screen: "view",
data: {
name: data.name,
onSuccess: (data: AssignmentRule) => {
createResource({
url: "frappe.client.insert",
params: {
doc: {
...data,
name: duplicateDialog.value.newName,
},
},
};
auto: true,
onSuccess(newAssignmentRuleData: AssignmentRule) {
assignmentRulesListData?.reload();
toast.success(__("Assignment rule duplicated successfully."));
duplicateDialog.value = {
show: false,
newName: "",
name: "",
};
setTimeout(() => {
assignmentRulesActiveScreen.value = {
screen: "view",
data: newAssignmentRuleData,
};
}, 250);
},
});
},
auto: true,
});
@@ -165,15 +193,26 @@ const onPriorityChange = () => {
setAssignmentRuleValue("priority", props.data.priority);
};
const onToggle = () => {
const onToggle = (enabled: boolean) => {
if (!props.data.users_exists && props.data.disabled) {
toast.error("Cannot enable rule without adding users in it");
toast.error(__("Cannot enable rule without adding users in it"));
return;
}
setAssignmentRuleValue("disabled", !props.data.disabled, "status");
// Optimistically flip so the switch reflects the new state immediately;
// revert if the backend update fails.
const previous = props.data.disabled;
props.data.disabled = enabled ? 0 : 1;
setAssignmentRuleValue("disabled", props.data.disabled, "status", () => {
props.data.disabled = previous;
});
};
const setAssignmentRuleValue = (key, value, fieldName = undefined) => {
const setAssignmentRuleValue = (
key: string,
value: any,
fieldName?: string,
onError?: () => void
) => {
createResource({
url: "frappe.client.set_value",
params: {
@@ -183,8 +222,15 @@ const setAssignmentRuleValue = (key, value, fieldName = undefined) => {
value: value,
},
onSuccess: () => {
assignmentRulesList.reload();
toast.success(`Assignment rule ${fieldName || key} updated`);
toast.success(
__("Assignment rule {0} updated successfully.", fieldName || key)
);
},
onError: () => {
onError?.();
toast.error(
__("Failed to update assignment rule {0}.", fieldName || key)
);
},
auto: true,
});

View File

@@ -1,34 +1,12 @@
<template>
<div
v-if="getAssignmentRuleData.loading"
class="flex items-center h-full justify-center"
<SettingsLayoutBase
:back-label="
assignmentRuleData.assignmentRuleName || __('New Assignment Rule')
"
:on-back="goBack"
:dirty="isDirty"
>
<LoadingIndicator class="w-4" />
</div>
<div
v-if="!getAssignmentRuleData.loading"
class="sticky top-0 z-10 bg-white pb-6 px-10 py-8"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<Button
variant="ghost"
icon-left="chevron-left"
:label="
assignmentRuleData.assignmentRuleName || 'New Assignment Rule'
"
size="md"
@click="goBack()"
class="cursor-pointer -ml-4 hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
/>
<Badge
:variant="'subtle'"
:theme="'orange'"
size="sm"
:label="__('Unsaved')"
v-if="isDirty"
/>
</div>
<template #header-actions>
<div class="flex items-center gap-4">
<div
class="flex items-center justify-between gap-2"
@@ -43,263 +21,297 @@
theme="gray"
variant="solid"
@click="saveAssignmentRule()"
:loading="isLoading || getAssignmentRuleData.loading"
:loading="
isLoading ||
getAssignmentRuleData.loading ||
createAssignmentRuleResource.loading
"
/>
</div>
</div>
</div>
<div v-if="!getAssignmentRuleData.loading" class="overflow-y-auto px-10 pb-8">
<div class="grid grid-cols-2 gap-5">
<div>
<FormControl
:type="'text'"
size="sm"
variant="subtle"
placeholder="Name"
label="Name"
v-model="assignmentRuleData.assignmentRuleName"
required
maxlength="50"
@change="validateAssignmentRule('assignmentRuleName')"
/>
<ErrorMessage
:message="assignmentRulesErrors.assignmentRuleName"
class="mt-2"
/>
</template>
<template #content>
<div
v-if="getAssignmentRuleData.loading"
class="flex items-center justify-center absolute inset-x-0 top-5.5 bottom-0"
>
<LoadingIndicator class="w-4" />
</div>
<div class="flex flex-col gap-1.5">
<FormLabel label="Priority" />
<Popover>
<template #target="{ togglePopover }">
<div
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] cursor-default"
@click="togglePopover()"
>
<div>
<div v-if="!getAssignmentRuleData.loading">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div>
<FormControl
:type="'text'"
size="sm"
variant="subtle"
:placeholder="__('Name')"
:label="__('Name')"
v-model="assignmentRuleData.assignmentRuleName"
required
maxlength="50"
@change="validateAssignmentRule('assignmentRuleName')"
/>
<ErrorMessage
:message="assignmentRulesErrors.assignmentRuleName"
class="mt-2"
/>
</div>
<div class="flex flex-col gap-1.5">
<FormLabel :label="__('Priority')" />
<Popover bare>
<template #trigger>
<div
class="flex items-center justify-between text-base rounded-4 h-7 py-1.5 ps-2 pe-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-elevation-2 hover:bg-surface-gray-3 focus:bg-surface-base focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors w-full dark:[color-scheme:dark] cursor-default"
>
<div>
{{
priorityOptions.find(
(option) => option.value == assignmentRuleData.priority
)?.label
}}
</div>
<LucideChevronDown class="size-4" />
</div>
</template>
<template #default="{ close: closePopover }">
<div
class="p-1 text-ink-gray-6 top-1 absolute bg-surface-base shadow-2xl rounded-4 w-[--reka-popper-anchor-width]"
>
<div
v-for="option in priorityOptions"
:key="option.value"
class="p-2 cursor-pointer hover:bg-surface-sidebar text-base flex items-center justify-between rounded-4"
@click="
assignmentRuleData.priority = option.value;
closePopover();
"
>
{{ option.label }}
<LucideCheck
v-if="assignmentRuleData.priority == option.value"
class="size-4"
/>
</div>
</div>
</template>
</Popover>
</div>
<div>
<FormControl
:type="'textarea'"
size="sm"
variant="subtle"
:placeholder="__('Description')"
:label="__('Description')"
required
maxlength="140"
@change="validateAssignmentRule('description')"
v-model="assignmentRuleData.description"
/>
<ErrorMessage
:message="assignmentRulesErrors.description"
class="mt-2"
/>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-md-semibold text-ink-gray-8">{{
__("Assignment Condition")
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
priorityOptions.find(
(option) => option.value == assignmentRuleData.priority
)?.label
__(
"Choose which tickets are affected by this assignment rule."
)
}}
</div>
<FeatherIcon name="chevron-down" class="size-4" />
</div>
</template>
<template #body="{ togglePopover }">
<div
class="p-1 text-ink-gray-6 top-1 absolute w-full bg-white shadow-2xl rounded"
>
<a
class="font-medium underline"
href="https://docs.frappe.io/helpdesk/assignment-rule"
target="_blank"
>{{ __("Learn about conditions") }}</a
>
</span>
<div
v-for="option in priorityOptions"
:key="option.value"
class="p-2 cursor-pointer hover:bg-gray-50 text-base flex items-center justify-between rounded"
@click="
assignmentRuleData.priority = option.value;
togglePopover();
v-if="isAssignConditionOld && assignmentRulesActiveScreen.data"
>
<HoverCard :hoverDelay="250" side="top" align="end">
<template #trigger>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap flex items-center"
>
<span>{{ __("Old Condition") }}</span>
<LucideInfo class="size-4" />
</div>
</template>
<template #default>
<div
class="text-sm text-ink-gray-6 p-2 bg-surface-base rounded-5 max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.assignCondition }}</code>
</div>
</template>
</HoverCard>
</div>
</div>
</div>
<div class="mt-5">
<div
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-outline-gray-2 rounded-5 p-3 py-4"
v-if="
!useNewUIForAssignCondition &&
assignmentRuleData.assignCondition
"
>
<span class="text-p-sm">
Conditions for this rule were created from
<a :href="deskUrl" target="_blank" class="underline">desk</a>
which are not compatible with this UI, you will need to recreate
the conditions here if you want to manage and add new conditions
from this UI.
</span>
<Button
label="I understand, add conditions"
variant="subtle"
theme="gray"
@click="useNewUIForAssignCondition = true"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.assignConditionJson"
name="assignCondition"
:errors="assignmentRulesErrors.assignConditionError"
v-else
/>
<ErrorMessage
:message="assignmentRulesErrors.assignCondition"
class="mt-2"
/>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-md-semibold text-ink-gray-8">{{
__("Unassignment Condition")
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__(
"Choose which tickets are affected by this un-assignment rule."
)
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/helpdesk/assignment-rule"
target="_blank"
>{{ __("Learn about conditions") }}</a
>
</span>
<div
v-if="
isUnassignConditionOld &&
assignmentRulesActiveScreen.data &&
assignmentRuleData.unassignCondition
"
>
{{ option.label }}
<FeatherIcon
v-if="assignmentRuleData.priority == option.value"
name="check"
class="size-4"
/>
<HoverCard :hoverDelay="250" side="top" align="end">
<template #trigger>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap flex items-center"
>
<span> {{ __("Old Condition") }} </span>
<LucideInfo class="size-4" />
</div>
</template>
<template #default>
<div
class="text-sm text-ink-gray-6 p-2 bg-surface-base rounded-5 max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.unassignCondition }}</code>
</div>
</template>
</HoverCard>
</div>
</div>
</template>
</Popover>
</div>
<div>
<FormControl
:type="'textarea'"
size="sm"
variant="subtle"
placeholder="Description"
label="Description"
required
maxlength="250"
@change="validateAssignmentRule('description')"
v-model="assignmentRuleData.description"
/>
<ErrorMessage
:message="assignmentRulesErrors.description"
class="mt-2"
/>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__("Assignment condition")
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__("Choose which tickets are affected by this assignment rule.")
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/helpdesk/assignment-rule"
target="_blank"
>{{ __("Learn about conditions") }}</a
</div>
<div class="mt-5">
<div
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-outline-gray-2 rounded-5 p-3 py-4"
v-if="
!useNewUIForUnassignCondition &&
assignmentRuleData.unassignCondition
"
>
</span>
<div v-if="isOldSla && assignmentRulesActiveScreen.data">
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap flex items-center"
>
<span>{{ __("Old Condition") }}</span>
<FeatherIcon name="info" class="size-4" />
</div>
</template>
<template #body-main>
<div
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.assignCondition }}</code>
</div>
</template>
</Popover>
<span class="text-p-sm">
Conditions for this rule were created from
<a :href="deskUrl" target="_blank" class="underline">desk</a>
which are not compatible with this UI, you will need to recreate
the conditions here if you want to manage and add new conditions
from this UI.
</span>
<Button
label="I understand, add conditions"
variant="subtle"
theme="gray"
@click="useNewUIForUnassignCondition = true"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.unassignConditionJson"
name="unassignCondition"
:errors="assignmentRulesErrors.unassignConditionError"
v-else
/>
</div>
</div>
</div>
<div class="mt-5">
<div
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-gray-300 rounded-md p-3 py-4"
v-if="!useNewUI && assignmentRuleData.assignCondition"
>
<span class="text-p-sm">
Conditions for this rule were created from
<a :href="deskUrl" target="_blank" class="underline">desk</a> which
are not compatible with this UI, you will need to recreate the
conditions here if you want to manage and add new conditions from
this UI.
</span>
<Button
label="I understand, add conditions"
variant="subtle"
theme="gray"
@click="useNewUI = true"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.assignConditionJson"
name="assignCondition"
:errors="assignmentRulesErrors.assignConditionError"
v-else
/>
<ErrorMessage
:message="assignmentRulesErrors.assignCondition"
class="mt-2"
/>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__("Unassignment condition")
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__(
"Choose which tickets are affected by this un-assignment rule."
)
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/helpdesk/assignment-rule"
target="_blank"
>{{ __("Learn about conditions") }}</a
>
</span>
<div
v-if="
isOldSla &&
assignmentRulesActiveScreen.data &&
assignmentRuleData.unassignCondition
"
>
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap flex items-center"
>
<span> {{ __("Old Condition") }} </span>
<FeatherIcon name="info" class="size-4" />
</div>
</template>
<template #body-main>
<div
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.unassignCondition }}</code>
</div>
</template>
</Popover>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-md-semibold text-ink-gray-8">{{
__("Assignment Schedule")
}}</span>
<span class="text-p-sm text-ink-gray-6">
{{
__(
"Choose the days of the week when this rule should be active."
)
}}
</span>
</div>
<div class="mt-6">
<AssignmentSchedule />
</div>
</div>
<hr class="my-8" />
<AssigneeRules />
</div>
<div class="mt-5">
<div
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-gray-300 rounded-md p-3 py-4"
v-if="!useNewUI && assignmentRuleData.unassignCondition"
>
<span class="text-p-sm">
Conditions for this rule were created from
<a :href="deskUrl" target="_blank" class="underline">desk</a> which
are not compatible with this UI, you will need to recreate the
conditions here if you want to manage and add new conditions from
this UI.
</span>
<Button
label="I understand, add conditions"
variant="subtle"
theme="gray"
@click="useNewUI = true"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.unassignConditionJson"
name="unassignCondition"
:errors="assignmentRulesErrors.unassignConditionError"
v-else
/>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__("Assignment Schedule")
}}</span>
<span class="text-p-sm text-ink-gray-6">
{{
__("Choose the days of the week when this rule should be active.")
}}
</span>
</div>
<div class="mt-6">
<AssignmentSchedule />
</div>
</div>
<hr class="my-8" />
<AssigneeRules />
</div>
</template>
</SettingsLayoutBase>
<ConfirmDialog
v-model="showConfirmDialog.show"
:title="showConfirmDialog.title"
:message="showConfirmDialog.message"
:onConfirm="showConfirmDialog.onConfirm"
:onCancel="() => (showConfirmDialog.show = false)"
:onCancel="
() => {
if (showConfirmDialog.onCancel) {
showConfirmDialog.onCancel();
} else {
showConfirmDialog.show = false;
}
}
"
/>
</template>
<script setup lang="ts">
import LucideCheck from "~icons/lucide/check";
import LucideChevronDown from "~icons/lucide/chevron-down";
import LucideInfo from "~icons/lucide/info";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import {
Badge,
@@ -309,12 +321,13 @@ import {
ErrorMessage,
FormControl,
FormLabel,
HoverCard,
LoadingIndicator,
Popover,
Switch,
toast,
} from "frappe-ui";
import { onMounted, onUnmounted, ref, watch } from "vue";
import { onMounted, onUnmounted, provide, ref, watch } from "vue";
import {
assignmentRuleData,
assignmentRulesActiveScreen,
@@ -322,25 +335,37 @@ import {
resetAssignmentRuleData,
resetAssignmentRuleErrors,
validateAssignmentRule,
} from "../../../stores/assignmentRules";
} from "@/stores/assignmentRules";
import AssigneeRules from "./AssigneeRules.vue";
import AssignmentRulesSection from "./AssignmentRulesSection.vue";
import AssignmentSchedule from "./AssignmentSchedule.vue";
import { convertToConditions } from "@/utils";
import { disableSettingModalOutsideClick } from "../settingsModal";
import { __ } from "@/translation";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
const isDirty = ref(false);
const initialData = ref(null);
const isLoading = ref(false);
provide("isAssignmentRuleFormDirty", isDirty);
const showConfirmDialog = ref({
const showConfirmDialog = ref<{
show: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel?: () => void;
}>({
show: false,
title: "",
message: "",
onConfirm: () => {},
});
const useNewUI = ref(true);
const isOldSla = ref(false);
const useNewUIForAssignCondition = ref(true);
const useNewUIForUnassignCondition = ref(true);
const isAssignConditionOld = ref(false);
const isUnassignConditionOld = ref(false);
const deskUrl = `${window.location.origin}/app/assignment-rule/${assignmentRulesActiveScreen.value.data?.name}`;
const getAssignmentRuleData = createResource({
@@ -351,12 +376,30 @@ const getAssignmentRuleData = createResource({
},
auto: Boolean(assignmentRulesActiveScreen.value.data),
onSuccess(data) {
let assignConditionJson;
let unassignConditionJson;
try {
assignConditionJson = JSON.parse(data.assign_condition_json || "[]");
} catch (error) {
toast.error(
"Assignment conditions are invalid or corrupt, recreate the conditions."
);
assignConditionJson = [];
}
try {
unassignConditionJson = JSON.parse(data.unassign_condition_json || "[]");
} catch (error) {
toast.error(
"Unassignment conditions are invalid or corrupt, recreate the conditions."
);
unassignConditionJson = [];
}
assignmentRuleData.value = {
loading: false,
assignCondition: data.assign_condition,
unassignCondition: data.unassign_condition,
assignConditionJson: JSON.parse(data.assign_condition_json || "[]"),
unassignConditionJson: JSON.parse(data.unassign_condition_json || "[]"),
assignConditionJson: assignConditionJson,
unassignConditionJson: unassignConditionJson,
rule: data.rule,
priority: data.priority,
users: data.users,
@@ -369,43 +412,52 @@ const getAssignmentRuleData = createResource({
initialData.value = JSON.stringify(assignmentRuleData.value);
const conditionsAvailable =
const assignConditionsAvailable =
assignmentRuleData.value.assignCondition?.length > 0;
const conditionsJsonAvailable =
const assignConditionsJsonAvailable =
assignmentRuleData.value.assignConditionJson?.length > 0;
if (conditionsAvailable && !conditionsJsonAvailable) {
useNewUI.value = false;
isOldSla.value = true;
if (assignConditionsAvailable && !assignConditionsJsonAvailable) {
useNewUIForAssignCondition.value = false;
isAssignConditionOld.value = true;
} else {
useNewUI.value = true;
isOldSla.value = false;
useNewUIForAssignCondition.value = true;
isAssignConditionOld.value = false;
}
const unassignConditionsAvailable =
assignmentRuleData.value.unassignCondition?.length > 0;
const unassignConditionsJsonAvailable =
assignmentRuleData.value.unassignConditionJson?.length > 0;
if (unassignConditionsAvailable && !unassignConditionsJsonAvailable) {
useNewUIForUnassignCondition.value = false;
isUnassignConditionOld.value = true;
} else {
useNewUIForUnassignCondition.value = true;
isUnassignConditionOld.value = false;
}
},
});
if (!assignmentRulesActiveScreen.value.data) {
disableSettingModalOutsideClick.value = true;
initialData.value = JSON.stringify(assignmentRuleData.value);
}
const goBack = () => {
const confirmDialogInfo = {
show: true,
title: "Unsaved changes",
message: "Are you sure you want to go back? Unsaved changes will be lost.",
title: __("Unsaved changes"),
message: __(
"Are you sure you want to go back? Unsaved changes will be lost."
),
onConfirm: goBack,
};
if (isDirty.value && !showConfirmDialog.value.show) {
showConfirmDialog.value = confirmDialogInfo;
return;
}
if (
!assignmentRulesActiveScreen.value.data &&
!showConfirmDialog.value.show
) {
showConfirmDialog.value = confirmDialogInfo;
return;
}
// Workaround fix for settings modal not closing after going back
setTimeout(() => {
assignmentRulesActiveScreen.value = {
@@ -417,27 +469,26 @@ const goBack = () => {
};
const saveAssignmentRule = () => {
const validationErrors = validateAssignmentRule(undefined, !useNewUI.value);
const validationErrors = validateAssignmentRule(
undefined,
!useNewUIForAssignCondition.value,
!useNewUIForUnassignCondition.value
);
if (Object.values(validationErrors).some((error) => error)) {
toast.error(
"Invalid fields, check if all are filled in and values are correct."
__("Invalid fields, check if all are filled in and values are correct.")
);
return;
}
if (assignmentRulesActiveScreen.value.data) {
if (isOldSla.value && useNewUI.value) {
showConfirmDialog.value = {
show: true,
title: "Confirm overwrite",
message:
"Your old condition will be overwritten. Are you sure you want to save?",
onConfirm: () => {
updateAssignmentRule();
showConfirmDialog.value.show = false;
},
};
if (isAssignConditionOld.value && useNewUIForAssignCondition.value) {
showOverwriteConfirm();
return;
}
if (isUnassignConditionOld.value && useNewUIForUnassignCondition.value) {
showOverwriteConfirm();
return;
}
updateAssignmentRule();
@@ -446,60 +497,65 @@ const saveAssignmentRule = () => {
}
};
const showOverwriteConfirm = () => {
showConfirmDialog.value = {
show: true,
title: "Confirm overwrite",
message:
"Your old condition will be overwritten. Are you sure you want to save?",
onConfirm: () => {
updateAssignmentRule();
showConfirmDialog.value.show = false;
},
};
};
const createAssignmentRule = () => {
isLoading.value = true;
createResource({
url: "frappe.client.insert",
params: {
doc: {
doctype: "Assignment Rule",
document_type: "HD Ticket",
rule: assignmentRuleData.value.rule,
priority: assignmentRuleData.value.priority,
users: assignmentRuleData.value.users,
disabled: assignmentRuleData.value.disabled,
description: assignmentRuleData.value.description,
assignment_days: assignmentRuleData.value.assignmentDays.map((day) => ({
day: day,
})),
name: assignmentRuleData.value.assignmentRuleName,
assignment_rule_name: assignmentRuleData.value.assignmentRuleName,
assign_condition: convertToConditions({
conditions: assignmentRuleData.value.assignConditionJson,
}),
unassign_condition: convertToConditions({
conditions: assignmentRuleData.value.unassignConditionJson,
}),
assign_condition_json: JSON.stringify(
assignmentRuleData.value.assignConditionJson
),
unassign_condition_json: JSON.stringify(
assignmentRuleData.value.unassignConditionJson
),
},
},
auto: true,
onSuccess(data) {
getAssignmentRuleData
.submit({
doctype: "Assignment Rule",
name: data.name,
})
.then(() => {
isLoading.value = false;
toast.success("Assignment rule created");
});
assignmentRulesActiveScreen.value = {
screen: "view",
data: data,
};
},
onError: () => {
isLoading.value = false;
createAssignmentRuleResource.submit({
doc: {
doctype: "Assignment Rule",
document_type: "HD Ticket",
rule: assignmentRuleData.value.rule,
priority: assignmentRuleData.value.priority,
users: assignmentRuleData.value.users,
disabled: assignmentRuleData.value.disabled,
description: assignmentRuleData.value.description,
assignment_days: assignmentRuleData.value.assignmentDays.map((day) => ({
day: day,
})),
name: assignmentRuleData.value.assignmentRuleName,
assignment_rule_name: assignmentRuleData.value.assignmentRuleName,
assign_condition: convertToConditions({
conditions: assignmentRuleData.value.assignConditionJson,
}),
unassign_condition: convertToConditions({
conditions: assignmentRuleData.value.unassignConditionJson,
}),
assign_condition_json: JSON.stringify(
assignmentRuleData.value.assignConditionJson
),
unassign_condition_json: JSON.stringify(
assignmentRuleData.value.unassignConditionJson
),
},
});
};
const createAssignmentRuleResource = createResource({
url: "frappe.client.insert",
onSuccess(data) {
toast.success(__("Assignment rule created successfully."));
assignmentRulesActiveScreen.value = {
screen: "view",
data: data,
};
getAssignmentRuleData.submit({
doctype: "Assignment Rule",
name: data.name,
});
},
});
const priorityOptions = [
{ label: "Low", value: "0" },
{ label: "Low-Medium", value: "1" },
@@ -524,26 +580,27 @@ const updateAssignmentRule = async () => {
assignment_days: assignmentRuleData.value.assignmentDays.map((day) => ({
day: day,
})),
assign_condition: useNewUI.value
assign_condition: useNewUIForAssignCondition.value
? convertToConditions({
conditions: assignmentRuleData.value.assignConditionJson,
})
: assignmentRuleData.value.assignCondition,
unassign_condition: useNewUI.value
unassign_condition: useNewUIForUnassignCondition.value
? convertToConditions({
conditions: assignmentRuleData.value.unassignConditionJson,
})
: assignmentRuleData.value.unassignCondition,
assign_condition_json: useNewUI.value
assign_condition_json: useNewUIForAssignCondition.value
? JSON.stringify(assignmentRuleData.value.assignConditionJson)
: null,
unassign_condition_json: useNewUI.value
unassign_condition_json: useNewUIForUnassignCondition.value
? JSON.stringify(assignmentRuleData.value.unassignConditionJson)
: null,
},
}).catch((er) => {
const error =
er?.messages?.[0] || "Some error occurred while updating assignment rule";
er?.messages?.[0] ||
__("Some error occurred while updating assignment rule");
toast.error(error);
isLoading.value = false;
});
@@ -559,7 +616,7 @@ const updateAssignmentRule = async () => {
}).catch(async (er) => {
const error =
er?.messages?.[0] ||
"Some error occurred while renaming assignment rule";
__("Some error occurred while renaming assignment rule");
toast.error(error);
// Reset assignment rule to previous state
await getAssignmentRuleData.reload();
@@ -574,7 +631,7 @@ const updateAssignmentRule = async () => {
}
isLoading.value = false;
toast.success("Assignment rule updated");
toast.success(__("Assignment rule updated successfully."));
};
watch(

View File

@@ -1,12 +1,15 @@
<template>
<div class="pb-8">
<AssignmentRulesList v-if="assignmentRulesActiveScreen.screen === 'list'" />
<AssignmentRuleView v-if="assignmentRulesActiveScreen.screen === 'view'" />
</div>
<AssignmentRulesList v-if="assignmentRulesActiveScreen.screen === 'list'" />
<AssignmentRuleView v-if="assignmentRulesActiveScreen.screen === 'view'" />
</template>
<script setup lang="ts">
import { assignmentRulesActiveScreen } from "../../../stores/assignmentRules";
import { provide, ref } from "vue";
import { assignmentRulesActiveScreen } from "@/stores/assignmentRules";
import AssignmentRulesList from "./AssignmentRulesList.vue";
import AssignmentRuleView from "./AssignmentRuleView.vue";
const assignmentRuleSearchQuery = ref("");
provide("assignmentRuleSearchQuery", assignmentRuleSearchQuery);
</script>

View File

@@ -1,48 +1,83 @@
<template>
<div class="px-10 py-8 sticky top-0">
<div class="flex items-start justify-between">
<div class="flex flex-col gap-1">
<h1 class="text-lg font-semibold text-ink-gray-8">
{{ __("Assignment rules") }}
</h1>
<p class="text-p-sm text-ink-gray-6 max-w-md">
{{
__(
"Assignment Rules automatically route tickets to the right team members based on predefined conditions."
)
}}
</p>
</div>
<SettingsLayoutBase>
<template #title>
<h1 class="text-md-semibold text-ink-gray-8">
{{ __("Assignment Rules") }}
</h1>
</template>
<template #description>
<p class="text-p-sm max-w-md text-ink-gray-6">
{{
__(
"Assignment Rules automatically route tickets to the right team members based on predefined conditions."
)
}}
</p>
</template>
<template #header-actions>
<Button
:label="__('Create new')"
:label="__('New')"
theme="gray"
variant="solid"
@click="goToNew()"
icon-left="plus"
icon-left="lucide-plus"
class="rtl:flex-row-reverse"
/>
</div>
</div>
<div class="overflow-y-auto px-10 pb-8">
<AssignmentRulesListView />
</div>
</template>
<template
v-if="
assignmentRulesListData.data?.length > 9 ||
assignmentRuleSearchQuery.length
"
#header-bottom
>
<div class="relative">
<TextInput
:model-value="assignmentRuleSearchQuery"
@update:model-value="assignmentRuleSearchQuery = $event"
:placeholder="__('Search')"
type="text"
class="focus:ring-0 border-outline-gray-2"
:debounce="300"
>
<template #prefix>
<LucideSearch class="size-4" />
</template>
</TextInput>
<Button
v-if="assignmentRuleSearchQuery"
icon="lucide-x"
variant="ghost"
@click="assignmentRuleSearchQuery = ''"
class="absolute end-1 top-1/2 -translate-y-1/2"
/>
</div>
</template>
<template #content>
<AssignmentRulesListView />
</template>
</SettingsLayoutBase>
</template>
<script setup lang="ts">
import { Button, createResource } from "frappe-ui";
import { provide } from "vue";
import { inject, provide, Ref } from "vue";
import {
assignmentRulesActiveScreen,
resetAssignmentRuleData,
} from "../../../stores/assignmentRules";
} from "@/stores/assignmentRules";
import AssignmentRulesListView from "./AssignmentRulesListView.vue";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import { AssignmentRuleListResourceSymbol } from "@/types";
const assignmentRulesListData = createResource({
url: "helpdesk.api.assignment_rule.get_assignment_rules_list",
cache: ["assignmentRules", "get_assignment_rules_list"],
auto: true,
});
const assignmentRuleSearchQuery = inject<Ref>("assignmentRuleSearchQuery");
provide("assignmentRulesList", assignmentRulesListData);
provide(AssignmentRuleListResourceSymbol, assignmentRulesListData);
const goToNew = () => {
resetAssignmentRuleData();

View File

@@ -1,32 +1,42 @@
<template>
<div
v-if="assignmentRulesList.loading && !assignmentRulesList.data"
class="flex items-center justify-center mt-12"
v-if="assignmentRulesListData.loading && !assignmentRulesListData.data"
class="flex items-center justify-center absolute inset-x-0 top-5.5 bottom-0"
>
<LoadingIndicator class="w-4" />
</div>
<div v-else>
<div v-else class="-ms-2 grow">
<div
v-if="assignmentRulesList.data?.length === 0"
class="flex items-center justify-center rounded-md border border-gray-200 p-4"
v-if="!assignmentRulesListData.loading && !assignmentRulesList?.length"
class="flex flex-col items-center justify-center gap-4 h-full"
>
<div class="text-sm text-ink-gray-7">
{{ __("No items in the list") }}
<div
class="p-4 size-14.5 rounded-full bg-surface-gray-1 flex justify-center items-center"
>
<Settings class="size-6 text-ink-gray-6 rotate-90" />
</div>
<div class="flex flex-col items-center gap-1">
<div class="text-base-medium text-ink-gray-6">
{{ __("No assignment rule found") }}
</div>
<div class="text-p-sm text-ink-gray-5 max-w-60 text-center">
{{ __("Add one to get started.") }}
</div>
</div>
</div>
<div v-else>
<div class="grid grid-cols-11 items-center gap-4 text-sm text-gray-600">
<div class="col-span-7 ml-2">{{ __("Assignment rule") }}</div>
<div class="col-span-2">{{ __("Priority") }}</div>
<div class="grid grid-cols-12 items-center gap-4 text-sm text-ink-gray-5">
<div class="col-span-7 ms-2">{{ __("Assignment rule") }}</div>
<div class="col-span-3">{{ __("Priority") }}</div>
<div class="col-span-2">{{ __("Enabled") }}</div>
</div>
<hr class="mt-2 mx-2" />
<div
v-for="assignmentRule in assignmentRulesList.data"
v-for="(assignmentRule, index) in assignmentRulesList"
:key="assignmentRule.name"
>
<AssignmentRuleListItem :data="assignmentRule" />
<hr class="mx-2" />
<hr v-if="index !== assignmentRulesList.length - 1" class="mx-2" />
</div>
</div>
</div>
@@ -34,8 +44,43 @@
<script setup lang="ts">
import { LoadingIndicator } from "frappe-ui";
import { inject } from "vue";
import { inject, Ref, ref, watch } from "vue";
import AssignmentRuleListItem from "./AssignmentRuleListItem.vue";
import {
assignmentRulesActiveScreen,
resetAssignmentRuleData,
} from "@/stores/assignmentRules";
import Settings from "~icons/lucide/settings-2";
import { AssignmentRuleListResourceSymbol } from "@/types";
const assignmentRulesList = inject<any>("assignmentRulesList");
const assignmentRulesListData = inject(AssignmentRuleListResourceSymbol);
const assignmentRulesList = ref(assignmentRulesListData.data || []);
const assignmentRuleSearchQuery = inject<Ref>("assignmentRuleSearchQuery");
const goToNew = () => {
resetAssignmentRuleData();
assignmentRulesActiveScreen.value = {
screen: "view",
data: null,
};
};
watch(
() => [assignmentRuleSearchQuery.value, assignmentRulesListData.data],
([query, data]) => {
if (!query) {
assignmentRulesList.value = data || [];
return;
}
assignmentRulesList.value =
data?.filter((item) => {
return (
item.name.toLowerCase().includes(query.toLowerCase()) ||
item.description.toLowerCase().includes(query.toLowerCase())
);
}) || [];
}
);
</script>

View File

@@ -7,13 +7,13 @@
/>
<div
v-if="props.conditions.length == 0"
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-gray-300 text-gray-600 rounded-md"
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-outline-gray-2 text-ink-gray-5 rounded-5"
@click="
props.conditions.push(['', '', '']);
validateAssignmentRule(props.name);
"
>
<FeatherIcon name="plus" class="h-4" />
<LucidePlus class="size-4" />
{{ __("Add a condition") }}
</div>
<div class="flex items-center justify-between mt-2">
@@ -25,7 +25,7 @@
>
<Button
:disabled="props.errors !== ''"
:icon-right="open ? 'chevron-up' : 'chevron-down'"
:icon-right="open ? 'lucide-chevron-up' : 'lucide-chevron-down'"
:label="__('Add condition')"
/>
</Dropdown>
@@ -38,11 +38,13 @@
</template>
<script setup lang="ts">
import LucidePlus from "~icons/lucide/plus";
import CFConditions from "@/components/conditions-filter/CFConditions.vue";
import { validateConditions } from "@/utils";
import { watchDebounced } from "@vueuse/core";
import { Button, Dropdown, ErrorMessage, FeatherIcon } from "frappe-ui";
import { validateAssignmentRule } from "../../../stores/assignmentRules";
import { Button, Dropdown, ErrorMessage } from "frappe-ui";
import { validateAssignmentRule } from "@/stores/assignmentRules";
import { __ } from "@/translation";
const props = defineProps({
conditions: Array<any>,
@@ -62,13 +64,13 @@ const getConjunction = () => {
const dropdownOptions = [
{
label: "Add condition",
label: __("Add condition"),
onClick: () => {
addCondition();
},
},
{
label: "Add condition group",
label: __("Add condition group"),
onClick: () => {
const conjunction = getConjunction();
props.conditions.push(conjunction, [[]]);

View File

@@ -1,5 +1,5 @@
<template>
<div class="rounded-md border px-2 border-gray-300 text-sm">
<div class="rounded-5 border px-2 border-outline-gray-2 text-sm">
<div
class="grid p-2 px-4 items-center"
style="grid-template-columns: 3fr 1fr"
@@ -7,7 +7,7 @@
<div
v-for="column in columns"
:key="column.key"
class="text-gray-600 overflow-hidden whitespace-nowrap text-ellipsis"
class="text-ink-gray-5 overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ column.label }}
</div>
@@ -29,16 +29,17 @@ import { onMounted, ref } from "vue";
import {
assignmentRuleData,
assignmentRulesErrors,
} from "../../../stores/assignmentRules";
} from "@/stores/assignmentRules";
import AssignmentScheduleItem from "./AssignmentScheduleItem.vue";
import { __ } from "@/translation";
const columns = [
{
label: "Days",
label: __("Days"),
key: "day",
},
{
label: "Active",
label: __("Active"),
key: "active",
},
];

View File

@@ -5,7 +5,15 @@
>
<div class="text-ink-gray-7 font-medium">{{ data.day }}</div>
<div class="flex justify-start">
<Switch v-model="data.active" @update:model-value="toggleDay" />
<Switch
:model-value="Boolean(data.active)"
@update:model-value="
(value) => {
data.active = value;
toggleDay(value);
}
"
/>
</div>
</div>
<hr v-if="!isLast" />

View File

@@ -1,140 +0,0 @@
<template>
<div class="flex flex-col gap-4 px-10 py-8">
<SettingsLayoutHeader
title="Customise your Helpdesk"
description="Customise Helpdesk with your own branding."
/>
<!-- Brand Logo & Favicon -->
<div v-for="config in brandingConfig" class="flex flex-col gap-2">
<p class="text-sm text-gray-600">{{ config.title }}</p>
<div class="flex gap-4 items-center">
<Avatar
v-if="config.image && !config.loading"
size="3xl"
:image="config.image"
/>
<FileUploader
v-if="!config.image"
:fileTypes="['image/*']"
@success="
(file) => {
update(file.file_url, config.doctype, config.fieldname);
}
"
>
<template #default="{ openFileSelector }">
<Button
@click="openFileSelector()"
iconLeft="upload"
label="Upload Image"
:loading="config.loading"
/>
</template>
</FileUploader>
<div v-else>
<Button
label="Remove"
@click="update('', config.doctype, config.fieldname)"
iconLeft="trash"
:loading="config.loading"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useConfigStore } from "@/stores/config";
import { Avatar, createResource, FileUploader, toast } from "frappe-ui";
import { computed, reactive } from "vue";
import SettingsLayoutHeader from "./SettingsLayoutHeader.vue";
const config = useConfigStore();
const websiteSettings = createResource({
url: "frappe.client.get_value",
cache: true,
params: {
doctype: "Website Settings",
fieldname: "favicon",
},
onSuccess(data) {
state.brandFavicon = data.favicon;
},
auto: true,
});
const state = reactive({
brandLogo: config.brandLogo,
brandFavicon: websiteSettings.data?.favicon || "",
});
const loadingState = reactive({
logoLoading: false,
faviconLoading: false,
});
const brandingConfig = computed(() => [
{
title: "Brand Logo",
image: state.brandLogo,
doctype: "HD Settings",
fieldname: "brand_logo",
loading: loadingState.logoLoading,
},
{
title: "Brand Favicon",
image: state.brandFavicon,
doctype: "Website Settings",
fieldname: "favicon",
loading: loadingState.faviconLoading,
},
]);
const settingsResource = createResource({
url: "frappe.client.set_value",
debounce: 1000,
onSuccess(data) {
if (data.doctype === "HD Settings") {
handleLogoChange(data.brand_logo);
} else {
handleFaviconChange(data.favicon);
}
},
onError() {
toast.error("Failed to update, please try again");
loadingState.logoLoading = false;
loadingState.faviconLoading = false;
},
});
function update(file: string, doctype: string, fieldname: string) {
settingsResource.submit({
doctype: doctype,
name: doctype,
fieldname: fieldname,
value: file,
});
doctype === "HD Settings"
? (loadingState.logoLoading = true)
: (loadingState.faviconLoading = true);
}
function handleLogoChange(url: string) {
state.brandLogo = url;
loadingState.logoLoading = false;
toast.success("Brand logo updated");
}
function handleFaviconChange(url: string) {
state.brandFavicon = url;
loadingState.faviconLoading = false;
toast.success("Favicon updated, please refresh the page to see the changes");
}
</script>
<style scoped></style>

View File

@@ -1,15 +1,17 @@
<template>
<div
class="flex justify-between items-center border-gray-200 p-2 pl-0 cursor-pointer"
class="flex justify-between items-center border-outline-elevation-2 p-2 cursor-pointer hover:bg-surface-sidebar rounded-4 h-14"
>
<!-- avatar and name -->
<div class="flex justify-between items-center gap-2">
<EmailProviderIcon :logo="emailIcon[emailAccount.service]" />
<div>
<p class="text-gray-700 font-semibold">
{{ emailAccount.email_account_name }}
<div class="rtl:text-right">
<p class="text-p-base-medium text-ink-gray-7">
{{ __(emailAccount.email_account_name) }}
</p>
<div class="text-sm text-gray-500">{{ emailAccount.email_id }}</div>
<div class="text-p-sm w-full text-ink-gray-5 mt-1">
{{ __(emailAccount.email_id) }}
</div>
</div>
</div>
<div>
@@ -28,6 +30,7 @@ import { EmailAccount } from "@/types";
import { emailIcon } from "./emailConfig";
import EmailProviderIcon from "./EmailProviderIcon.vue";
import { computed } from "vue";
import { __ } from "@/translation";
interface P {
emailAccount: EmailAccount;
@@ -44,16 +47,16 @@ const badgeTitleColor = computed(() => {
props.emailAccount.enable_incoming && props.emailAccount.enable_outgoing
? "blue"
: "gray";
return ["Default Sending and Inbox", color];
return [__("Default Sending and Inbox"), color];
} else if (props.emailAccount.default_incoming) {
const color = props.emailAccount.enable_incoming ? "blue" : "gray";
return ["Default Inbox", color];
return [__("Default Inbox"), color];
} else if (props.emailAccount.default_outgoing) {
const color = props.emailAccount.enable_outgoing ? "blue" : "gray";
return ["Default Sending", color];
return [__("Default Sending"), color];
} else {
const color = props.emailAccount.enable_incoming ? "blue" : "gray";
return ["Inbox", color];
return [__("Inbox"), color];
}
});
</script>

View File

@@ -1,44 +1,68 @@
<template>
<div>
<!-- header -->
<SettingsLayoutHeader
title="Email Accounts"
description="Manage your email accounts and configure incoming and outgoing settings."
>
<template #actions>
<Button
label="Add Account"
theme="gray"
variant="solid"
@click="emit('update:step', 'email-add')"
icon-left="plus"
/>
</template>
</SettingsLayoutHeader>
<!-- list accounts -->
<div
v-if="!emailAccounts.loading && Boolean(emailAccounts.data?.length)"
class="mt-4 divide-y"
>
<div v-for="emailAccount in emailAccounts.data" :key="emailAccount.name">
<EmailAccountCard
:emailAccount="emailAccount"
@click="emit('update:step', 'email-edit', emailAccount)"
/>
<SettingsLayoutBase
:title="__('Email Accounts')"
:description="
__(
'Manage your email accounts and configure incoming and outgoing settings.'
)
"
>
<template #header-actions>
<Button
:label="__('New')"
theme="gray"
class="rtl:flex-row-reverse"
variant="solid"
@click="emit('update:step', 'email-add')"
icon-left="lucide-plus"
/>
</template>
<template #content>
<!-- list accounts -->
<div
class="-ms-2 grow"
v-if="!emailAccounts.loading && Boolean(emailAccounts.data?.length)"
>
<div class="flex text-sm text-ink-gray-5">
<div class="ms-2">{{ __("Email account name") }}</div>
</div>
<hr class="mx-2 mt-2" />
<div
v-for="emailAccount in emailAccounts.data"
:key="emailAccount.name"
>
<EmailAccountCard
:emailAccount="emailAccount"
@click="emit('update:step', 'email-edit', emailAccount)"
/>
<hr
class="mx-2"
v-if="
emailAccount !==
emailAccounts?.data[emailAccounts?.data?.length - 1]
"
/>
</div>
</div>
</div>
<!-- fallback if no email accounts -->
<div v-else class="flex items-center justify-center h-64 text-gray-500">
Please add an email account to continue.
</div>
</div>
<!-- fallback if no email accounts -->
<EmptyState
v-else-if="!emailAccounts.loading"
variant="badge"
:icon="EmailIcon"
:title="__('No email account found')"
:description="__('Add one to get started.')"
/>
</template>
</SettingsLayoutBase>
</template>
<script setup lang="ts">
import EmptyState from "@/components/EmptyState.vue";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import { EmailAccount } from "@/types";
import { createListResource } from "frappe-ui";
import { EmailIcon } from "../icons";
import EmailAccountCard from "./EmailAccountCard.vue";
import SettingsLayoutHeader from "./SettingsLayoutHeader.vue";
const emit = defineEmits(["update:step"]);

View File

@@ -1,106 +1,180 @@
<template>
<div class="flex h-full flex-col gap-4">
<!-- title and desc -->
<div role="heading" aria-level="1" class="flex flex-col gap-1">
<h5 class="text-lg font-semibold pt-[5px]">Setup Email</h5>
<p class="text-sm text-gray-600">
Choose the email service provider you want to configure.
</p>
</div>
<!-- email service provider selection -->
<div class="flex flex-wrap items-center gap-4">
<div
v-for="s in services"
:key="s.name"
class="min-w-3 flex flex-col items-center gap-1"
@click="handleSelect(s)"
>
<EmailProviderIcon
:service-name="s.name"
:logo="s.icon"
:selected="selectedService?.name === s?.name"
/>
</div>
</div>
<div v-if="selectedService" class="flex flex-col gap-4">
<!-- email service provider info -->
<div class="flex items-center gap-2 rounded-md p-2 ring-1 ring-gray-200">
<CircleAlert
class="h-6 w-5 w-min-5 w-max-5 min-h-5 max-w-5 text-blue-500"
/>
<div class="text-wrap text-xs text-gray-700">
{{ selectedService.info }}
<a
:href="selectedService.link"
target="_blank"
class="text-blue-500 underline"
>here</a
>
.
</div>
</div>
<!-- service provider fields -->
<div class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4">
<div
v-for="field in fields"
:key="field.name"
class="flex flex-col gap-1"
>
<FormControl
v-model="state[field.name]"
:label="field.label"
:name="field.name"
:type="field.type"
:placeholder="field.placeholder"
/>
<SettingsLayoutBase
:title="__('Setup Email')"
:description="
__('Choose the email service provider you want to configure.')
"
>
<template #content>
<div class="flex h-full flex-col gap-4">
<div class="flex flex-col gap-4 overflow-y-auto p-0.5">
<!-- email service provider selection -->
<div class="flex flex-wrap items-center gap-4">
<div
v-for="s in services"
:key="s.name"
class="min-w-3 flex flex-col items-center gap-1"
@click="handleSelect(s)"
>
<EmailProviderIcon
:service-name="s.name"
:logo="s.icon"
:selected="selectedService?.name === s?.name"
/>
</div>
</div>
<div v-if="selectedService" class="flex flex-col gap-4">
<!-- email service provider info -->
<div>
<div
class="flex items-center gap-2 rounded-5 p-2 ring-1 ring-outline-elevation-2"
>
<CircleAlert
class="h-6 w-5 w-min-5 w-max-5 min-h-5 max-w-5 text-ink-blue-5"
/>
<div class="text-wrap text-xs text-ink-gray-7">
{{ selectedService.info }}
<a
:href="selectedService.link"
target="_blank"
class="text-ink-blue-5 underline"
>here</a
>.
</div>
</div>
</div>
<!-- service provider fields -->
<div class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4">
<div
v-for="field in fields"
:key="field.name"
class="flex flex-col gap-1"
>
<div
v-if="field.name === 'domain'"
class="flex flex-col gap-2"
>
<Link
v-model="customState.domain"
:label="field.label"
:placeholder="field.placeholder"
doctype="Email Domain"
:onCreate="handleCreateDomainClick"
/>
<div v-if="customState.domain" class="flex gap-2">
<Button
size="sm"
variant="subtle"
theme="gray"
:label="__('Edit Domain')"
@click="handleEditDomainClick"
/>
</div>
</div>
<FormControl
v-else
v-model="state[field.name]"
:label="field.label"
:name="field.name"
:type="field.type"
:placeholder="field.placeholder"
/>
</div>
</div>
<div
v-if="isCustomSelected"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div class="flex flex-col gap-4">
<div
v-for="field in customIncomingFields"
:key="field.name"
class="flex flex-col gap-1"
>
<FormControl
v-model="customState[field.name]"
:label="field.label"
:name="field.name"
:type="field.type"
:placeholder="field.placeholder"
/>
</div>
</div>
<div class="flex flex-col gap-4">
<div
v-for="field in customOutgoingFields"
:key="field.name"
class="flex flex-col gap-1"
>
<FormControl
v-model="customState[field.name]"
:label="field.label"
:name="field.name"
:type="field.type"
:placeholder="field.placeholder"
/>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div
v-for="field in incomingOutgoingFields"
:key="field.name"
class="flex flex-col gap-1"
>
<FormControl
v-model="state[field.name]"
:label="field.label"
:name="field.name"
:type="field.type"
/>
<p class="text-ink-gray-4 text-p-sm">
{{ field.description }}
</p>
</div>
</div>
<ErrorMessage v-if="error" class="ms-1" :message="error" />
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div
v-for="field in incomingOutgoingFields"
:key="field.name"
class="flex flex-col gap-1"
>
<FormControl
v-model="state[field.name]"
:label="field.label"
:name="field.name"
:type="field.type"
/>
<p class="text-gray-500 text-p-sm">{{ field.description }}</p>
</div>
<!-- action button -->
<div class="mt-auto flex justify-between -mb-8">
<Button
:label="__('Back')"
theme="gray"
variant="outline"
:disabled="addEmailRes.loading"
@click="emit('update:step', 'email-list')"
/>
<Button
:label="__('Create')"
variant="solid"
:loading="addEmailRes.loading"
@click="createEmailAccount"
/>
</div>
<ErrorMessage v-if="error" class="ml-1" :message="error" />
</div>
</div>
<!-- action button -->
<div v-if="selectedService" class="mt-auto flex justify-between">
<Button
label="Back"
theme="gray"
variant="outline"
:disabled="addEmailRes.loading"
@click="emit('update:step', 'email-list')"
/>
<Button
label="Create"
variant="solid"
:loading="addEmailRes.loading"
@click="createEmailAccount"
/>
</div>
</div>
</template>
</SettingsLayoutBase>
</template>
<script setup lang="ts">
import { Link } from "@/components";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import { capture } from "@/telemetry";
import { __ } from "@/translation";
import { EmailAccount, EmailService, EmailStep } from "@/types";
import { createResource, toast } from "frappe-ui";
import { useOnboarding } from "frappe-ui/frappe";
import { computed, Reactive, reactive, Ref, ref } from "vue";
import { call, createResource, toast } from "frappe-ui";
import { useOnboarding } from "@framework/ui";
import { computed, reactive, Ref, ref, watch } from "vue";
import CircleAlert from "~icons/lucide/circle-alert";
import {
customProviderFields,
customIncomingFields,
customOutgoingFields,
customProviderTopFields,
frappeMailFields,
incomingOutgoingFields,
popularProviderFields,
services,
@@ -108,15 +182,49 @@ import {
} from "./emailConfig";
import EmailProviderIcon from "./EmailProviderIcon.vue";
interface EmailAccountBaseState {
email_account_name: string;
email_id: string;
service: string;
enable_incoming: boolean;
enable_outgoing: boolean;
default_incoming: boolean;
default_outgoing: boolean;
}
interface EmailAccountProviderAuthState extends EmailAccountBaseState {
password?: string;
api_key?: string;
api_secret?: string;
frappe_mail_site?: string;
}
type CustomEmailAccountState = {
domain?: string;
email_server?: string;
incoming_port?: string | number;
smtp_server?: string;
smtp_port?: string | number;
use_ssl?: boolean | number;
use_starttls?: boolean | number;
use_tls?: boolean | number;
use_ssl_for_outgoing?: boolean | number;
validate_ssl_certificate?: boolean | number;
validate_ssl_certificate_for_outgoing?: boolean | number;
attachment_limit?: string | number;
append_emails_to_sent_folder?: boolean | number;
sent_folder_name?: string;
};
interface E {
(event: "update:step", value: EmailStep): void;
}
const emit = defineEmits<E>();
const { updateOnboardingStep } = useOnboarding("helpdesk");
const { updateOnboardingStep } = useOnboarding("helpdesk") ?? {};
const state: Reactive<EmailAccount> = reactive({
const state = reactive<EmailAccountProviderAuthState>({
service: "",
email_account_name: "",
email_id: "",
@@ -130,39 +238,174 @@ const state: Reactive<EmailAccount> = reactive({
default_outgoing: false,
});
const getDefaultCustomState = (): CustomEmailAccountState => ({
domain: "",
email_server: "",
smtp_server: "",
incoming_port: "",
smtp_port: "",
use_ssl: false,
use_starttls: false,
use_tls: false,
use_ssl_for_outgoing: false,
validate_ssl_certificate: true,
validate_ssl_certificate_for_outgoing: true,
attachment_limit: "",
append_emails_to_sent_folder: false,
sent_folder_name: "",
});
const customState = reactive<CustomEmailAccountState>(getDefaultCustomState());
function resetCustomState() {
Object.assign(customState, getDefaultCustomState());
}
const selectedService: Ref<EmailService> = ref(null);
const fields = computed(() =>
selectedService.value.custom ? customProviderFields : popularProviderFields
const fields = computed(() => {
if (!selectedService.value) return [];
if (selectedService.value.name === "Frappe Mail") {
return frappeMailFields;
}
if (selectedService.value.custom) {
return customProviderTopFields;
}
return popularProviderFields;
});
const isCustomSelected = computed(
() => selectedService.value?.name === "Custom"
);
watch(
() => customState.domain,
async (val) => {
if (state.service !== "Custom") return;
if (!isCustomSelected.value || !val) return;
try {
const doc = await call("frappe.client.get", {
doctype: "Email Domain",
name: val,
});
syncAccountFieldsFromDomain(doc);
} catch (err) {
console.warn("Failed to load Email Domain", err);
}
}
);
function handleSelect(service: EmailService) {
selectedService.value = service;
state.service = service.name;
if (service.name !== "Custom") {
resetCustomState();
}
}
const addEmailRes = createResource({
url: "helpdesk.api.settings.email.create_email_account",
makeParams: (val: EmailAccount) => {
return {
...val,
};
},
makeParams: (val: EmailAccount) => ({ ...val }),
onSuccess: () => {
toast.success("Email account created");
toast.success(__("Email account created"));
emit("update:step", "email-list");
updateOnboardingStep("setup_email_account");
updateOnboardingStep?.("setup_email_account");
capture("email_account_created", { data: { service: state.service } });
},
onError: () => {
error.value = "Failed to create email account, Invalid credentials";
error.value = __("Failed to create email account, Invalid credentials");
},
});
const error = ref<string | undefined>();
function createEmailAccount() {
error.value = validateInputs(state, selectedService.value.custom);
const validationState = { ...state, ...customState };
error.value = validateInputs(validationState, state.service);
if (error.value) return;
addEmailRes.submit({ data: state });
addEmailRes.submit({ data: buildCreatePayload() });
}
function buildCreatePayload() {
const commonPayload = {
email_account_name: state.email_account_name,
email_id: state.email_id,
service: state.service,
enable_incoming: state.enable_incoming,
enable_outgoing: state.enable_outgoing,
default_incoming: state.default_incoming,
default_outgoing: state.default_outgoing,
};
if (state.service === "Frappe Mail") {
return {
...commonPayload,
frappe_mail_site: state.frappe_mail_site,
api_key: state.api_key,
api_secret: state.api_secret,
};
}
if (state.service === "Custom") {
return {
...commonPayload,
password: state.password,
...customState,
};
}
return {
...commonPayload,
password: state.password,
};
}
function syncAccountFieldsFromDomain(payload: Record<string, any>) {
customState.email_server = payload.email_server ?? customState.email_server;
customState.smtp_server = payload.smtp_server ?? customState.smtp_server;
customState.incoming_port =
payload.incoming_port ?? customState.incoming_port;
customState.smtp_port = payload.smtp_port ?? customState.smtp_port;
customState.use_ssl = Boolean(payload.use_ssl ?? customState.use_ssl);
customState.use_starttls = Boolean(
payload.use_starttls ?? customState.use_starttls
);
customState.use_tls = Boolean(payload.use_tls ?? customState.use_tls);
customState.use_ssl_for_outgoing = Boolean(
payload.use_ssl_for_outgoing ?? customState.use_ssl_for_outgoing
);
customState.validate_ssl_certificate = Boolean(
payload.validate_ssl_certificate ?? customState.validate_ssl_certificate
);
customState.validate_ssl_certificate_for_outgoing = Boolean(
payload.validate_ssl_certificate_for_outgoing ??
customState.validate_ssl_certificate_for_outgoing
);
customState.append_emails_to_sent_folder = Boolean(
payload.append_emails_to_sent_folder ??
customState.append_emails_to_sent_folder
);
customState.sent_folder_name =
payload.sent_folder_name || customState.sent_folder_name;
customState.attachment_limit =
payload.attachment_limit || customState.attachment_limit;
}
function handleCreateDomainClick(value: any, close?: () => void) {
close?.();
window.open(
"/desk/email-domain/new-email-domain",
"_blank",
"noopener,noreferrer"
);
}
function handleEditDomainClick() {
if (!customState.domain) return;
window.open(
`/desk/email-domain/${encodeURIComponent(customState.domain)}`,
"_blank",
"noopener,noreferrer"
);
}
</script>

View File

@@ -1,14 +1,12 @@
<template>
<div class="flex-1 px-10 py-8">
<div v-if="step === 'email-add'" class="h-full">
<EmailAdd @update:step="updateStep" />
</div>
<div v-else-if="step === 'email-list'" class="h-full">
<EmailAccountList @update:step="updateStep" />
</div>
<div v-else-if="step === 'email-edit'" class="h-full">
<EmailEdit :account-data="accountData" @update:step="updateStep" />
</div>
<div v-if="step === 'email-add'" class="h-full">
<EmailAdd @update:step="updateStep" />
</div>
<div v-else-if="step === 'email-list'" class="h-full">
<EmailAccountList @update:step="updateStep" />
</div>
<div v-else-if="step === 'email-edit'" class="h-full">
<EmailEdit :account-data="accountData" @update:step="updateStep" />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More