diff --git a/clefincode_chat/__init__.py b/clefincode_chat/__init__.py index 6bb6ad1..435191f 100644 --- a/clefincode_chat/__init__.py +++ b/clefincode_chat/__init__.py @@ -1 +1 @@ -__version__ = '1.3.910' \ No newline at end of file +__version__ = '1.3.911' \ No newline at end of file diff --git a/clefincode_chat/api/api_1_3_4/api.py b/clefincode_chat/api/api_1_3_4/api.py index d2a8550..b1fb651 100644 --- a/clefincode_chat/api/api_1_3_4/api.py +++ b/clefincode_chat/api/api_1_3_4/api.py @@ -10889,18 +10889,32 @@ def _normalize_reference_row(row): def _normalize_topic_doc(topic_doc): refs = [] - for row in (topic_doc.references or []): - parsed = _normalize_reference_row(row) - if parsed: - refs.append(parsed) - if not topic_doc.topic_color: - topic_doc.topic_color = get_random_topic_color() - topic_doc.save(ignore_permissions=True) + try: + for row in (topic_doc.references or []): + parsed = _normalize_reference_row(row) + if parsed: + refs.append(parsed) + except Exception: + frappe.log_error( + title="Failed to normalize topic references", + message=frappe.get_traceback() + ) + refs = [] + + try: + if not topic_doc.topic_color: + topic_doc.topic_color = get_random_topic_color() + topic_doc.save(ignore_permissions=True) + except Exception: + frappe.log_error( + title=f"Failed to set topic color for {topic_doc.name}", + message=frappe.get_traceback() + ) return { "name": topic_doc.name, - "date":str(topic_doc.creation), + "date": str(topic_doc.creation), "subject": topic_doc.subject, "chat_channel": topic_doc.chat_channel, "topic_status": topic_doc.topic_status, @@ -10949,30 +10963,91 @@ def remove_message_topic(message_name): } @frappe.whitelist() -def get_channel_topics(chat_channel, topic_status=None): - +def get_channel_topics(chat_channel, topic_status=None, limit=10, offset=0, query=None): if not chat_channel: frappe.throw(_("chat_channel is required")) - filters = {"chat_channel": chat_channel} + limit = cint(limit) if limit is not None else 10 + offset = cint(offset) if offset is not None else 0 + + if limit <= 0: + limit = 10 + + if offset < 0: + offset = 0 + + filters = { + "chat_channel": chat_channel + } if topic_status and topic_status != "All": filters["topic_status"] = topic_status + or_filters = [] + + if query: + query = query.strip() + + if query: + reference_rows = frappe.get_all( + "ClefinCode Chat Topic Reference", + filters={ + "parenttype": "ClefinCode Chat Topic", + }, + or_filters=[ + ["ClefinCode Chat Topic Reference", "doctype_link", "like", f"%{query}%"], + ["ClefinCode Chat Topic Reference", "docname", "like", f"%{query}%"], + ], + fields=["parent"], + distinct=True, + ) + + reference_topic_names = [row.parent for row in reference_rows if row.parent] + + or_filters = [ + + ["ClefinCode Chat Topic", "subject", "like", f"%{query}%"], + ] + + if reference_topic_names: + or_filters.append( + ["ClefinCode Chat Topic", "name", "in", reference_topic_names] + ) + + count_rows = frappe.get_all( + "ClefinCode Chat Topic", + filters=filters, + or_filters=or_filters, + fields=["count(name) as count"], + ) + + total_count = count_rows[0].count if count_rows else 0 + topic_names = frappe.get_all( "ClefinCode Chat Topic", filters=filters, + or_filters=or_filters, fields=["name"], order_by="modified desc", + limit_start=offset, + limit_page_length=limit, ) topics = [] + for row in topic_names: doc = frappe.get_doc("ClefinCode Chat Topic", row.name) topics.append(_normalize_topic_doc(doc)) + next_offset = offset + len(topics) + return { "chat_channel": chat_channel, "count": len(topics), + "total_count": total_count, + "limit": limit, + "offset": offset, + "next_offset": next_offset, + "has_more": next_offset < total_count, "topics": topics, - } + } \ No newline at end of file diff --git a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.js b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.js index c519af8..a78e2e6 100644 --- a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.js +++ b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.js @@ -1,15 +1,15 @@ // Copyright (c) 2024, ClefinCode L.L.C-FZ and contributors // For license information, please see license.txt -frappe.ui.form.on('ClefinCode WhatsApp Profile', { - refresh: function(frm){ - frm.fields_dict['message_template'].get_query = function(doc) { - return { - filters:[{ - "whatsapp_profile": doc.name - } - ] - } - } - } -}); +// frappe.ui.form.on('ClefinCode WhatsApp Profile', { +// refresh: function(frm){ +// frm.fields_dict['message_template'].get_query = function(doc) { +// return { +// filters:[{ +// "whatsapp_profile": doc.name +// } +// ] +// } +// } +// } +// }); diff --git a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.py b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.py index 39a7d8f..f4c1bfd 100644 --- a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.py +++ b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_profile/clefincode_whatsapp_profile.py @@ -3,56 +3,104 @@ import frappe from frappe.model.document import Document +from frappe import _ +from frappe.utils import get_url_to_form +from bs4 import BeautifulSoup class ClefinCodeWhatsAppProfile(Document): - def before_validate(self): - if self.type == "Personal": - get_whatsapp_numbers_for_profile(self.name , self.user) - else: - for user in self.authorized_users: - get_whatsapp_numbers_for_profile(self.name, user.user) - - def after_insert(self): - self.create_whatsapp_template() - - def create_whatsapp_template(self): - doc = frappe.get_doc({ - "doctype" : "ClefinCode WhatsApp Template", - "whatsapp_profile": self.name, - "template_name": f"{self.business_account_id}_confirm_message", - "meta_template_name": self.meta_template_name, - "category": "Utility", - "body": self.template_content, - "buttons": [{"type" : "Custom" , "button_text" : "Yes" }, {"type" : "Custom" , "button_text" : "No" } ], - }).insert(ignore_permissions = True) - doc.submit() - self.message_template = doc.name - self.save() + def before_validate(self): + if self.type == "Personal": + get_whatsapp_numbers_for_profile(self.name , self.user) + else: + for user in self.authorized_users: + get_whatsapp_numbers_for_profile(self.name, user.user) + + def after_insert(self): + self.create_whatsapp_template() + + def create_whatsapp_template(self): + template_language = "en" + + temp_doc = frappe.new_doc("ClefinCode WhatsApp Template") + + temp_doc.template_language = template_language + temp_doc.body = self.template_content + temp_doc.buttons = [ + {"type": "Custom", "button_text": "Yes"}, + {"type": "Custom", "button_text": "No"}, + ] + + existing_template = temp_doc.find_existing_same_content_language() + + if existing_template: + + self.message_template = existing_template.name + self.db_set("message_template", existing_template.name) + + url = get_url_to_form( + "ClefinCode WhatsApp Template", + existing_template.name + ) + + frappe.msgprint( + msg=_( + "An existing WhatsApp template was found: {0}.

" + "This template will be used for this profile instead of creating a new one." + ).format( + f"{existing_template.meta_template_name or existing_template.name}" + ), + title=_("Existing WhatsApp Template Found"), + indicator="orange" + ) + + return existing_template + + + doc = frappe.get_doc({ + "doctype": "ClefinCode WhatsApp Template", + "whatsapp_profile": self.name, + "template_name": f"{self.business_account_id}_confirm_message", + "meta_template_name": self.meta_template_name, + "category": "Utility", + "template_language": template_language, + "body": self.template_content, + "buttons": [ + {"type": "Custom", "button_text": "Yes"}, + {"type": "Custom", "button_text": "No"}, + ], + }).insert(ignore_permissions=True) + + doc.submit() + + self.message_template = doc.name + self.db_set("message_template", doc.name) + + return doc def get_whatsapp_numbers_for_profile(doc_name, user): - whatsapp_numbers_list = [] - - personal_numbers = frappe.db.sql(f""" - SELECT name AS number - FROM `tabClefinCode WhatsApp Profile` - WHERE type = 'Personal' AND user = '{user}' AND name <> '{doc_name}' - """, as_dict=True) - - if personal_numbers: - for n in personal_numbers: - whatsapp_numbers_list.append(n['number']) - - support_numbers = frappe.db.sql(f""" - SELECT DISTINCT wp.name AS number - FROM `tabClefinCode WhatsApp Profile` AS wp - INNER JOIN `tabAuthorized Users` users ON users.parent = wp.name - WHERE type = 'Support' AND users.user = '{user}' AND wp.name <> '{doc_name}' - """, as_dict=True) - - if support_numbers: - for n in support_numbers: - whatsapp_numbers_list.append(n['number']) - - if whatsapp_numbers_list and len(whatsapp_numbers_list) > 0: - frappe.throw(f"The user {user} already has WhatsApp number {frappe.utils.get_link_to_form('ClefinCode WhatsApp Profile' , whatsapp_numbers_list[0])} ") + whatsapp_numbers_list = [] + + personal_numbers = frappe.db.sql(f""" + SELECT name AS number + FROM `tabClefinCode WhatsApp Profile` + WHERE type = 'Personal' AND user = '{user}' AND name <> '{doc_name}' + """, as_dict=True) + + if personal_numbers: + for n in personal_numbers: + whatsapp_numbers_list.append(n['number']) + + support_numbers = frappe.db.sql(f""" + SELECT DISTINCT wp.name AS number + FROM `tabClefinCode WhatsApp Profile` AS wp + INNER JOIN `tabAuthorized Users` users ON users.parent = wp.name + WHERE type = 'Support' AND users.user = '{user}' AND wp.name <> '{doc_name}' + """, as_dict=True) + + if support_numbers: + for n in support_numbers: + whatsapp_numbers_list.append(n['number']) + + if whatsapp_numbers_list and len(whatsapp_numbers_list) > 0: + frappe.throw(f"The user {user} already has WhatsApp number {frappe.utils.get_link_to_form('ClefinCode WhatsApp Profile' , whatsapp_numbers_list[0])} ") diff --git a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.json b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.json index ae2e996..879f4f2 100644 --- a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.json +++ b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.json @@ -167,7 +167,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-12-11 11:22:21.293278", + "modified": "2026-07-05 12:34:22.780581", "modified_by": "Administrator", "module": "ClefinCode Chat", "name": "ClefinCode WhatsApp Template", diff --git a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.py b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.py index cdac2ff..2f7daf3 100644 --- a/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.py +++ b/clefincode_chat/clefincode_chat/doctype/clefincode_whatsapp_template/clefincode_whatsapp_template.py @@ -1,4 +1,6 @@ import frappe +from frappe.utils import get_url_to_form +from frappe import _ from frappe.model.document import Document from bs4 import BeautifulSoup @@ -8,6 +10,7 @@ from clefincode_chat.utils.utils import get_access_token, get_auth_token_twillio + def _sanitize_name(name: str) -> str: """make lowercase, replace invalid chars with underscores""" if not name: @@ -17,6 +20,28 @@ def _sanitize_name(name: str) -> str: return safe class ClefinCodeWhatsAppTemplate(Document): + def validate(self): + existing_template = self.find_existing_same_content_language() + + + + if existing_template: + url = get_url_to_form( + "ClefinCode WhatsApp Template", + existing_template.name + ) + frappe.throw( + msg = _( + "An existing WhatsApp template with the same content, language, and buttons already exists: {0}.

" + "Please use the existing template instead.

" + "If you want to create a new template, please change the message content or buttons." + ).format( + f""" + {existing_template.meta_template_name or existing_template.name} + """ + ), + title=_("Existing WhatsApp Template Found") + ) def before_insert(self): if self.whatsapp_business_account_id: self.template_name = f"{self.whatsapp_business_account_id}_{self.meta_template_name}" @@ -46,55 +71,115 @@ def on_submit(self): frappe.throw(str(e)) def post_whatsapp_template_meta(self): - try: - access_token = get_access_token() - api_base = "https://graph.facebook.com/v23.0" - endpoint = f"{api_base}/{self.whatsapp_business_account_id}/message_templates" + try: + existing_meta_template = self.find_existing_template_in_meta() - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - } - - data = { - "name": self.meta_template_name, - "category": self.category, - "language": self.template_language, - "components": [ - { - "type": "BODY", - "text": BeautifulSoup(self.body, 'html.parser').get_text() - }, - { - "type": "BUTTONS", - "buttons": [ - { - "type": "QUICK_REPLY", - "text": "Yes" - }, - { - "type": "QUICK_REPLY", - "text": "No" - } - ] - } - ] - } + if existing_meta_template: + self.whatsapp_template_id = existing_meta_template.get("id") + self.template_status = existing_meta_template.get("status") + self.save(ignore_permissions=True) + frappe.db.commit() + + frappe.msgprint( + msg=_( + "An existing WhatsApp template was found in Meta with the same content, language, and buttons. " + "It has been linked to this ClefinCode template instead of creating a new one." + ), + title=_("Existing Meta Template Found"), + indicator="orange" + ) + + return + + access_token = get_access_token() + api_base = "https://graph.facebook.com/v23.0" + endpoint = f"{api_base}/{self.whatsapp_business_account_id}/message_templates" + + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + data = { + "name": self.meta_template_name, + "category": self.category, + "language": self.template_language, + "components": [ + { + "type": "BODY", + "text": BeautifulSoup(self.body or "", "html.parser").get_text() + }, + { + "type": "BUTTONS", + "buttons": [ + { + "type": "QUICK_REPLY", + "text": "Yes" + }, + { + "type": "QUICK_REPLY", + "text": "No" + } + ] + } + ] + } + + response = requests.post(endpoint, json=data, headers=headers) + + if response.ok: + response_data = response.json() + + self.whatsapp_template_id = response_data.get("id") + self.template_status = response_data.get("status") + self.save(ignore_permissions=True) + frappe.db.commit() + + frappe.msgprint( + msg=_( + "WhatsApp message template {0} has been created to proceed communication with the customer outside of the 24-hour window." + ).format( + f"{self.meta_template_name}" + ), + title=_("WhatsApp Template Created"), + indicator="green" + ) + + return + + try: + error_data = response.json() + except Exception: + error_data = {} + + error = error_data.get("error", {}) + error_subcode = error.get("error_subcode") + + if error_subcode == 2388024: + existing_meta_template = self.find_existing_template_in_meta() + + if existing_meta_template: + self.whatsapp_template_id = existing_meta_template.get("id") + self.template_status = existing_meta_template.get("status") + self.save(ignore_permissions=True) + frappe.db.commit() + + frappe.msgprint( + msg=_( + "Meta reported that a template with the same content and language already exists. " + "The existing Meta template has been linked instead of creating a duplicate." + ), + title=_("Existing Meta Template Linked"), + indicator="orange" + ) + + return - response = requests.post(endpoint, json=data, headers=headers) - if response.ok: - frappe.msgprint(f"WhatsApp message template {self.meta_template_name} has been created to proceed communication with the customer outside of the 24-hour window") - response_data = response.json() - self.whatsapp_template_id = response_data.get("id") - self.template_status = response_data.get("status") - self.save() - frappe.db.commit() - else: frappe.throw(response.text) + + except Exception as e: + frappe.throw(str(e)) - except Exception as e: - frappe.throw(str(e)) - def post_whatsapp_template_twilio(self, profile): """ Create Content template in Twilio and submit approval for WhatsApp. @@ -205,4 +290,114 @@ def post_whatsapp_template_twilio(self, profile): except Exception as e: frappe.log_error(f"post_whatsapp_template_twilio exception: {str(e)}", "post_whatsapp_template_twilio") - frappe.throw(str(e)) \ No newline at end of file + frappe.throw(str(e)) + def find_existing_same_content_language(self): + if not self.body or not self.template_language: + return None + + existing_templates = frappe.get_all( + "ClefinCode WhatsApp Template", + filters={ + "template_language": self.template_language, + "docstatus": ["!=", 2], + "name": ["!=", self.name], + }, + fields=["name", "meta_template_name", "template_name", "body"] + ) + + current_body = BeautifulSoup( + self.body or "", + "html.parser" + ).get_text(" ").strip().lower() + + current_buttons = [] + for button in self.buttons or []: + current_buttons.append({ + "type": (button.get("type") or "").strip().lower(), + "button_text": (button.get("button_text") or "").strip().lower(), + }) + + for template in existing_templates: + existing_doc = frappe.get_doc( + "ClefinCode WhatsApp Template", + template.name + ) + + existing_body = BeautifulSoup( + existing_doc.body or "", + "html.parser" + ).get_text(" ").strip().lower() + + existing_buttons = [] + for button in existing_doc.buttons or []: + existing_buttons.append({ + "type": (button.get("type") or "").strip().lower(), + "button_text": (button.get("button_text") or "").strip().lower(), + }) + + + if existing_body == current_body and existing_buttons == current_buttons: + return existing_doc + + return None + def find_existing_template_in_meta(self): + try: + access_token = get_access_token() + api_base = "https://graph.facebook.com/v23.0" + endpoint = f"{api_base}/{self.whatsapp_business_account_id}/message_templates" + + headers = { + "Authorization": f"Bearer {access_token}", + } + + params = { + "fields": "id,name,status,language,category,components", + "limit": 100 + } + + response = requests.get(endpoint, headers=headers, params=params) + + if not response.ok: + frappe.log_error(response.text, "meta_template_lookup_error") + return None + + templates = response.json().get("data", []) + + current_body = BeautifulSoup( + self.body or "", + "html.parser" + ).get_text(" ").strip().lower() + + current_buttons = [] + for button in self.buttons or []: + current_buttons.append( + (button.get("button_text") or "").strip().lower() + ) + + for template in templates: + if template.get("language") != self.template_language: + continue + + existing_body = "" + existing_buttons = [] + + for component in template.get("components", []): + if component.get("type") == "BODY": + existing_body = ( + component.get("text") or "" + ).strip().lower() + + if component.get("type") == "BUTTONS": + for btn in component.get("buttons", []): + existing_buttons.append( + (btn.get("text") or "").strip().lower() + ) + + if existing_body == current_body and existing_buttons == current_buttons: + return template + + return None + + except Exception as e: + frappe.log_error(str(e), "find_existing_template_in_meta") + return None \ No newline at end of file diff --git a/clefincode_chat/public/js/components/erpnext_chat_space.js b/clefincode_chat/public/js/components/erpnext_chat_space.js index ed1b7eb..e9e596b 100644 --- a/clefincode_chat/public/js/components/erpnext_chat_space.js +++ b/clefincode_chat/public/js/components/erpnext_chat_space.js @@ -1532,84 +1532,57 @@ async openMessageInfoDialog(messageName) { const linkedTopic = await this.getLinkedTopicInfo(cached); const linkedTopicReferences = linkedTopic?.references || []; -const linkedTopicReferencesHtml = linkedTopicReferences.length - ? ` -
-
- ${__("References")} -
- - ${linkedTopicReferences.map(ref => { - const doctype = ref.doctype || ref.reference_doctype || ""; - const docname = ref.docname || ref.reference_docname || ""; - - if (!doctype || !docname) return ""; + const linkedTopicReferencesHtml = renderReferenceLinksHtml(linkedTopicReferences, { + wrapperClass: "message-info-references", + linkClass: "message-info-doctype-link", + title: __("References") + }); + + const linkedTopicName = linkedTopic?.name || ""; + const linkedTopicSubject = linkedTopic?.subject || linkedTopicName || "—"; + const linkedTopicColor = + linkedTopic?.topic_color || + linkedTopic?.color || + this.topicColorMap?.get(linkedTopicName) || + ""; - return ` - - ${frappe.utils.escape_html(doctype)} / ${frappe.utils.escape_html(docname)} - - `; - }).join("")} -
- ` - : ""; -const linkedTopicName = linkedTopic?.name || ""; -const linkedTopicSubject = linkedTopic?.subject || linkedTopicName || "—"; -const linkedTopicColor = - linkedTopic?.topic_color || - linkedTopic?.color || - this.topicColorMap?.get(linkedTopicName) || - ""; - -const safeLinkedTopicName = frappe.utils.escape_html(linkedTopicName); -const safeLinkedTopicSubject = frappe.utils.escape_html(linkedTopicSubject); -const safeLinkedTopicColor = frappe.utils.escape_html(linkedTopicColor || ""); -const linkedTopicHtml = linkedTopic - ? ` -
-
${__("Linked Topic")}
- - ${safeLinkedTopicSubject} - - ${ - linkedTopic.name && linkedTopic.subject && linkedTopic.name !== linkedTopic.subject - ? `
- ${frappe.utils.escape_html(linkedTopic.name)} -
` - : `` - } - ${ - linkedTopic.status - ? `
- ${__("Status")}: ${frappe.utils.escape_html(linkedTopic.status)} - ${linkedTopic.is_private ? " • " + __("Private") : ""} -
` - : `` -} + const safeLinkedTopicName = frappe.utils.escape_html(linkedTopicName); + const safeLinkedTopicSubject = frappe.utils.escape_html(linkedTopicSubject); + const safeLinkedTopicColor = frappe.utils.escape_html(linkedTopicColor || ""); + const linkedTopicHtml = linkedTopic + ? ` +
+
${__("Linked Topic")}
+ + ${safeLinkedTopicSubject} + + ${ + linkedTopic.name && linkedTopic.subject && linkedTopic.name !== linkedTopic.subject + ? `
+ ${frappe.utils.escape_html(linkedTopic.name)} +
` + : `` + } + ${ + linkedTopic.status + ? `
+ ${__("Status")}: ${frappe.utils.escape_html(linkedTopic.status)} + ${linkedTopic.is_private ? " • " + __("Private") : ""} +
` + : `` + } ${linkedTopicReferencesHtml}
` @@ -1725,26 +1698,10 @@ ${linkedTopicReferencesHtml} topic_color: topicColor }); }); - d.$wrapper.off("click", ".message-info-doctype-link") - .on("click", ".message-info-doctype-link", function(e) { - e.preventDefault(); - e.stopPropagation(); - - const doctype = $(this).attr("data-doctype"); - const docname = $(this).attr("data-docname"); - - if (doctype && docname) { - d.hide(); - - - - const url = getFormUrl(doctype, docname); - console.log("Dsdsdsds"); - - window.open(url, "_blank", "noopener,noreferrer"); - - } - }); + bindReferenceLinks(d.$wrapper, ".message-info-doctype-link", { + dialog: d, + hideDialog: true + }); } openEmojiMenu({ $bubble, messageName }) { this.closeEmojiMenu(); @@ -2704,7 +2661,7 @@ async fetch_single_message(messageName) { ); await this.setup_messages(res.results || []); await this.setup_actions(); - await this.applySavedActiveTopic(); + // await this.applySavedActiveTopic(); this.render(); this.checkAndShowTopicInactiveNotice?.(); } catch (error) { @@ -4964,7 +4921,7 @@ async setup_messages(messages_list) { } getOutgoingTopicInfo() { - const isTopicWindow = this.is_topic_window || this.profile.room_type === "Topic"; + const isTopicWindow = this.is_topic_window ; const topicName = isTopicWindow ? this.chat_topic_space || this.profile.chat_topic || this.chat_topic @@ -7824,7 +7781,7 @@ if (!is_deleted && type !== "info-message") { is_document: this.is_document, is_voice_clip: this.is_voice_clip, file_id: file_id, - chat_topic: topicReferenceTarget ? topicReferenceTarget.name : this.chat_topic, + chat_topic: topicReferenceTarget ? topicReferenceTarget.name : null, chat_topic_subject: topicReferenceTarget ? topicReferenceTarget.subject : null, topic_color: topicReferenceTarget ? topicReferenceTarget.color : null, }; @@ -7953,118 +7910,118 @@ if (!is_deleted && type !== "info-message") { }); } // ========================================================================= - else if (mention_doctypes.length > 0) { - const topicReferenceTarget = this.getTopicReferenceTargetForSend(); + else if (mention_doctypes.length > 0) { + const isTopicContext = this.isDedicatedTopicContext?.(); + + const topicReferenceTarget = isTopicContext + ? this.getTopicReferenceTargetForSend() + : null; + + let message_info = { + content: + content && content.length == 1 + ? content.prop("outerHTML") + : content, + user: this.profile.user, + room: chat_room, + email: this.profile.user_email, + is_first_message: this.is_first_message, + attachment: attachment, + sub_channel: + this.last_active_sub_channel == chat_room + ? "" + : this.last_active_sub_channel, + is_link: this.is_link, + is_media: this.is_media, + is_document: this.is_document, + is_voice_clip: this.is_voice_clip, + file_id: file_id, - let message_info = { - content: - content && content.length == 1 - ? content.prop("outerHTML") - : content, - user: this.profile.user, - room: chat_room, - email: this.profile.user_email, - is_first_message: this.is_first_message, - attachment: attachment, - sub_channel: - this.last_active_sub_channel == chat_room - ? "" - : this.last_active_sub_channel, - is_link: this.is_link, - is_media: this.is_media, - is_document: this.is_document, - is_voice_clip: this.is_voice_clip, - file_id: file_id, + + chat_topic: topicReferenceTarget ? topicReferenceTarget.name : null, + chat_topic_subject: topicReferenceTarget ? topicReferenceTarget.subject : null, + topic_color: topicReferenceTarget ? topicReferenceTarget.color : null, + }; - chat_topic: topicReferenceTarget ? topicReferenceTarget.name : null, - chat_topic_subject: topicReferenceTarget ? topicReferenceTarget.subject : null, - topic_color: topicReferenceTarget ? topicReferenceTarget.color : null, - }; + + if (topicReferenceTarget) { + this.showMentionWillBeAddedToTopicMessage( + topicReferenceTarget, + mention_doctypes + ); - if (topicReferenceTarget) { - this.showMentionWillBeAddedToTopicMessage( - topicReferenceTarget, - mention_doctypes - ); + this.last_chat_space_message = await send_message(message_info); - this.last_chat_space_message = await send_message(message_info); + await add_reference_doctype( + mention_doctypes, + topicReferenceTarget.name, + this.last_active_sub_channel + ); - await add_reference_doctype( - mention_doctypes, - topicReferenceTarget.name, - this.last_active_sub_channel - ); + this.mergeTopicReferencesLocally( + topicReferenceTarget.name, + mention_doctypes + ); - this.mergeTopicReferencesLocally( - topicReferenceTarget.name, - mention_doctypes - ); + this.clearTopicMentionReferenceHint?.(); + this.chat_info?.refreshTopicReferencesSection?.(); - this.clearTopicMentionReferenceHint?.(); - this.chat_info?.refreshTopicReferencesSection?.(); + await this.send_add_document_message(mention_doctypes, chat_room); + const topicToClear = this.activeMessageTopic || this.chat_topic || createdTopicName || null; - await this.send_add_document_message(mention_doctypes, chat_room); + this.clearMessageTopic(false); + await this.clearUserActiveChatTopic(topicToClear); - return; - } + if (!this.isDedicatedTopicContext?.()) { + this.chat_topic = null; + this.chat_topic_subject = null; + this.updateActiveTopicButton?.(null); + this.updatePlusTopicButton?.(); + } - // No selected/current topic: keep old behavior and create a new topic. - let results = await create_chat_topic( - mention_doctypes, - chat_room, - this.last_active_sub_channel - ); + return; + } - const createdTopicName = results[0].chat_topic; - const createdTopicColor = results[0].topic_color || null; + + let results = await create_chat_topic( + mention_doctypes, + chat_room, + this.last_active_sub_channel + ); - if (createdTopicName) { - this.topicColorMap.set( - createdTopicName, - createdTopicColor || this.getTopicColor(createdTopicName) - ); + const createdTopicName = results?.[0]?.chat_topic || results?.[0]?.name || null; + const createdTopicSubject = mention_doctypes?.[0]?.docname || createdTopicName; + const createdTopicColor = results?.[0]?.topic_color || null; - this.activeMessageTopic = createdTopicName; - this.activeMessageTopicSubject = - mention_doctypes[0].docname || createdTopicName; - this.activeMessageTopicColor = - createdTopicColor || this.getTopicColor(createdTopicName); + this.activeMessageTopic = null; + this.activeMessageTopicSubject = null; + this.activeMessageTopicColor = null; - this.updatePlusTopicButton?.(); - this.saveUserActiveChatTopic(createdTopicName); - } + this.clearMessageTopic(false); + this.updatePlusTopicButton?.(); + await this.clearUserActiveChatTopic?.(createdTopicName); - message_info.chat_topic = createdTopicName; - message_info.chat_topic_subject = - mention_doctypes[0].docname || createdTopicName; - message_info.topic_color = createdTopicColor; - this.last_chat_space_message = await send_message(message_info); + message_info.chat_topic = null; + message_info.chat_topic_subject = null; + message_info.topic_color = null; - if (this.last_chat_space_message && createdTopicName) { - setTimeout(() => { - this.applyTopicToRenderedMessage( - this.last_chat_space_message, - createdTopicName, - this.activeMessageTopicSubject, - createdTopicColor - ); - }, 150); - } + this.last_chat_space_message = await send_message(message_info); - await this.send_set_topic_message( - mention_doctypes[0].docname, - chat_room - ); - if (createdTopicName) { - await this.openTopicChatWindow(createdTopicName, this.activeMessageTopicSubject, { - topic_color: createdTopicColor - }); - } + await this.send_set_topic_message( + createdTopicSubject, + chat_room + ); - return; - } + if (createdTopicName) { + await this.openTopicChatWindow(createdTopicName, createdTopicSubject, { + topic_color: createdTopicColor + }); + } + + return; +} } // ================= End Handling with Mentions =========================== @@ -8089,7 +8046,7 @@ if (!is_deleted && type !== "info-message") { is_document: this.is_document, is_voice_clip: this.is_voice_clip, file_id: file_id, - chat_topic: finalOutgoingTopic ? finalOutgoingTopic.chat_topic : (this.chat_topic || messageChatTopic), + chat_topic: finalOutgoingTopic ? finalOutgoingTopic.chat_topic :null,// (this.chat_topic || messageChatTopic), chat_topic_subject: finalOutgoingTopic ? finalOutgoingTopic.chat_topic_subject : null, topic_color: finalOutgoingTopic ? finalOutgoingTopic.topic_color : null, is_screenshot: is_screenshot, @@ -10571,8 +10528,121 @@ function show_doctype_selector(doctype, callback) { return `${getDeskBasePath()}/${routeDoctype}/${encodeURIComponent(docname)}`; } +function normalizeTopicReferences(refsOrTopic = {}) { + const refs = + Array.isArray(refsOrTopic) + ? refsOrTopic + : refsOrTopic.reference_doctypes || + refsOrTopic.references || + refsOrTopic.mention_doctypes || + []; + + if (typeof refs === "string") { + try { + return normalizeTopicReferences(JSON.parse(refs)); + } catch (e) { + console.warn("Failed to parse topic references", e); + return []; + } + } + + if (!Array.isArray(refs)) return []; + + return refs + .map(ref => ({ + doctype: ref.doctype || ref.reference_doctype || "", + docname: + ref.docname || + ref.reference_docname || + ref.reference_name || + "" + })) + .filter(ref => ref.doctype && ref.docname); +} + +function renderReferenceLinksHtml(refsOrTopic = {}, options = {}) { + const references = normalizeTopicReferences(refsOrTopic); + + if (!references.length) return ""; + + const wrapperClass = options.wrapperClass || "topic-reference-links"; + const linkClass = options.linkClass || "topic-reference-link"; + const title = options.title || __("References"); + + return ` +
+
+ ${title} +
-window.CCOpenChannelTopicPicker = async function ({ chatChannel, topicStatus = "All", title = __("Select Topic"), selectLabel = __("Select"), showAddNew = false, addNewHandler = null, getTopicColor = null } = {}) { + ${references.map(ref => { + const url = getFormUrl(ref.doctype, ref.docname); + + return ` + + ${frappe.utils.escape_html(ref.doctype)} / ${frappe.utils.escape_html(ref.docname)} + + `; + }).join("")} +
+ `; +} +function bindReferenceLinks($wrapper, selector = ".topic-reference-link", opts = {}) { + $wrapper + .off("click.referenceLinks", selector) + .on("click.referenceLinks", selector, function (e) { + // Let browser handle open in new tab/window: + // middle click, Ctrl/Cmd click, Shift click, Alt click + if ( + e.which === 2 || + e.ctrlKey || + e.metaKey || + e.shiftKey || + e.altKey + ) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + const url = $(this).attr("href"); + + if (!url || url === "#") return; + + if (opts.dialog && opts.hideDialog) { + opts.dialog.hide(); + } + + window.open(url, "_blank", "noopener,noreferrer"); + }); +} + + + + +window.CCOpenChannelTopicPicker = async function ({ + chatChannel, + topicStatus = "All", + title = __("Select Topic"), + selectLabel = __("Select"), + showAddNew = false, + addNewHandler = null, + getTopicColor = null +} = {}) { if (!chatChannel) { frappe.msgprint({ title: __("Error"), @@ -10585,12 +10655,23 @@ window.CCOpenChannelTopicPicker = async function ({ chatChannel, topicStatus = " return new Promise((resolve) => { let settled = false; + const state = { + limit: 10, + offset: 0, + hasMore: true, + loading: false, + query: "", + topicStatus: topicStatus || "All" + }; + const d = new frappe.ui.Dialog({ title, - size: "large", + size: "small", fields: [{ fieldtype: "HTML", fieldname: "topics_html" }], primary_action_label: __("Close"), - primary_action() { d.hide(); } + primary_action() { + d.hide(); + } }); d.$wrapper.on("hidden.bs.modal", () => { @@ -10600,107 +10681,298 @@ window.CCOpenChannelTopicPicker = async function ({ chatChannel, topicStatus = " }); d.show(); + - const renderTopics = async () => { - d.fields_dict.topics_html.$wrapper.html(`
${__("Loading topics...")}
`); + const makeTopicRowHtml = (topic) => { + const topicName = topic.name || topic.chat_topic || ""; + const subject = (topic.subject || topic.chat_topic_subject || "").trim(); + const displayTitle = subject || topicName; + + const safeDisplayTitle = frappe.utils.escape_html(displayTitle || ""); + const safeTopicName = frappe.utils.escape_html(topicName || ""); + + const topicColor = + typeof getTopicColor === "function" + ? getTopicColor(topic) + : topic.topic_color || topic.color || null; + + const rawDate = + topic.creation || + topic.date || + topic.created_on || + topic.creation_date || + topic.created_date || + null; + + let createdAt = "—"; + + if (rawDate) { + try { + const tz = + frappe.boot?.time_zone?.system || + frappe.boot?.time_zone?.user || + "UTC"; + + createdAt = + get_date_from_now(rawDate, "space", tz) + + " " + + get_time(rawDate, tz); + } catch (e) { + createdAt = String(rawDate); + } + } + + const topicNameHtml = + subject && topicName && subject !== topicName + ? `
${safeTopicName}
` + : ""; + + const refsHtml = renderReferenceLinksHtml(topic, { + wrapperClass: "topic-picker-references", + linkClass: "topic-picker-reference-link", + title: __("References") + }); + + const colorDotHtml = topicColor + ? `` + : ""; + + return ` +
+
+
${colorDotHtml}${safeDisplayTitle}
+ + ${topicNameHtml} + +
+ ${__("Status")}: ${frappe.utils.escape_html(topic.topic_status || "Open")} + ${topic.is_private ? " • " + __("Private") : ""} +
+ +
+ ${__("Created At")}: ${frappe.utils.escape_html(createdAt)} +
+ + ${ + refsHtml || + `
+ ${__("No references")} +
` + } +
+ + +
+ `; + }; + + const renderShell = () => { + d.fields_dict.topics_html.$wrapper.html(` +
+
+ + + + + ${ + showAddNew + ? `` + : "" + } +
+ +
+
+ + + + +
+ `); + }; + + const resetState = () => { + state.offset = 0; + state.hasMore = true; + state.loading = false; + + d.$wrapper.find(".topic-picker-list").empty(); + d.$wrapper.find(".topic-picker-empty").hide(); + d.$wrapper.find(".topic-picker-loading").hide(); + }; + + const renderTopics = async ({ reset = false } = {}) => { + if (state.loading) return; + if (!state.hasMore && !reset) return; + + if (reset) { + resetState(); + } + + state.loading = true; + d.$wrapper.find(".topic-picker-loading").show(); try { const r = await frappe.call({ method: "clefincode_chat.api.api_1_3_4.api.get_channel_topics", - args: { chat_channel: chatChannel, topic_status: topicStatus } + args: { + chat_channel: chatChannel, + topic_status: state.topicStatus, + query: state.query, + limit: state.limit, + offset: state.offset + } }); const topics = r.message?.topics || []; - const rows = topics.length - ? topics.map((topic) => { - const refs = topic.references || []; - const topicName = topic.name || topic.chat_topic || ""; - const subject = (topic.subject || topic.chat_topic_subject || "").trim(); - const displayTitle = subject || topicName; - const safeDisplayTitle = frappe.utils.escape_html(displayTitle || ""); - const safeTopicName = frappe.utils.escape_html(topicName || ""); - - const topicColor = typeof getTopicColor === "function" ? getTopicColor(topic) : (topic.topic_color || topic.color || null); - - const rawDate = topic.creation || topic.date || topic.created_on || topic.creation_date || topic.created_date || null; - let createdAt = "—"; - if (rawDate) { - try { - const tz = (frappe.boot?.time_zone?.system || frappe.boot?.time_zone?.user || "UTC"); - createdAt = get_date_from_now(rawDate, "space", tz) + " " + get_time(rawDate, tz); - } catch (e) { createdAt = String(rawDate); } - } + const hasMore = Boolean(r.message?.has_more); + + if (topics.length) { + const rows = topics.map(makeTopicRowHtml).join(""); + d.$wrapper.find(".topic-picker-list").append(rows); + } - const topicNameHtml = subject && topicName && subject !== topicName - ? `
${safeTopicName}
` - : ""; - - const refsHtml = refs.length - ? refs.map(ref => `
${frappe.utils.escape_html(ref.doctype || "")} / ${frappe.utils.escape_html(ref.docname || "")}
`).join("") - : `
${__("No references")}
`; - - const colorDotHtml = topicColor - ? `` - : ""; - - return ` -
-
-
${colorDotHtml}${safeDisplayTitle}
- ${topicNameHtml} -
- ${__("Status")}: ${frappe.utils.escape_html(topic.topic_status || "Open")} - ${topic.is_private ? " • " + __("Private") : ""} -
-
- ${__("Created At")}: ${frappe.utils.escape_html(createdAt)} -
-
-
${__("References")}:
- ${refsHtml} -
-
- -
`; - }).join("") - : `
${__("No topics found for this channel.")}
`; - - d.fields_dict.topics_html.$wrapper.html(` -
- ${showAddNew ? `
` : ""} -
${rows}
-
`); + state.offset += topics.length; + state.hasMore = hasMore; + + const hasAnyRows = d.$wrapper.find(".topic-picker-row").length > 0; + d.$wrapper.find(".topic-picker-empty").toggle(!hasAnyRows); } catch (e) { console.error("Failed to load channel topics", e); - d.fields_dict.topics_html.$wrapper.html(`
${__("Failed to load topics.")}
`); + + if (reset) { + d.$wrapper.find(".topic-picker-list").html(` +
+ ${__("Failed to load topics.")} +
+ `); + } + } finally { + state.loading = false; + d.$wrapper.find(".topic-picker-loading").hide(); } }; + const bindTopicListScroll = () => { + const $list = d.$wrapper.find(".topic-picker-list"); + + $list.off("scroll.topicPicker").on("scroll.topicPicker", function () { + const el = this; - renderTopics(); + if (state.loading || !state.hasMore) return; + + const reachedBottom = + el.scrollTop + el.clientHeight >= el.scrollHeight - 20; + + if (reachedBottom) { + renderTopics(); + } + }); + }; + + renderShell(); + bindTopicListScroll(); + renderTopics({ reset: true }); + + d.$wrapper + .off("input", ".topic-picker-search") + .on( + "input", + ".topic-picker-search", + frappe.utils.debounce(function () { + state.query = ($(this).val() || "").trim(); + renderTopics({ reset: true }); + }, 300) + ); + + d.$wrapper + .off("change", ".topic-picker-status") + .on("change", ".topic-picker-status", function () { + state.topicStatus = $(this).val() || "All"; + renderTopics({ reset: true }); + }); + + d.$wrapper + .off("scroll", ".topic-picker-list") + .on("scroll", ".topic-picker-list", function () { + const el = this; + + if (state.loading || !state.hasMore) return; + + if (el.scrollTop + el.clientHeight >= el.scrollHeight - 80) { + renderTopics(); + } + }); if (showAddNew && typeof addNewHandler === "function") { - d.$wrapper.off("click", ".add-new-topic-btn").on("click", ".add-new-topic-btn", async (e) => { + d.$wrapper + .off("click", ".add-new-topic-btn") + .on("click", ".add-new-topic-btn", async (e) => { + e.stopPropagation(); + + await addNewHandler({ + dialog: d, + renderTopics: async () => { + await renderTopics({ reset: true }); + } + }); + }); + } + + d.$wrapper + .off("click", ".pick-topic-btn") + .on("click", ".pick-topic-btn", async (e) => { + e.preventDefault(); e.stopPropagation(); - await addNewHandler({ dialog: d, renderTopics }); + + const $btn = $(e.currentTarget); + + const topicName = $btn.attr("data-topic-name"); + const topicSubject = $btn.attr("data-topic-subject") || topicName; + const topicColor = $btn.attr("data-topic-color") || null; + + if (!topicName) return; + + settled = true; + + resolve({ + name: topicName, + chat_topic: topicName, + subject: topicSubject, + chat_topic_subject: topicSubject, + topic_color: topicColor + }); + + d.hide(); }); - } - d.$wrapper.off("click", ".pick-topic-btn").on("click", ".pick-topic-btn", async (e) => { - e.preventDefault(); - e.stopPropagation(); - const $btn = $(e.currentTarget); - const topicName = $btn.attr("data-topic-name"); - const topicSubject = $btn.attr("data-topic-subject") || topicName; - const topicColor = $btn.attr("data-topic-color") || null; - if (!topicName) return; - settled = true; - resolve({ name: topicName, chat_topic: topicName, subject: topicSubject, chat_topic_subject: topicSubject, topic_color: topicColor }); - d.hide(); + bindReferenceLinks(d.$wrapper, ".topic-picker-reference-link", { + dialog: d, + hideDialog: false }); }); }; + + + + diff --git a/clefincode_chat/public/js/topic_button.js b/clefincode_chat/public/js/topic_button.js index a2f3d6d..15ef213 100644 --- a/clefincode_chat/public/js/topic_button.js +++ b/clefincode_chat/public/js/topic_button.js @@ -1,4 +1,3 @@ - console.log("topic_button.js loaded"); (function () { @@ -94,54 +93,54 @@ console.log("topic_button.js loaded"); d.fields_dict.picker_html.$wrapper.empty().append($mount); const contactList = new window.CCChatContactList({ - $wrapper: $mount, - profile: get_chat_profile(), - topic_picker: 1, - on_select: async (target) => { - await link_doc_to_selected_target(frm, target); - d.hide(); - }, - on_cancel: () => { - d.hide(); - } - }); + $wrapper: $mount, + profile: get_chat_profile(), + topic_picker: 1, + on_select: async (target) => { + await link_doc_to_selected_target(frm, target); + d.hide(); + }, + on_cancel: () => { + d.hide(); + } + }); if (contactList.ready) { await contactList.ready; } contactList.forward_message = async function () { - if (!this.selected_contacts || !this.selected_contacts.length) { - frappe.msgprint({ - title: __("Selection Required"), - message: __("Select one chat or contact first."), - indicator: "orange" - }); - return; - } + if (!this.selected_contacts || !this.selected_contacts.length) { + frappe.msgprint({ + title: __("Selection Required"), + message: __("Select one chat or contact first."), + indicator: "orange" + }); + return; + } - if (this.selected_contacts.length > 1) { - frappe.msgprint({ - title: __("Only one target allowed"), - message: __("Please select only one chat or contact."), - indicator: "orange" - }); - return; - } + if (this.selected_contacts.length > 1) { + frappe.msgprint({ + title: __("Only one target allowed"), + message: __("Please select only one chat or contact."), + indicator: "orange" + }); + return; + } - try { - const target = this.selected_contacts[0]; - await link_doc_to_selected_target(frm, target); - d.hide(); - } catch (e) { - console.error("Linking failed:", e); - frappe.msgprint({ - title: __("Error"), - message: e.message || __("Failed to link document to chat"), - indicator: "red" - }); - } - }; + try { + const target = this.selected_contacts[0]; + await link_doc_to_selected_target(frm, target); + d.hide(); + } catch (e) { + console.error("Linking failed:", e); + frappe.msgprint({ + title: __("Error"), + message: e.message || __("Failed to link document to chat"), + indicator: "red" + }); + } + }; contactList.render(); @@ -152,12 +151,10 @@ console.log("topic_button.js loaded"); })(); async function ensure_room_from_target(target) { - if (target.type === "room") { return target.room || target.name; } - const contact_email = target.email || target.user_email || @@ -176,7 +173,7 @@ async function ensure_room_from_target(target) { platform: "Chat" } }); - + console.log(check); const existing_room = check.message?.results?.name && @@ -188,7 +185,6 @@ async function ensure_room_from_target(target) { return existing_room; } - const users = JSON.stringify([ { email: frappe.session.user, platform: "Chat" }, { email: contact_email, platform: "Chat" } @@ -229,51 +225,44 @@ async function link_current_doc_to_room(frm, room) { const topicRow = topicInfo.message?.results?.[0] || null; const existing_topic = topicRow?.chat_topic || null; - const current_ref = { - doctype: frm.doctype, - docname: frm.doc.name - }; - + const current_ref = build_current_doc_reference(frm); const mention_doctypes = JSON.stringify([current_ref]); - -if (!existing_topic) { - const createdTopic = await frappe.call({ - method: "clefincode_chat.api.api_1_3_4.api.create_chat_topic_with_message", - args: { - mention_doctypes, - chat_channel: room, - user_email: frappe.session.user, - user_name: frappe.session.user_fullname || frappe.session.user - } - }); + if (!existing_topic) { + const createdTopic = await frappe.call({ + method: "clefincode_chat.api.api_1_3_4.api.create_chat_topic_with_message", + args: { + mention_doctypes, + chat_channel: room, + user_email: frappe.session.user, + user_name: frappe.session.user_fullname || frappe.session.user + } + }); - const new_chat_topic = - createdTopic.message?.results?.[0]?.chat_topic || - createdTopic.message?.results?.[0]?.name || - createdTopic.message?.chat_topic || - createdTopic.message?.name || - null; + const new_chat_topic = + createdTopic.message?.results?.[0]?.chat_topic || + createdTopic.message?.results?.[0]?.name || + createdTopic.message?.chat_topic || + createdTopic.message?.name || + null; - if (!new_chat_topic) { - throw new Error("Topic was created but chat_topic was not returned"); - } + if (!new_chat_topic) { + throw new Error("Topic was created but chat_topic was not returned"); + } - return { - room, - chat_topic: new_chat_topic, - mode: "created" - }; -} + return { + room, + chat_topic: new_chat_topic, + mode: "created" + }; + } const refs = get_topic_references(topicRow); - const same_doc_exists = refs.some((ref) => { return ref.doctype === frm.doctype && ref.docname === frm.doc.name; }); - if (same_doc_exists) { return { room, @@ -282,91 +271,54 @@ if (!existing_topic) { }; } - - if (refs.length > 0) { - const action = await ask_topic_conflict_action(refs); - - if (action === "cancel") { - return { - room, - chat_topic: existing_topic, - mode: "cancelled" - }; - } + const selectedTopic = await pick_topic_for_room(room, { + showTopActions: true, + compact: true + }); - if (action === "select") { - const selectedTopic = await pick_topic_for_room(room); - if (!selectedTopic) { - return { - room, - chat_topic: existing_topic, - mode: "cancelled" - }; - } - const selectedTopicName = selectedTopic.chat_topic || selectedTopic.name; - await frappe.call({ - method: "clefincode_chat.api.api_1_3_4.api.add_reference_doctype_with_message", - args: { - mention_doctypes, - chat_topic: selectedTopicName, - chat_channel: room, - user_email: frappe.session.user, - user_name: frappe.session.user_fullname || frappe.session.user - } - }); - return { - room, - chat_topic: selectedTopicName, - mode: "attached" - }; - } + if (!selectedTopic) { + return { + room, + chat_topic: existing_topic, + mode: "cancelled" + }; + } - if (action === "append") { - await frappe.call({ - method: "clefincode_chat.api.api_1_3_4.api.add_reference_doctype_with_message", - args: { - mention_doctypes, - chat_topic: existing_topic, - chat_channel: room, - user_email: frappe.session.user, - user_name: frappe.session.user_fullname || frappe.session.user - } - }); + if (selectedTopic.__action === "create_new") { + const replaced = await replace_topic_references(existing_topic, room, current_ref); - return { - room, - chat_topic: existing_topic, - mode: "attached" - }; - } + return { + room, + chat_topic: replaced.chat_topic || existing_topic, + old_chat_topic: replaced.old_chat_topic || existing_topic, + mode: "replaced" + }; + } - if (action === "replace") { - const replaced = await replace_topic_references(existing_topic, room, current_ref); + const selectedTopicName = selectedTopic.chat_topic || selectedTopic.name; - return { - room, - chat_topic: replaced.chat_topic || existing_topic, - old_chat_topic: replaced.old_chat_topic || existing_topic, - mode: "replaced" - }; - } + if (!selectedTopicName) { + throw new Error("Selected topic has no name"); } - // fallback await frappe.call({ - method: "clefincode_chat.api.api_1_3_4.api.add_reference_doctype", + method: "clefincode_chat.api.api_1_3_4.api.add_reference_doctype_with_message", args: { mention_doctypes, - chat_topic: existing_topic + chat_topic: selectedTopicName, + chat_channel: room, + user_email: frappe.session.user, + user_name: frappe.session.user_fullname || frappe.session.user } }); return { room, - chat_topic: existing_topic, + chat_topic: selectedTopicName, mode: "attached" }; } + async function link_doc_to_selected_target(frm, target) { console.log("Selected target object:", target); @@ -376,15 +328,16 @@ async function link_doc_to_selected_target(frm, target) { if (result.mode === "cancelled") { return result; } + ensure_chat_widget_open(); + const messages = { - created: __("Document linked and new chat topic created"), - attached: __("Document added to existing chat topic"), - already_linked: __("This document is already linked"), - replaced: __("Old topic closed and a new topic was created for this document") - }; + created: __("Document linked and new chat topic created"), + attached: __("Document added to existing chat topic"), + already_linked: __("This document is already linked"), + replaced: __("Old topic closed and a new topic was created for this document") + }; - open_chat_room({ is_admin: frappe.user.has_role("System Manager"), user: frappe.session.user_fullname || frappe.session.user, @@ -400,43 +353,56 @@ async function link_doc_to_selected_target(frm, target) { platform: target.raw?.platform || "Chat", chat_topic: result.chat_topic || null }); + frappe.show_alert({ message: messages[result.mode] || __("Done"), indicator: "green" }); - - return result; } -function extract_topic_references(topicRow) { - - if (!topicRow) return []; +function get_topic_references(topicRow) { + console.log(topicRow); + if (!topicRow) return []; if (Array.isArray(topicRow.reference_doctypes)) { - return topicRow.reference_doctypes.map(ref => ({ - doctype: ref.doctype || ref.reference_doctype, - docname: ref.docname || ref.reference_name - })).filter(ref => ref.doctype && ref.docname); + return topicRow.reference_doctypes.map((r) => ({ + doctype: r.doctype, + docname: r.docname + })).filter(r => r.doctype && r.docname); + } + + if (typeof topicRow.reference_doctypes === "string" && topicRow.reference_doctypes.trim()) { + try { + const parsed = JSON.parse(topicRow.reference_doctypes); + if (Array.isArray(parsed)) { + return parsed.map((r) => ({ + doctype: r.doctype_link || r.doctype, + docname: r.docname, + active: cint(r.active || 0) + })).filter(r => r.doctype && r.docname); + } + } catch (e) { + console.warn("Failed to parse references", e); + } } if (topicRow.mention_doctypes) { try { const parsed = JSON.parse(topicRow.mention_doctypes); if (Array.isArray(parsed)) { - return parsed.map(ref => ({ - doctype: ref.doctype, - docname: ref.docname - })).filter(ref => ref.doctype && ref.docname); + return parsed.map((r) => ({ + doctype: r.doctype, + docname: r.docname + })).filter(r => r.doctype && r.docname); } } catch (e) { console.warn("Could not parse mention_doctypes", e); } } - if (topicRow.reference_doctype && topicRow.reference_name) { return [{ doctype: topicRow.reference_doctype, @@ -447,84 +413,38 @@ function extract_topic_references(topicRow) { return []; } -function ask_topic_conflict_action(existing_refs) { - return new Promise((resolve) => { - let settled = false; - - const finish = (value) => { - if (settled) return; - settled = true; - resolve(value); - }; - - const refs_text = existing_refs - .map(ref => `${frappe.utils.escape_html(ref.doctype)} / ${frappe.utils.escape_html(ref.docname)}`) - .join("
"); - - const d = new frappe.ui.Dialog({ - title: __("Chat already linked"), - fields: [ - { - fieldtype: "HTML", - fieldname: "message", - options: ` -
-
- ${__("This chat already has another topic/document:")} -
-
${refs_text}
-
- ${__("Choose what you want to do")} -
-
- ` - } - ], - primary_action_label: __("Select Topic"), - primary_action() { - finish("select"); - d.hide(); - } - }); - - d.show(); - -const $replace_btn = $(` - -`); +function cint(value) { + return parseInt(value || 0, 10); +} -const $cancel_btn = $(` - -`); +function build_current_doc_reference(frm) { + const doctypeSlug = frappe.router?.slug + ? frappe.router.slug(frm.doctype) + : frm.doctype.replace(/\s+/g, "-").toLowerCase(); - d.show(); + const route = `/app/${doctypeSlug}/${encodeURIComponent(frm.doc.name)}`; + const absolute_url = `${window.location.origin}${route}`; + return { + doctype: frm.doctype, + docname: frm.doc.name, - $replace_btn.on("click", () => { - finish("replace"); - d.hide(); - }); + // Keep old names for backend compatibility + reference_doctype: frm.doctype, + reference_name: frm.doc.name, - $cancel_btn.on("click", () => { - finish("cancel"); - d.hide(); - }); + // Link fields + route, + link: route, + reference_link: route, + url: absolute_url, - - const $footer = d.$wrapper.find(".modal-footer"); - $footer.append($replace_btn); - $footer.append($cancel_btn); - - - d.$wrapper.on("hidden.bs.modal", () => { - finish("cancel"); - }); - }); + // Display fields + label: `${frm.doctype} / ${frm.doc.name}`, + title: frm.doc.title || frm.doc.subject || frm.doc.name + }; } + async function replace_topic_references(chat_topic, chat_channel, current_ref) { const r = await frappe.call({ method: "clefincode_chat.api.api_1_3_4.api.replace_topic_references", @@ -533,58 +453,17 @@ async function replace_topic_references(chat_topic, chat_channel, current_ref) { chat_channel, user_email: frappe.session.user, user_name: frappe.session.user_fullname || frappe.session.user, - mention_doctypes: JSON.stringify([ - { - doctype: current_ref.doctype, - docname: current_ref.docname - } - ]) + mention_doctypes: JSON.stringify([current_ref]) } }); return r.message?.results?.[0] || r.message || {}; } -function get_topic_references(topicRow) { - console.log(topicRow); - if (!topicRow) return []; - - - if (Array.isArray(topicRow.reference_doctypes)) { - - return topicRow.reference_doctypes.map((r) => ({ - doctype: r.doctype, - docname: r.docname, - - })).filter(r => r.doctype && r.docname); - } - - - if (typeof topicRow.reference_doctypes === "string" && topicRow.reference_doctypes.trim()) { - try { - const parsed = JSON.parse(topicRow.reference_doctypes); - if (Array.isArray(parsed)) { - return parsed.map((r) => ({ - doctype: r.doctype_link, - docname: r.docname, - active: cint(r.active || 0) - })).filter(r => r.doctype && r.docname); - } - } catch (e) { - console.warn("Failed to parse references", e); - } - } - - return []; -} -function cint(value) { - return parseInt(value || 0, 10); -} function ensure_chat_widget_open() { const app = window.erpnext_chat_app; if (!app) return false; - if (typeof app.show_chat_widget === "function") { app.show_chat_widget(); return true; @@ -594,50 +473,123 @@ function ensure_chat_widget_open() { app.$chat_element.show(); } - if (app.$chat_bubble?.length) { app.$chat_bubble.hide(); } return true; } -function ensure_chat_widget_open() { - const app = window.erpnext_chat_app; - if (!app) return false; - - if (typeof app.show_chat_widget === "function") { - app.show_chat_widget(); - return true; +async function pick_topic_for_room(room, opts = {}) { + if (!window.CCOpenChannelTopicPicker) { + frappe.msgprint({ + title: __("Error"), + message: __("Topic picker is not available."), + indicator: "red" + }); + return null; } - if (app.$chat_element?.length) { - app.$chat_element.show(); - } + let externalResolve; - if (app.$chat_bubble?.length) { - app.$chat_bubble.hide(); + const externalAction = new Promise((resolve) => { + externalResolve = resolve; + }); + + const pickerPromise = window.CCOpenChannelTopicPicker({ + chatChannel: room, + topicStatus: "Open", + title: __("Select Topic"), + selectLabel: __("Select"), + showAddNew: false + }); + + if (opts.showTopActions || opts.compact) { + decorate_open_topic_picker_dialog(externalResolve, opts); } - return true; + return await Promise.race([pickerPromise, externalAction]); } -async function pick_topic_for_room(room) { - if (!window.CCOpenChannelTopicPicker) { - frappe.msgprint({ - title: __("Error"), - message: __("Topic picker is not available."), - indicator: "red" - }); - return null; - } - - return await window.CCOpenChannelTopicPicker({ - chatChannel: room, - topicStatus: "Open", - title: __("Select Topic"), - selectLabel: __("Select"), - showAddNew: false - }); + +function decorate_open_topic_picker_dialog(resolve, opts = {}) { + let tries = 0; + const maxTries = 80; + + const timer = setInterval(() => { + tries += 1; + + const $modal = $(".modal.show").filter(function () { + return $(this).find(".modal-title").text().trim() === __("Select Topic"); + }).last(); + + if (!$modal.length) { + if (tries >= maxTries) { + clearInterval(timer); + } + return; + } + + clearInterval(timer); + + $modal.addClass("cc-open-topic-picker-modal"); + + if (opts.compact) { + $modal.find(".modal-dialog").css({ + "max-width": "560px", + "width": "560px" + }); + } + + if ($modal.find(".cc-topic-picker-top-actions").length) { + return; + } + + const $topActions = $(` +
+
+ +
+ +
+ +
+
+ `); + + $modal.find(".modal-header").after($topActions); + + $topActions.find(".cc-topic-picker-cancel").on("click", () => { + resolve(null); + $modal.modal("hide"); + }); + + $topActions.find(".cc-topic-picker-create-new").on("click", () => { + resolve({ __action: "create_new" }); + $modal.modal("hide"); + }); + + $modal.one("hidden.bs.modal", () => { + resolve(null); + }); + }, 50); } function open_chat_room(profile, chat_status = null) { @@ -648,16 +600,14 @@ function open_chat_room(profile, chat_status = null) { return; } - if (app.$chat_element?.length) { app.$chat_element.show(); } - -if (!profile.chat_topic && window.CCCheckIfChatWindowOpen(profile.room, "room")) { - $(".expand-chat-window[data-id|='" + profile.room + "']").click(); - return; -} + if (!profile.chat_topic && window.CCCheckIfChatWindowOpen(profile.room, "room")) { + $(".expand-chat-window[data-id|='" + profile.room + "']").click(); + return; + } const chat_window = new window.CCChatWindow({ profile: { room: profile.room } @@ -674,6 +624,7 @@ if (!profile.chat_topic && window.CCCheckIfChatWindowOpen(profile.room, "room")) chatSpaceOpts.chat_topic_channel = profile.room; chatSpaceOpts.chat_topic_subject = profile.chat_topic_subject || null; chatSpaceOpts.topic_write_mode = true; + chatSpaceOpts.is_topic_window = true; } new window.CCChatSpace(chatSpaceOpts); diff --git a/clefincode_chat/translations/ar.csv b/clefincode_chat/translations/ar.csv index 316fc9e..6f5129b 100644 --- a/clefincode_chat/translations/ar.csv +++ b/clefincode_chat/translations/ar.csv @@ -3,4 +3,5 @@ Open Topic,بدء نقاش "This user has limited chat access.","هذا المستخدم لديه وصول محدود إلى المحادثة." "A limited user can communicate with internal users or support team members, but cannot see or start conversations with other limited users.","يمكن للمستخدم المحدود التواصل مع المستخدمين الداخليين أو أعضاء فريق الدعم، لكنه لا يستطيع رؤية أو بدء محادثات مع مستخدمين محدودين آخرين." "This restriction helps protect external users' privacy and prevents them from discovering or contacting each other inside the system.","يساعد هذا التقييد على حماية خصوصية المستخدمين الخارجيين ومنعهم من اكتشاف أو التواصل مع بعضهم داخل النظام." -"Is Limited","مستخدم محدود" \ No newline at end of file +"Is Limited","مستخدم محدود" +"An existing WhatsApp template with the same content, language, and buttons already exists: {0}.

Please use the existing template instead.

If you want to create a new template, please change the message content or buttons.","يوجد قالب واتساب بنفس المحتوى واللغة والأزرار: {0}.

يرجى استخدام القالب الموجود بدلًا من إنشاء قالب جديد.

إذا كنت تريد إنشاء قالب جديد، يرجى تغيير محتوى الرسالة أو الأزرار." \ No newline at end of file diff --git a/setup.py b/setup.py index f4738bb..edf5570 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="clefincode_chat", - version='1.3.910', + version='1.3.911', description="ERPNext & Frappe Business Chat: A self-hosted communication solution.", author="ClefinCode L.L.C-FZ", author_email="info@clefincode.com",