contactcats/ang/crmContactcats.js
Rich Lott / Artful Robot 8d0a9d0926 Various UI improvements
2025-02-28 12:37:58 +00:00

248 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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. <crm-reset-password token='foo' />
// 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')) {
// 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 (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';
}
/**
* 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._);