diff --git a/src/apps/callflows/app.js b/src/apps/callflows/app.js index 26d2e0a..b5c8ef9 100644 --- a/src/apps/callflows/app.js +++ b/src/apps/callflows/app.js @@ -21,6 +21,7 @@ define(function(require) { 'flushdtmf', 'groups', 'language', + 'lists', 'lookupcidname', 'media', 'menu', diff --git a/src/apps/callflows/i18n/en-US.json b/src/apps/callflows/i18n/en-US.json index ca1c9d4..2e12526 100644 --- a/src/apps/callflows/i18n/en-US.json +++ b/src/apps/callflows/i18n/en-US.json @@ -1103,6 +1103,9 @@ "blacklist": { "title": "Blacklists" }, + "lists": { + "title": "Lists" + }, "preflow": { "title": "Preflow", "help": "Preflow is an option to execute a callflow before any other callflow. It is set on the account level and will apply on any call using callflow for that account. It can be useful to activate call recording, or the missed call alert feature for example.", @@ -1208,6 +1211,17 @@ "are_you_sure_you_want_to_delete": "Are you sure you want to delete this blacklist?", "help": "In order to activate this blacklist, head to the Account Settings section" }, + "lists": { + "title": "Lists", + "addNumber": "Add Number", + "create": "Create List", + "edit": "Edit List", + "deleteList": "Delete List", + "name": "Name", + "listNumbers": "Entries:", + "are_you_sure_you_want_to_delete": "Are you sure you want to delete this list? (All entries will also be removed.)", + "help": "You can use this list for CID matching in a callflow." + }, "__comment": "UI-2216: adding tts to adv. callflows", "__version": "v4.0", diff --git a/src/apps/callflows/submodules/lists/lists.css b/src/apps/callflows/submodules/lists/lists.css new file mode 100644 index 0000000..1d588d1 --- /dev/null +++ b/src/apps/callflows/submodules/lists/lists.css @@ -0,0 +1,93 @@ +#lists_wrapper .list-numbers { + display: block; + width: 470px; +} + +#lists_wrapper .list-numbers .delete-number { + margin-bottom: 8px; +} + +#lists_wrapper .list-numbers .delete-number .fa { + color: white; + font-size: 20px; +} + +#lists_wrapper .list-numbers .add-number { + margin-bottom: 10px; + border-radius: 4px; +} + +#lists_wrapper .list-numbers .add-number input { + margin: 0 20px 0 0; +} + +#lists_wrapper .list-numbers .add-number #cancel_number { + margin-left: 20px; +} + +#lists_wrapper .list-numbers .saved-numbers { + max-height: 300px; + overflow: auto; +} + +#lists_wrapper .number-wrapper { + background: #22A5FF; + border-top: 1px solid #22A5FF; + border-right: 1px solid #22A5FF; + border-left: 1px solid #22A5FF; + line-height: 30px; + color: white; +} + +#lists_wrapper .number-wrapper.placeholder > div, +#lists_wrapper .number-wrapper:not(.placeholder) { + padding: 5px 10px; +} + +#lists_wrapper .number-wrapper.placeholder .create-text { + color: #303039; +} + +#lists_wrapper .number-wrapper.placeholder .create-text:hover { + background: #22A5FF; + cursor: pointer; + color: white; +} + +#lists_wrapper .saved-numbers .number-wrapper:first-child { + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} + +#lists_wrapper .saved-numbers .number-wrapper:last-child { + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + + border-bottom: 1px solid #22A5FF; +} + +#lists_wrapper .saved-numbers .number-wrapper + .number-wrapper { + border-top: none; + margin-top: 1px; +} + +#lists_wrapper .number-wrapper.placeholder { + background: none; + border: 1px dashed #999; +} + +#lists_wrapper .number-wrapper.placeholder .create-inputs, +#lists_wrapper .number-wrapper.placeholder.active .create-text { + display: none; +} + +#lists_wrapper .number-wrapper.placeholder.active .create-inputs, +#lists_wrapper .number-wrapper.placeholder .create-text { + display: inherit; +} + +#lists_wrapper .help-text-icon { + color: #22A5FF; + margin-right: 10px; + margin-top :15px; +} diff --git a/src/apps/callflows/submodules/lists/lists.js b/src/apps/callflows/submodules/lists/lists.js new file mode 100644 index 0000000..be75ebb --- /dev/null +++ b/src/apps/callflows/submodules/lists/lists.js @@ -0,0 +1,483 @@ +////////////////////////////////////////////////// +// Lists Submodule for Callflows +// @copyright 2024 RuhNet https://ruhnet.co +// @author Ruel Tmeizeh +// +// Derived from blacklist submodule. +// cb_lists Crossbar module must be loaded: +// sup crossbar_maintenance start_module cb_lists +// +////////////////////////////////////////////////// + +define(function(require) { + var $ = require('jquery'), + _ = require('lodash'), + monster = require('monster'); + + var app = { + requests: {}, + + subscribe: { + 'callflows.lists.edit': 'listsEdit', + 'callflows.fetchActions': 'listsDefineActions' + }, + + // Defines API requests not included in the SDK + requests: { + 'lists.list': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists', + 'verb': 'GET' + }, + 'lists.get': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists/{listId}', + 'verb': 'GET' + }, + 'lists.create': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists', + 'verb': 'PUT' + }, + 'lists.delete': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists/{listId}', + 'verb': 'DELETE' + }, + 'lists.getEntries': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists/{listId}/entries', + 'verb': 'GET' + }, + 'lists.addEntry': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists/{listId}/entries', + 'verb': 'PUT' + }, + 'lists.updateEntry': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists/{listId}/entries/{listEntryId}', + 'verb': 'PATCH' + }, + 'lists.deleteEntry': { + 'apiRoot': monster.config.api.default, + 'url': 'accounts/{accountId}/lists/{listId}/entries/{listEntryId}', + 'verb': 'DELETE' + } + }, + + + + listsDefineActions: function(args) { + var self = this, + callflow_nodes = args.actions; + + $.extend(callflow_nodes, { + 'lists': { + name: self.i18n.active().callflows.lists.title, + module: 'lists', + listEntities: function(callback) { + + monster.request({ + resource: "lists.list", + data: { + accountId: self.accountId, + }, + success: function(res) { + callback && callback(res.data); + }, + error: function(res) { + if (res.status == 404) { + callback([]); //Populate results with nothing + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to get lists: " + parsedError); + callback([]); //Populate results with nothing + } + } + }); + }, + editEntity: 'callflows.lists.edit' + } + }); + }, + + // Added for the subscribed event to avoid refactoring conferenceEdit + listsEdit: function(args) { + //console.log(args); + var self = this, + afterGetData = function(data) { + var template = $(self.getTemplate({ + name: 'edit', + data: data, + submodule: 'lists' + })), + listsForm = template.find('#lists-form'), + $listNumbers = template.find('.saved-numbers'); + + monster.ui.validate(listsForm, { + rules: { + 'name': { required: true } + } + }); + + _.each(data.entries, function(entry) { + entry['state'] = 'saved'; + $listNumbers.append($(self.getTemplate({ + name: 'addNumber', + data: entry, + submodule: 'lists' + }))); + }); + + self.listsBindEvents(data, template, args.callbacks); + + (args.target) + .empty() + .append(template); + }; + + if (args.data.id) { + self.listsGet(args.data.id, function(data) { + afterGetData(data); //data = {id: listid, name: listname, entries: [entry1,entry2,...]} + }); + } else { + afterGetData({}); + } + }, + + listsBindEvents: function(data, template, callbacks) { + var self = this, + addNumber = function(e) { + var number = template.find('#number_value').val(); + var list_id = template.find('#active-list').attr('data-listid'); + + if (number) { + var entryData = { + number: number, + }; + + $('.list-numbers .saved-numbers', template) + .prepend($(self.getTemplate({ + name: 'addNumber', + data: { + key: list_id ? list_id : null, + id: null, + state: 'pending-add', + value: { + number: number, + }, + }, + submodule: 'lists' + }))); + $('#number_value', template).val(''); + } + }; + + $('.number-wrapper.placeholder:not(.active)', template).click(function() { + var $this = $(this); + + $this.addClass('active'); + + $('#number_value', template).focus(); + }); + + $('#add_number', template).click(function(e) { + e.preventDefault(); + addNumber(); + }); + + $('.add-number', template).bind('keypress', function(e) { + var code = e.keyCode || e.which; + + if (code === 13) { + addNumber(e); + } + }); + + $(template).delegate('.delete-number', 'click', function(e) { + if ($(this).parents('.number-wrapper').data('state') == 'pending-add') { //if this has just been added and not saved, remove it + $(this).parents('.number-wrapper').remove(); + } else { //toggle background red to signify that it will be deleted when save is clicked + if ($(this).parents('.number-wrapper').hasClass('pending-delete')) { + $(this).parents('.number-wrapper').removeClass('pending-delete'); + $(this).parents('.number-wrapper').css('background-color',''); + $(this).parents('.number-wrapper').attr('data-state', 'saved'); + } else { + $(this).parents('.number-wrapper').addClass('pending-delete'); + $(this).parents('.number-wrapper').attr('data-state', 'pending-delete'); + $(this).parents('.number-wrapper').css('background-color','#B91B0C'); + } + } + }); + + $('#cancel_number', template).click(function(e) { + e.stopPropagation(); + + $('.number-wrapper.placeholder.active', template).removeClass('active'); + $('#number_value', template).val(''); + }); + + $('.lists-save', template).click(function() { + var formData = monster.ui.getFormData('blacklist-form'), + cleanData = self.listsCleanFormData(formData), + entries = []; + + $('.saved-numbers .number-wrapper', template).each(function(k, wrapper) { + entry = { + list_id: $(wrapper).attr('data-listid'), + id: $(wrapper).attr('id'), + number: $(wrapper).attr('data-number'), + state: $(wrapper).attr('data-state'), + } + entries.push(entry); + }); + + cleanData.entries = entries; + + if (data.id) { + cleanData.id = data.id; + } + + self.listsSave(cleanData, callbacks.save_success); + }); + + $('.lists-delete', template).click(function() { + monster.ui.confirm(self.i18n.active().callflows.lists.are_you_sure_you_want_to_delete, function() { + self.listsDelete(data.id, callbacks.delete_success); + }); + }); + }, + + listsCleanFormData: function(data) { + delete data.extra; + + return data; + }, + + listsSave: function(data, callback) { + var self = this; + + if (data.id) { + self.listsUpdate(data, callback); + } else { + var newListData = { name: data.name }; + self.listsCreate(newListData, function(createdListData) { + data.id = createdListData.id; + self.listsUpdate(data, callback); + }); + } + }, + + listsList: function(callback) { + var self = this; + + monster.request({ + resource: "lists.list", + data: { + accountId: self.accountId, + }, + success: function(res) { + callback && callback(res.data); + }, + error: function(res) { + if (res.status == 404) { + callback([]); //Populate results with nothing + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to get lists: " + parsedError); + callback([]); //Populate results with nothing + } + } + }); + }, + + listsGet: function(id, callback) { + var self = this; + + monster.request({ + resource: "lists.get", + data: { + accountId: self.accountId, + listId: id, + }, + success: function(data) { + listprops = data.data; + monster.request({ + resource: "lists.getEntries", + data: { + accountId: self.accountId, + listId: id, + }, + success: function(res) { + var listdata = { + id: listprops.id, + name: listprops.name, + entries: res.data + }; + callback && callback(listdata); + }, + error: function(res) { + if (res.status == 404) { + callback([]); //Populate results with nothing + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to get list entries from list " + id + ": " + parsedError); + callback([]); //Populate results with nothing + } + } + }); + }, + error: function(res) { + if (res.status == 404) { + callback([]); //Populate results with nothing + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to get list " + id + ": " + parsedError); + callback([]); //Populate results with nothing + } + } + }); + + }, + + listsCreate: function(data, callback) { + var self = this; + monster.request({ + resource: "lists.create", + data: { + accountId: self.accountId, + data: data, + }, + success: function(res) { + callback && callback(res.data); + }, + error: function(res) { + if (res.status == 404) { + callback([]); + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to create list: " + parsedError); + callback([]); + } + } + }); + }, + + listsAddEntry: function(data, callback) { + var self = this; + monster.request({ + resource: "lists.addEntry", + data: { + listId: data.list_id, + accountId: self.accountId, + data: { + number: data.number, + }, + }, + success: function(res) { + callback && callback(res.data); + }, + error: function(res) { + if (res.status == 404) { + callback([]); + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to add number to list: " + parsedError); + callback([]); + } + } + }); + }, + + listsUpdate: function(data, callback) { + var self = this; + var entrySaves = _.map(data.entries, function(entry) { + if (!entry.list_id) entry.list_id = data.id; + if (entry.state == 'pending-add') { + self.listsAddEntry(entry, function(data) { + //console.log("Added entry: "); + console.log(data); + }); + entry.state = 'saved'; + } else if ((entry.state == 'pending-delete') && entry.id) { + self.listsDeleteEntry(entry, function(data) { + //console.log("Deleted entry: "); + console.log(data); + }); + entry.state = 'deleted'; + } + return entry; + }); + console.log(entrySaves); + callback(entrySaves); + }, + + listsDeleteEntry: function(data, callback) { + var self = this; + + console.log("listsDeleteEntry input data:"); + console.log(data); + + monster.request({ + resource: "lists.deleteEntry", + data: { + accountId: self.accountId, + listId: data.list_id, + listEntryId: data.id, + }, + success: function(res) { + + callback && callback(res.data); + }, + error: function(res) { + if (res.status == 404) { + callback([]); + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to add number to list: " + parsedError); + callback([]); + } + } + }); + }, + + listsDelete: function(id, callback) { + var self = this; + + monster.request({ + resource: "lists.delete", + data: { + accountId: self.accountId, + listId: id, + }, + success: function(res) { + callback && callback(res.data); + }, + error: function(res) { + if (res.status == 404) { + callback([]); + } else if (res.status == 401) { + monster.util.logoutAndReload(); + } else { + monster.ui.alert("ERROR: Failed to add number to list: " + parsedError); + callback([]); + } + } + }); + } + + + + + }; + + return app; +}); diff --git a/src/apps/callflows/submodules/lists/views/addNumber.html b/src/apps/callflows/submodules/lists/views/addNumber.html new file mode 100644 index 0000000..bc6ac50 --- /dev/null +++ b/src/apps/callflows/submodules/lists/views/addNumber.html @@ -0,0 +1,9 @@ +
+ {{value.number}} + +
+ + + +
+
diff --git a/src/apps/callflows/submodules/lists/views/edit.html b/src/apps/callflows/submodules/lists/views/edit.html new file mode 100644 index 0000000..928ebb4 --- /dev/null +++ b/src/apps/callflows/submodules/lists/views/edit.html @@ -0,0 +1,62 @@ +
+
+
+

{{#if id}}{{ i18n.callflows.lists.edit }}{{else}}{{ i18n.callflows.lists.create }}{{/if}}

+
+ +
+
+
+
+ {{#if id}} +
+

{{name}}

+
+ +
+ {{else}} +
+ +
+ +
+ {{/if}} +
+ +
+

{{ i18n.callflows.lists.listNumbers }}

+
+
+
+ + {{ i18n.callflows.lists.addNumber }} +
+ +
+ + + {{ i18n.cancel }} +
+
+
+ +
+
+
+ +
+

{{ i18n.callflows.lists.help }}

+
+
+
+ + +
+ {{#if id}} + + {{/if}} + +
+
+
+