diff --git a/package.json b/package.json
index a4b934904c..a976e1f307 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,8 @@
]
},
"dependencies": {
- "@nyaruka/flow-editor": "1.36.1",
- "@nyaruka/temba-components": "0.111.7",
+ "@nyaruka/flow-editor": "1.36.3",
+ "@nyaruka/temba-components": "0.112.0",
"codemirror": "5.18.2",
"colorette": "1.2.2",
"fa-icons": "0.2.0",
diff --git a/static/css/temba-components.css b/static/css/temba-components.css
index 2a88df524d..b7ad9ba71a 100644
--- a/static/css/temba-components.css
+++ b/static/css/temba-components.css
@@ -29,7 +29,7 @@ html {
--color-borders: rgba(0, 0, 0, 0.07);
--color-placeholder: #ccc;
--color-primary-light: #eee;
- --color-secondary-light: rgba(var(--secondary-rgb), .3);
+ --color-secondary-light: rgba(var(--secondary-rgb), 0.3);
--color-primary-dark: rgb(var(--primary-rgb));
--color-secondary-dark: rgb(var(--secondary-rgb));
--color-focus: #a4cafe;
@@ -38,7 +38,7 @@ html {
--color-widget-border: rgb(225, 225, 225);
--color-options-bg: var(--color-widget-bg);
--color-selection: #f0f6ff;
- --color-row-hover: rgba(var(--selection-light-rgb), .4);
+ --color-row-hover: rgba(var(--selection-light-rgb), 0.4);
--color-available: #00f100;
--color-tertiary: rgb(var(--tertiary-rgb));
--color-text-light: rgba(255, 255, 255, 1);
@@ -59,7 +59,7 @@ html {
--color-button-secondary-text: var(--color-text-dark);
--color-button-destructive: rgb(var(--error-rgb));
--color-button-destructive-text: var(--color-text-light);
- --color-button-attention: #2ecc71;
+ --color-button-attention: #3ca96a;
--color-label-primary: var(--color-primary-dark);
--color-label-primary-text: var(--color-text-light);
--color-label-secondary: var(--color-secondary-dark);
@@ -77,27 +77,31 @@ html {
--color-alert: rgb(var(--error-rgb));
--icon-color: var(--text-color);
--icon-color-circle: rgb(240, 240, 240);
- --icon-color-circle-hover: rgba(245, 245, 245, .8);
+ --icon-color-circle-hover: rgba(245, 245, 245, 0.8);
--header-bg: var(--color-primary-dark);
--header-text: var(--color-text-light);
--color-text-help: rgb(120, 120, 120);
--color-automated: rgb(78, 205, 106);
-
/* Shadows */
- --widget-box-shadow: rgba(-1, -1, 0, .1) 0px 1px 7px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
- --widget-box-shadow-focused: 0 0 0 3px rgba(164, 202, 254, .45), rgba(0, 0, 0, 0.05) 0px 3px 7px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
+ --widget-box-shadow: rgba(-1, -1, 0, 0.1) 0px 1px 7px 0px,
+ rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
+ --widget-box-shadow-focused: 0 0 0 3px rgba(164, 202, 254, 0.45),
+ rgba(0, 0, 0, 0.05) 0px 3px 7px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
--widget-box-shadow-focused-error: 0 0 0 3px rgba(var(--error-rgb), 0.3);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
- --shadow-widget: 0 3px 20px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.02);
+ --shadow-widget: 0 3px 20px 0 rgba(0, 0, 0, 0.04),
+ 0 1px 2px 0 rgba(0, 0, 0, 0.02);
/* temba-select */
--select-input-height: inherit;
--temba-select-selected-font-size: 1em;
- --temba-select-selected-padding: .6em .8em;
+ --temba-select-selected-padding: 0.6em 0.8em;
--temba-select-selected-line-height: 1.2em;
- --options-block-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
- --options-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ --options-block-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
+ 0 1px 2px 0 rgba(0, 0, 0, 0.03);
+ --options-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--dropdown-shadow: rgb(0 0 0 / 15%) 0px 0px 30px, rgb(0 0 0 / 12%) 0px 2px 6px;
/* buttons */
@@ -106,7 +110,7 @@ html {
--button-x: 1.5em;
/* textinput */
- --temba-textinput-padding: 0.6em .8em;
+ --temba-textinput-padding: 0.6em 0.8em;
--temba-textinput-font-size: 1;
/* charcount */
@@ -126,6 +130,4 @@ html {
--event-padding: 0.5em 1em;
--control-margin-bottom: 15px;
--menu-padding: 1em;
-
-
-}
\ No newline at end of file
+}
diff --git a/temba/api/v2/views.py b/temba/api/v2/views.py
index 3252e13bcb..00dca1e49d 100644
--- a/temba/api/v2/views.py
+++ b/temba/api/v2/views.py
@@ -3497,7 +3497,7 @@ class UsersEndpoint(ListAPIMixin, BaseEndpoint):
A **GET** returns the users in your workspace, ordered by newest created first.
- * **email** - the email address of the user (string).
+ * **email** - the email address of the user (string), filterable as `email`.
* **first_name** - the first name of the user (string).
* **last_name** - the last name of the user (string).
* **role** - the role of the user (string), filterable as `role` which can be repeated.
@@ -3531,6 +3531,11 @@ class UsersEndpoint(ListAPIMixin, BaseEndpoint):
def derive_queryset(self):
org = self.request.org
+ # filter by email if specified
+ email = self.request.query_params.get("email")
+ if email:
+ return org.users.filter(email__iexact=email).prefetch_related("settings")
+
# limit to roles if specified
roles = self.request.query_params.getlist("role")
if roles:
diff --git a/temba/contacts/tests.py b/temba/contacts/tests.py
index ec48beb36c..8556dca59d 100644
--- a/temba/contacts/tests.py
+++ b/temba/contacts/tests.py
@@ -912,42 +912,42 @@ def test_interrupt(self, mr_mocks):
other_org_contact = self.create_contact("Hans", phone="+593979123456", org=self.org2)
read_url = reverse("contacts.contact_read", args=[contact.uuid])
- interrupt_url = reverse("contacts.contact_interrupt", args=[contact.id])
+ interrupt_url = reverse("contacts.contact_interrupt", args=[contact.uuid])
self.login(self.admin)
- # no interrupt option if not in a flow
+ # shoud see start flow option
response = self.client.get(read_url)
- self.assertNotContains(response, interrupt_url)
+ self.assertContentMenu(read_url, self.admin, ["Edit", "Start Flow", "Open Ticket"])
MockSessionWriter(contact, self.create_flow("Test")).wait().save()
MockSessionWriter(other_org_contact, self.create_flow("Test", org=self.org2)).wait().save()
- # now it's an option
- self.assertContentMenu(read_url, self.admin, ["Edit", "Start Flow", "Open Ticket", "Interrupt"])
+ # start option should be gone
+ self.assertContentMenu(read_url, self.admin, ["Edit", "Open Ticket"])
# can't interrupt if not logged in
self.client.logout()
- response = self.client.post(interrupt_url, {"id": contact.id})
+ response = self.client.post(interrupt_url)
self.assertLoginRedirect(response)
self.login(self.user)
# can't interrupt if just regular user
- response = self.client.post(interrupt_url, {"id": contact.id})
+ response = self.client.post(interrupt_url)
self.assertLoginRedirect(response)
self.login(self.admin)
- response = self.client.post(interrupt_url, {"id": contact.id})
+ response = self.client.post(interrupt_url)
self.assertEqual(302, response.status_code)
contact.refresh_from_db()
self.assertIsNone(contact.current_flow)
# can't interrupt contact in other org
- restore_url = reverse("contacts.contact_interrupt", args=[other_org_contact.id])
- response = self.client.post(restore_url, {"id": other_org_contact.id})
+ other_contact_interrupt = reverse("contacts.contact_interrupt", args=[other_org_contact.uuid])
+ response = self.client.post(other_contact_interrupt)
self.assertLoginRedirect(response)
# contact should be unchanged
diff --git a/temba/contacts/views.py b/temba/contacts/views.py
index 7dee509eed..5a536cd7ff 100644
--- a/temba/contacts/views.py
+++ b/temba/contacts/views.py
@@ -336,7 +336,7 @@ def build_context_menu(self, menu):
)
if obj.status == Contact.STATUS_ACTIVE:
- if self.has_org_perm("flows.flow_start"):
+ if not obj.current_flow and self.has_org_perm("flows.flow_start"):
menu.add_modax(
_("Start Flow"),
"start-flow",
@@ -348,8 +348,6 @@ def build_context_menu(self, menu):
menu.add_modax(
_("Open Ticket"), "open-ticket", reverse("contacts.contact_open_ticket", args=[obj.id])
)
- if self.has_org_perm("contacts.contact_interrupt") and obj.current_flow:
- menu.add_url_post(_("Interrupt"), reverse("contacts.contact_interrupt", args=(obj.id,)))
class Scheduled(BaseReadView):
"""
@@ -800,13 +798,15 @@ def save(self, obj):
def get_success_url(self):
return f"{reverse('tickets.ticket_list')}all/open/{self.ticket.uuid}/"
- class Interrupt(OrgObjPermsMixin, SmartUpdateView):
+ class Interrupt(ModalFormMixin, OrgObjPermsMixin, SmartUpdateView):
"""
Interrupt this contact
"""
+ slug_url_kwarg = "uuid"
fields = ()
- success_url = "uuid@contacts.contact_read"
+ success_url = "hide"
+ submit_button_name = _("Interrupt")
def save(self, obj):
obj.interrupt(self.request.user)
diff --git a/temba/tickets/views.py b/temba/tickets/views.py
index 1bfa5bb75f..235c276ce6 100644
--- a/temba/tickets/views.py
+++ b/temba/tickets/views.py
@@ -317,12 +317,7 @@ def build_context_menu(self, menu):
on_submit="handleNoteAdded()",
)
- if ticket.contact.current_flow:
- if self.has_org_perm("contacts.contact_interrupt"):
- menu.add_url_post(
- _("Interrupt"), reverse("contacts.contact_interrupt", args=(ticket.contact.id,))
- )
- else:
+ if not ticket.contact.current_flow:
if self.has_org_perm("flows.flow_start"):
menu.add_modax(
_("Start Flow"),
diff --git a/templates/contacts/contact_interrupt.html b/templates/contacts/contact_interrupt.html
new file mode 100644
index 0000000000..231242ee10
--- /dev/null
+++ b/templates/contacts/contact_interrupt.html
@@ -0,0 +1,8 @@
+{% extends "includes/modax.html" %}
+{% load i18n %}
+
+{% block fields %}
+ {% blocktrans trimmed %}
+ You are about to interrupt the current flow for {{ object }}. There is no way to undo this. Are you sure?
+ {% endblocktrans %}
+{% endblock fields %}
diff --git a/templates/contacts/contact_read.html b/templates/contacts/contact_read.html
index bea14dd919..49da65e790 100644
--- a/templates/contacts/contact_read.html
+++ b/templates/contacts/contact_read.html
@@ -13,7 +13,11 @@
class="flex-grow -mt-2">
-
+
@@ -80,6 +84,13 @@
{% endblock extra-style %}
{% block extra-script %}