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 %}