(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.moveIdx = null; ctrl.categoryToEdit = null; ctrl.categoryDefinitions = null; ctrl.categoryDefinitions = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { label: 'ASC' }, withLabels: true }); updateOrders(); console.log("Got", ctrl.categoryDefinitions); $scope.$digest(); /** * Presentation order is by label. Promoting "0123 label" type labels. * * We set execution_order to the array index to keep things simpler. */ function updateOrders() { // re-set execution_order. ctrl.categoryDefinitions.forEach((item, idx) => { item.execution_order = idx; }); const po = ctrl.categoryDefinitions.slice(); po.sort((a, b) => { if (a.label < b.label) return -1; if (a.label > b.label) return 1; return (a.execution_order ?? 1) - (b.execution_order ?? 1); }); ctrl.presentationOrder = po; } 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 = { id: 0, execution_order: ctrl.categoryDefinitions.length, label: '', search_type: 'search', search_data: { saved_search_id: null }, color: '#' + [r, g, b].map(conv).join(''), icon: '', description: '', }; } else { ctrl.categoryToEdit = Object.assign({}, JSON.parse(JSON.stringify(ctrl.categoryDefinitions[idx]))); } ctrl.view = 'edit'; }; ctrl.moveTo = idx => { let item = ctrl.categoryDefinitions.splice(ctrl.moveIdx, 1)[0]; if (idx > ctrl.moveIdx) { idx--; } ctrl.categoryDefinitions.splice(idx, 0, item); ctrl.moveIdx = null; updateOrders(); $ctrl.dirty = 'dirty'; }; ctrl.deleteCategory = idx => { if (!confirm(ts( 'Confirm deleting category ‘%1’. You will lose history related to this category. Sure?', {1: ctrl.categoryDefinitions[idx].label} ))) { return; } ctrl.categoryDefinitions[idx].deleted = true; ctrl.categoryToEdit = null; updateOrders(); ctrl.view = 'list'; ctrl.dirty = 'dirty'; }; ctrl.updateEditedThing = () => { const edited = ctrl.categoryToEdit; // @todo validate, e.g. if (!edited.label) { alert("No name"); return; } const search_data = edited.search_data; if (edited.search_type === 'group') { // Only store what we need. const {group_id} = search_data; edited.search_data = {group_id}; } else if (edited.search_type === 'search') { const {saved_search_id} = search_data; edited.search_data = {saved_search_id}; } const idx = edited.execution_order; ctrl.categoryDefinitions[idx] = edited; ctrl.categoryToEdit = null; updateOrders(); ctrl.dirty = 'dirty'; ctrl.view = 'list'; } // We need to ensure the search_data object contains the fields required for the selected search_type ctrl.fixSearchData = () => { const search_data = ctrl.categoryToEdit.search_data; if (ctrl.categoryToEdit.search_type === 'group') { if (!('group_id' in search_data)) { search_data.group_id = null; } } else if (ctrl.categoryToEdit.search_type === 'search') { if (!('saved_search_id' in search_data)) { search_data.saved_search_id = null; } } }; ctrl.save = () => { if (!confirm(ts("Confirm saving changes to categories? Note that categories will not be fully applied until tomorrow."))) { return; } // Handle deletions first. const deletedIds = ctrl.categoryDefinitions.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 model. ctrl.categoryDefinitions = ctrl.categoryDefinitions.filter(d => !d.deleted); // Tidy execution_order updateOrders(); if (ctrl.categoryDefinitions.length) { chain.then(() => crmApi4('ContactCategoryDefinition', 'save', {records:ctrl.categoryDefinitions})); } 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._);