(function(angular, $, _) { // Declare a list of dependencies. angular.module("crmContactcats", CRM.angRequires("crmContactcats")); angular.module("crmContactcats").component("crmContactCategorySettings", { templateUrl: "~/crmContactcats/crmContactCategorySettings.html", bindings: { // things listed here become properties on the controller using // values from attributes. @ means a fixed string is passed // e.g. // token: '@', // & is special. In the child, call ctrl.onMyAction with an // Object whose keys provide parameter names for the code in the parent. // onMyAction: '&', // '<' means one-way binding (parent»child) and I think is what you are // supposed to use for components. // v: '<' }, controller: function($scope, $timeout, crmApi4, crmStatus, $document) { var ts = ($scope.ts = CRM.ts(null)), ctrl = this; // this.$onInit gets run after the this controller is called, and after the bindings have been applied. this.$onInit = async function() { ctrl.dirty = 'pristine'; //pristine|dirty ctrl.view = 'list'; ctrl.uxOrderField = 'execution_order'; ctrl.moveIdx = null; ctrl.categoryToEdit = null; ctrl.presentation_order = []; ctrl.execution_order = []; const blankDefn = { id: 0, execution_order: 10, presentation_order: 10, label:'', search_type: 'search', search_data: {}, color: '#666666', icon: 'fa-circle-half-stroke', description: '', }; catDefs = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { execution_order: 'ASC' }, withLabels: true }); if (catDefs.count === 0) { // First time. catDefs = []; } // Ensure we have the minimum of what we need. if (!catDefs.find(c => c.search_type === 'default')) { console.log("Adding default category as was missing"); // There's no default one. catDefs.push(Object.assign({}, blankDefn, { execution_order: catDefs.length, presentation_order: catDefs.length, label: ts('Default'), search_type: 'default', icon: 'fa-person-circle-question', description: ts('Any contact not matching any of the categories is assigned this category.'), })); } // Ensure the fields are all present in the data. catDefs.forEach((item, idx) => { catDefs[idx] = Object.assign({}, blankDefn, item); }); // Create ordered arrays. ['presentation_order', 'execution_order'].forEach(orderField => { ctrl[orderField] = catDefs.slice(); ctrl[orderField].sort((a, b) => { // console.log({orderField, aST: a.search_type, bST: b.search_type, aOrd:a[orderField], bOrd:b[orderField], aLab:a.label, bLab:b.label}); if (orderField === 'execution_order') { // Enforce default at end for assignment order if (a.search_type === 'default' && b.search_type !== 'default') return 1; if (b.search_type === 'default' && a.search_type !== 'default') return -1; } if (a[orderField] > b[orderField]) return 1; if (a[orderField] < b[orderField]) return -1; if (a.label < b.label) return 1; if (a.label > b.label) return -1; return 0; }); }); updateOrders(); $scope.$digest(); // Functions follow. /** * Copy execution to presenation or vice versa. */ ctrl.copyOrder = (from) => { ctrl[ctrl.uxOrderField] = ctrl[from].slice(); updateOrders(); }; /** * We set execution_order to the array index to keep things simpler. */ function updateOrders() { // Update execution_order or presentation_order field to match the specified order. ctrl[ctrl.uxOrderField].forEach((item, idx) => { item[ctrl.uxOrderField] = idx; }); } /** * Begin editing something */ ctrl.edit = idx => { if (idx === -1) { // New item. // Create a random dark colour let [r, g, b] = [Math.random(), Math.random(), Math.random()], min = Math.min(r, g, b), range = Math.max(r, g, b) - min, conv = (x) => ('0' + Math.ceil((x-min)/range * 128).toString(16)).replace(/^.*(..)$/, '$1'); // Create a blank ctrl.categoryToEdit = Object.assign({}, blankDefn, { execution_order: -1, presentation_order: -1, search_type: 'search', search_data: { saved_search_id: null }, color: '#' + [r, g, b].map(conv).join(''), }); } else { ctrl.categoryToEdit = Object.assign({}, JSON.parse(JSON.stringify(catDefs[idx]))); } ctrl.view = 'edit'; }; /** * End editing something */ ctrl.updateEditedThing = () => { const edited = ctrl.categoryToEdit; // @todo validate, e.g. if (!edited.label) { alert("No name"); return; } if (edited.execution_order === -1) { // This is a new category, we need to insert it before the default one. const newIdx = ctrl.execution_order.length - 1; edited.presentation_order = newIdx; edited.execution_order = newIdx; ctrl.execution_order.splice(newIdx, 0, edited); ctrl.presentation_order.splice(newIdx, 0, edited); } else { console.log("Update:", ctrl.uxOrderField); const targetCat = ctrl[ctrl.uxOrderField][edited[ctrl.uxOrderField]]; Object.keys(blankDefn).forEach(k => { console.log("Setting", k, "to ", edited[k], 'on item', targetCat); targetCat[k] = edited[k]; }); } ctrl.categoryToEdit = null; updateOrders(); ctrl.dirty = 'dirty'; ctrl.view = 'list'; } ctrl.canMoveBelow = function(row, idx) { // Only show move buttons when moving. if (ctrl.moveIdx === null) return false; if (ctrl.uxOrderField === 'execution_order') { // The default MUST be the last item in execution_order. if (row.search_type === 'default') return false; } return (idx + 1 < ctrl.moveIdx || idx > ctrl.moveIdx); }; /** * Move a category within the currently selected order. */ ctrl.moveTo = idx => { let item = ctrl[ctrl.uxOrderField].splice(ctrl.moveIdx, 1)[0]; if (idx > ctrl.moveIdx) { idx--; } ctrl[ctrl.uxOrderField].splice(idx, 0, item); ctrl.moveIdx = null; updateOrders(); ctrl.dirty = 'dirty'; }; /** * Mark a category for deletion. */ ctrl.deleteCategory = idx => { if (!confirm(ts( 'Confirm deleting category ‘%1’. You will lose history related to this category. Sure?', {1: catDefs[idx].label} ))) { return; } ctrl[ctrl.uxOrderField][idx].deleted = true; ctrl.categoryToEdit = null; updateOrders(); ctrl.view = 'list'; ctrl.dirty = 'dirty'; }; /** * We need to ensure the search_data object contains the * expected fields and no more. */ ctrl.fixSearchData = () => { const search_data = ctrl.categoryToEdit.search_data; if (ctrl.categoryToEdit.search_type === 'group') { Object.assign(search_data, {group_id: null}); let {group_id} = search_data; ctrl.categoryToEdit.search_data = {group_id}; } else if (ctrl.categoryToEdit.search_type === 'search') { Object.assign(search_data, {saved_search_id: null}); let {saved_search_id} = search_data; ctrl.categoryToEdit.search_data = {saved_search_id}; } }; ctrl.save = () => { if (!confirm(ts("Confirm saving changes to categories? Note that categories will not be fully applied until tomorrow."))) { return; } const records = ctrl[ctrl.uxOrderField]; // Handle deletions first. const deletedIds = records.filter(d => d.deleted && d.id > 0).map(d => d.id); const chain = Promise.resolve(); if (deletedIds.length) { chain.then(() => crmApi4('ContactCategoryDefinition', 'delete', { where: [ ['id', 'IN', deletedIds] ] })); } // Now enact the deletions on our local models ctrl.presentation_order = ctrl.presentation_order.filter(d => !d.deleted); ctrl.execution_order = ctrl.execution_order.filter(d => !d.deleted); updateOrders(); if (ctrl.presentation_order.length) { // Save if anything to save. chain.then(() => crmApi4('ContactCategoryDefinition', 'save', {records})); } chain.then(() => { ctrl.dirty = 'pristine'; }) crmStatus({ start: ts('Saving...'), success: ts('Saved')}, chain); }; }; // this.$onChange = function(changes) { // // changes is object keyed by name what '<' binding changed in // // the parent (e.g. 'v'), and value is another obj with keys // // something like previous, new, ...?... // }; } }); })(angular, CRM.$, CRM._);