mirror of
https://codeberg.org/artfulrobot/contactcats.git
synced 2025-06-25 14:38:05 +02:00
Various UI improvements
This commit is contained in:
parent
bcbf5de9c4
commit
8d0a9d0926
7 changed files with 253 additions and 123 deletions
|
@ -7,6 +7,36 @@ use CRM_Contactcats_ExtensionUtil as E;
|
||||||
*/
|
*/
|
||||||
class CRM_Contactcats_Upgrader extends CRM_Extension_Upgrader_Base {
|
class CRM_Contactcats_Upgrader extends CRM_Extension_Upgrader_Base {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add presentation_order
|
||||||
|
*
|
||||||
|
* @return TRUE on success
|
||||||
|
* @throws CRM_Core_Exception
|
||||||
|
*/
|
||||||
|
public function upgrade_0001(): bool {
|
||||||
|
$this->ctx->log->info('Applying update 0001: add presentation_order');
|
||||||
|
CRM_Core_DAO::executeQuery(<<<SQL
|
||||||
|
ALTER TABLE civicrm_contact_category_definition
|
||||||
|
ADD `presentation_order` int(1) unsigned NOT NULL DEFAULT 10 COMMENT 'The order to present categories in, lowest number first.'
|
||||||
|
SQL);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add description field
|
||||||
|
*
|
||||||
|
* @return TRUE on success
|
||||||
|
* @throws CRM_Core_Exception
|
||||||
|
*/
|
||||||
|
public function upgrade_0002(): bool {
|
||||||
|
$this->ctx->log->info('Applying update 0001: add presentation_order');
|
||||||
|
CRM_Core_DAO::executeQuery(<<<SQL
|
||||||
|
ALTER TABLE civicrm_contact_category_definition
|
||||||
|
ADD `description` text DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// By convention, functions that look like "function upgrade_NNNN()" are
|
// By convention, functions that look like "function upgrade_NNNN()" are
|
||||||
// upgrade tasks. They are executed in order (like Drupal's hook_update_N).
|
// upgrade tasks. They are executed in order (like Drupal's hook_update_N).
|
||||||
|
|
||||||
|
|
|
@ -36,13 +36,30 @@ crm-contact-category-settings .panel {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
crm-contact-category-settings .panel-body {
|
crm-contact-category-settings .panel-body {
|
||||||
display: flex;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0.5em 0;
|
display: grid;
|
||||||
align-items: center;
|
gap: 1rem;
|
||||||
justify-content: space-between;
|
grid-template-columns:1fr;
|
||||||
gap: 1em;
|
grid-template-rows: auto auto auto;
|
||||||
}
|
}
|
||||||
|
.crm-container crm-contact-category-settings .panel-body {
|
||||||
|
padding: var(--crm-padding-small) var(--crm-padding-reg);
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 800px) {
|
||||||
|
crm-contact-category-settings .panel-body {
|
||||||
|
grid-template-columns: minmax(16ch,max-content) 1fr auto;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crm-contact-category-settings .contactcats-label {
|
||||||
|
}
|
||||||
|
crm-contact-category-settings .contactcats-description {
|
||||||
|
}
|
||||||
|
crm-contact-category-settings .contactcats-actions {
|
||||||
|
}
|
||||||
|
|
||||||
crm-contact-category-settings li label {
|
crm-contact-category-settings li label {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,54 +23,85 @@
|
||||||
this.$onInit = async function() {
|
this.$onInit = async function() {
|
||||||
ctrl.dirty = 'pristine'; //pristine|dirty
|
ctrl.dirty = 'pristine'; //pristine|dirty
|
||||||
ctrl.view = 'list';
|
ctrl.view = 'list';
|
||||||
|
ctrl.uxOrderField = 'execution_order';
|
||||||
ctrl.moveIdx = null;
|
ctrl.moveIdx = null;
|
||||||
ctrl.categoryToEdit = null;
|
ctrl.categoryToEdit = null;
|
||||||
ctrl.categoryDefinitions = null;
|
ctrl.presentation_order = [];
|
||||||
ctrl.categoryDefinitions = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { execution_order: 'ASC' }, withLabels: true });
|
ctrl.execution_order = [];
|
||||||
if (ctrl.categoryDefinitions.count === 0) {
|
const blankDefn = {
|
||||||
// First time.
|
|
||||||
ctrl.categoryDefinitions = [];
|
|
||||||
}
|
|
||||||
// Ensure we have the minimum of what we need.
|
|
||||||
if (!ctrl.categoryDefinitions.find(c => c.search_type === 'default')) {
|
|
||||||
// There's no default one.
|
|
||||||
ctrl.categoryDefinitions.push({
|
|
||||||
id: 0,
|
id: 0,
|
||||||
execution_order: ctrl.categoryDefinitions.length,
|
execution_order: 10,
|
||||||
label: '9999 ' + ts('Other'),
|
presentation_order: 10,
|
||||||
search_type: 'default',
|
label:'',
|
||||||
|
search_type: 'search',
|
||||||
search_data: {},
|
search_data: {},
|
||||||
color: '#666666',
|
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',
|
icon: 'fa-person-circle-question',
|
||||||
description: ts('Any contact not matching any of the categories is assigned this category.'),
|
description: ts('Any contact not matching any of the categories is assigned this category.'),
|
||||||
})
|
}));
|
||||||
}
|
}
|
||||||
updateOrders();
|
// Ensure the fields are all present in the data.
|
||||||
console.log("Got", ctrl.categoryDefinitions);
|
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();
|
$scope.$digest();
|
||||||
|
|
||||||
|
// Functions follow.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy execution to presenation or vice versa.
|
||||||
|
*/
|
||||||
|
ctrl.copyOrder = (from) => {
|
||||||
|
ctrl[ctrl.uxOrderField] = ctrl[from].slice();
|
||||||
|
updateOrders();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation order is by label. Promoting "0123 label" type labels.
|
|
||||||
*
|
|
||||||
* We set execution_order to the array index to keep things simpler.
|
* We set execution_order to the array index to keep things simpler.
|
||||||
*/
|
*/
|
||||||
function updateOrders() {
|
function updateOrders() {
|
||||||
|
// Update execution_order or presentation_order field to match the specified order.
|
||||||
// re-set execution_order.
|
ctrl[ctrl.uxOrderField].forEach((item, idx) => {
|
||||||
ctrl.categoryDefinitions.forEach((item, idx) => {
|
item[ctrl.uxOrderField] = 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin editing something
|
||||||
|
*/
|
||||||
ctrl.edit = idx => {
|
ctrl.edit = idx => {
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
// New item.
|
// New item.
|
||||||
|
@ -82,74 +113,47 @@
|
||||||
conv = (x) => ('0' + Math.ceil((x-min)/range * 128).toString(16)).replace(/^.*(..)$/, '$1');
|
conv = (x) => ('0' + Math.ceil((x-min)/range * 128).toString(16)).replace(/^.*(..)$/, '$1');
|
||||||
|
|
||||||
// Create a blank
|
// Create a blank
|
||||||
ctrl.categoryToEdit = {
|
ctrl.categoryToEdit = Object.assign({}, blankDefn, {
|
||||||
id: 0,
|
|
||||||
execution_order: -1,
|
execution_order: -1,
|
||||||
label: '',
|
presentation_order: -1,
|
||||||
search_type: 'search',
|
search_type: 'search',
|
||||||
search_data: { saved_search_id: null },
|
search_data: { saved_search_id: null },
|
||||||
color: '#' + [r, g, b].map(conv).join(''),
|
color: '#' + [r, g, b].map(conv).join(''),
|
||||||
icon: '',
|
});
|
||||||
description: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ctrl.categoryToEdit = Object.assign({}, JSON.parse(JSON.stringify(ctrl.categoryDefinitions[idx])));
|
ctrl.categoryToEdit = Object.assign({}, JSON.parse(JSON.stringify(catDefs[idx])));
|
||||||
}
|
}
|
||||||
ctrl.view = 'edit';
|
ctrl.view = 'edit';
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.moveTo = idx => {
|
/**
|
||||||
let item = ctrl.categoryDefinitions.splice(ctrl.moveIdx, 1)[0];
|
* End editing something
|
||||||
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 = () => {
|
ctrl.updateEditedThing = () => {
|
||||||
const edited = ctrl.categoryToEdit;
|
const edited = ctrl.categoryToEdit;
|
||||||
|
|
||||||
// @todo validate, e.g.
|
// @todo validate, e.g.
|
||||||
if (!edited.label) {
|
if (!edited.label) {
|
||||||
alert("No name");
|
alert("No name");
|
||||||
return;
|
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};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edited.execution_order === -1) {
|
if (edited.execution_order === -1) {
|
||||||
// This is a new category, we need to insert it before the default one.
|
// This is a new category, we need to insert it before the default one.
|
||||||
const newIdx = ctrl.categoryDefinitions.length - 1;
|
const newIdx = ctrl.execution_order.length - 1;
|
||||||
ctrl.categoryDefinitions.splice(newIdx, 0, edited);
|
edited.presentation_order = newIdx;
|
||||||
|
edited.execution_order = newIdx;
|
||||||
|
ctrl.execution_order.splice(newIdx, 0, edited);
|
||||||
|
ctrl.presentation_order.splice(newIdx, 0, edited);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ctrl.categoryDefinitions[edited.execution_order] = edited;
|
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;
|
ctrl.categoryToEdit = null;
|
||||||
updateOrders();
|
updateOrders();
|
||||||
|
@ -157,38 +161,76 @@
|
||||||
ctrl.view = 'list';
|
ctrl.view = 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to ensure the search_data object contains the fields required for the selected search_type
|
/**
|
||||||
|
* 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 = () => {
|
ctrl.fixSearchData = () => {
|
||||||
const search_data = ctrl.categoryToEdit.search_data;
|
const search_data = ctrl.categoryToEdit.search_data;
|
||||||
if (ctrl.categoryToEdit.search_type === 'group') {
|
if (ctrl.categoryToEdit.search_type === 'group') {
|
||||||
if (!('group_id' in search_data)) {
|
Object.assign(search_data, {group_id: null});
|
||||||
search_data.group_id = null;
|
let {group_id} = search_data;
|
||||||
}
|
ctrl.categoryToEdit.search_data = {group_id};
|
||||||
}
|
}
|
||||||
else if (ctrl.categoryToEdit.search_type === 'search') {
|
else if (ctrl.categoryToEdit.search_type === 'search') {
|
||||||
if (!('saved_search_id' in search_data)) {
|
Object.assign(search_data, {saved_search_id: null});
|
||||||
search_data.saved_search_id = null;
|
let {saved_search_id} = search_data;
|
||||||
}
|
ctrl.categoryToEdit.search_data = {saved_search_id};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.save = () => {
|
ctrl.save = () => {
|
||||||
if (!confirm(ts("Confirm saving changes to categories? Note that categories will not be fully applied until tomorrow."))) { return; }
|
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.
|
// Handle deletions first.
|
||||||
const deletedIds = ctrl.categoryDefinitions.filter(d => d.deleted && d.id > 0).map(d => d.id);
|
const deletedIds = records.filter(d => d.deleted && d.id > 0).map(d => d.id);
|
||||||
|
|
||||||
const chain = Promise.resolve();
|
const chain = Promise.resolve();
|
||||||
if (deletedIds.length) {
|
if (deletedIds.length) {
|
||||||
chain.then(() => crmApi4('ContactCategoryDefinition', 'delete', { where: [ ['id', 'IN', deletedIds] ] }));
|
chain.then(() => crmApi4('ContactCategoryDefinition', 'delete', { where: [ ['id', 'IN', deletedIds] ] }));
|
||||||
}
|
}
|
||||||
// Now enact the deletions on our local model.
|
// Now enact the deletions on our local models
|
||||||
ctrl.categoryDefinitions = ctrl.categoryDefinitions.filter(d => !d.deleted);
|
ctrl.presentation_order = ctrl.presentation_order.filter(d => !d.deleted);
|
||||||
// Tidy execution_order
|
ctrl.execution_order = ctrl.execution_order.filter(d => !d.deleted);
|
||||||
|
|
||||||
updateOrders();
|
updateOrders();
|
||||||
if (ctrl.categoryDefinitions.length) {
|
if (ctrl.presentation_order.length) {
|
||||||
chain.then(() => crmApi4('ContactCategoryDefinition', 'save', {records:ctrl.categoryDefinitions}));
|
// Save if anything to save.
|
||||||
|
chain.then(() => crmApi4('ContactCategoryDefinition', 'save', {records}));
|
||||||
}
|
}
|
||||||
chain.then(() => {
|
chain.then(() => {
|
||||||
ctrl.dirty = 'pristine';
|
ctrl.dirty = 'pristine';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<div id=bootstrap-theme>
|
<div id=bootstrap-theme>
|
||||||
<div ng-if="$ctrl.categoryDefinitions === null">
|
<div ng-if="$ctrl.presentation_order === null">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<form ng-if="$ctrl.categoryDefinitions"
|
<form ng-if="$ctrl.presentation_order"
|
||||||
ng-show="$ctrl.view === 'list'"
|
ng-show="$ctrl.view === 'list'"
|
||||||
crm-ui-id-scope
|
crm-ui-id-scope
|
||||||
>
|
>
|
||||||
|
@ -13,20 +13,50 @@
|
||||||
longer title/description. "regular givers who..."
|
longer title/description. "regular givers who..."
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<h2>{{ts('Execution order')}}</h2>
|
<div class=help >{{ts('Each contact is assigned the first category that matches, using the assignment order. When categories are listed, the presentation order is used.')}}</div>
|
||||||
<p>{{ts('A contact is assigned the first category that matches according to the order defined here. Categories are presented in order of their label - see list below - so you may want to consider this when naming your categories.')}}</p>
|
|
||||||
|
|
||||||
|
<div crm-ui-field="{name: 'cc.label', title: ts('Show')}" >
|
||||||
|
<input
|
||||||
|
crm-ui-id="ci.showOrder-execution"
|
||||||
|
type=radio
|
||||||
|
class=crm-form-radio
|
||||||
|
name="useOrder"
|
||||||
|
ng-model="$ctrl.uxOrderField"
|
||||||
|
value="execution_order"
|
||||||
|
ng-disabled="$ctrl.moveIdx !== null"
|
||||||
|
/>
|
||||||
|
<label crm-ui-for="ci.showOrder-execution">{{ts('Assignment order')}}</label>
|
||||||
|
<input
|
||||||
|
crm-ui-id="ci.showOrder-presentation"
|
||||||
|
type=radio
|
||||||
|
class=crm-form-radio
|
||||||
|
name="useOrder"
|
||||||
|
ng-model="$ctrl.uxOrderField"
|
||||||
|
value="presentation_order"
|
||||||
|
ng-disabled="$ctrl.moveIdx !== null"
|
||||||
|
/>
|
||||||
|
<label crm-ui-for="ci.showOrder-presentation">{{ts('Presentation order')}}</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 ng-if="$ctrl.uxOrderField === 'execution_order'" >{{ts('Assignment order')}}</h2>
|
||||||
|
<h2 ng-if="$ctrl.uxOrderField === 'presentation_order'" >{{ts('Presentation order')}}</h2>
|
||||||
|
<p ng-if="$ctrl.uxOrderField === 'execution_order'" ><button class="btn btn-secondary" ng-click="$ctrl.copyOrder('presentation_order')" >{{ts('Copy from Presentation Order')}}</button></p>
|
||||||
|
<p ng-if="$ctrl.uxOrderField === 'presentation_order'" ><button class="btn btn-secondary" ng-click="$ctrl.copyOrder('execution_order')" >{{ts('Copy from Assignment Order')}}</button></p>
|
||||||
<ol class="crm-catmap {{$ctrl.moveIdx !== null ? 'moving' : ''}}">
|
<ol class="crm-catmap {{$ctrl.moveIdx !== null ? 'moving' : ''}}">
|
||||||
<li ng-repeat="(idx, row) in $ctrl.categoryDefinitions" class="{{idx === $ctrl.moveIdx ? 'being-moved' : ''}} {{row.deleted ? 'deleted' : ''}}">
|
<li ng-repeat="(idx, row) in $ctrl[$ctrl.uxOrderField]" class="{{idx === $ctrl.moveIdx ? 'being-moved' : ''}} {{row.deleted ? 'deleted' : ''}}">
|
||||||
<div class=panel>
|
<div class=panel>
|
||||||
<div class=panel-body>
|
<div class=panel-body>
|
||||||
<span style="color: {{row.color ? row.color : 'inherit'}};" >
|
<span class="contactcats-label" style="color: {{row.color ? row.color : 'inherit'}};" >
|
||||||
<i ng-if="row.icon" class="crm-i {{row.icon}}" ></i>
|
<i ng-if="row.icon" class="crm-i {{row.icon}}" ></i>
|
||||||
{{row.label}}
|
{{row.label}}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="contactcats-description" >
|
||||||
|
{{row.description}}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- button group? -->
|
<!-- button group? -->
|
||||||
<span>
|
<span class=contactcats-actions>
|
||||||
<button ng-if="$ctrl.moveIdx === null && !row.deleted" ng-click="$ctrl.edit(idx)" >
|
<button ng-if="$ctrl.moveIdx === null && !row.deleted" ng-click="$ctrl.edit(idx)" >
|
||||||
<i class="fa-pencil crm-i"></i> {{ts('Edit')}}
|
<i class="fa-pencil crm-i"></i> {{ts('Edit')}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -38,7 +68,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button ng-if="$ctrl.moveIdx === null && !row.deleted && row.search_type !== 'default'" ng-click="$ctrl.moveIdx = idx"
|
<button ng-if="$ctrl.moveIdx === null && !row.deleted && row.search_type !== 'default'" ng-click="$ctrl.moveIdx = idx"
|
||||||
class="btn"
|
class="btn"
|
||||||
ng-disabled="$ctrl.categoryDefinitions.length === 1"
|
ng-disabled="$ctrl.presentation_order.length === 1"
|
||||||
>
|
>
|
||||||
<i class="fa-arrows-up-down crm-i"></i> {{ts('Move')}}
|
<i class="fa-arrows-up-down crm-i"></i> {{ts('Move')}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -50,8 +80,8 @@
|
||||||
<button ng-click="$ctrl.moveTo(idx)" ng-show="$ctrl.moveIdx !== null && (idx === 0 && $ctrl.moveIdx > 0)" ><i class="fa-sort-up crm-i" ></i>{{ts('Before this')}}</button>
|
<button ng-click="$ctrl.moveTo(idx)" ng-show="$ctrl.moveIdx !== null && (idx === 0 && $ctrl.moveIdx > 0)" ><i class="fa-sort-up crm-i" ></i>{{ts('Before this')}}</button>
|
||||||
<button ng-click="$ctrl.moveTo(idx+1)" ng-show="$ctrl.moveIdx !== null && row.search_type !== 'default' && (idx + 1 < $ctrl.moveIdx || idx > $ctrl.moveIdx)"><i class="fa-sort-down crm-i" ></i>{{ts('After this')}}</button>
|
<button ng-click="$ctrl.moveTo(idx+1)" ng-show="$ctrl.moveIdx !== null && row.search_type !== 'default' && (idx + 1 < $ctrl.moveIdx || idx > $ctrl.moveIdx)"><i class="fa-sort-down crm-i" ></i>{{ts('After this')}}</button>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- lower idx : {{idx}} moveIdx {{$ctrl.moveIdx}} -->
|
<!-- lower idx : {{idx}} moveIdx {{$ctrl.moveIdx}} -->
|
||||||
<!-- <div class="crm-contact-cats-move-target" ng-show="($ctrl.moveIdx < idx) || (idx +1 < $ctrl.moveIdx)" > -->
|
<!-- <div class="crm-contact-cats-move-target" ng-show="($ctrl.moveIdx < idx) || (idx +1 < $ctrl.moveIdx)" > -->
|
||||||
<!-- <button ng-click="$ctrl.moveTo(idx+1)" ><i class="fa-right-long crm-i" ></i>{{ts('Move to here')}}</button> -->
|
<!-- <button ng-click="$ctrl.moveTo(idx+1)" ><i class="fa-right-long crm-i" ></i>{{ts('Move to here')}}</button> -->
|
||||||
|
@ -72,24 +102,13 @@
|
||||||
Categories saved. Contacts will be updated shortly (when the next Scheduled
|
Categories saved. Contacts will be updated shortly (when the next Scheduled
|
||||||
Job run happens).
|
Job run happens).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Presentation order</h2>
|
|
||||||
<ol style="padding-left: 3ch;list-style: decimal;">
|
|
||||||
<li ng-repeat="(idx, row) in $ctrl.presentationOrder" >
|
|
||||||
<span style="color: {{row.color ? row.color : 'inherit'}};" >
|
|
||||||
<i ng-if="row.icon" class="crm-i {{row.icon}}" ></i>
|
|
||||||
{{row.label}}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<form ng-show="$ctrl.view === 'edit'"
|
<form ng-show="$ctrl.view === 'edit'"
|
||||||
crm-ui-id-scope>
|
crm-ui-id-scope>
|
||||||
<div>
|
<div>
|
||||||
<h2 ng-if="$ctrl.categoryDefinitions.length > $ctrl.categoryToEdit.idx">{{ts('Edit category %1', {1 : $ctrl.categoryDefinitions[$ctrl.categoryToEdit.idx].label })}}</h2>
|
<h2 ng-if="$ctrl.presentation_order.length > $ctrl.categoryToEdit.idx">{{ts('Edit category %1', {1 : $ctrl.[$ctrl.uxOrderField][$ctrl.categoryToEdit[$ctrl.uxOrderField]].label })}}</h2>
|
||||||
<h2 ng-if="$ctrl.categoryDefinitions.length == $ctrl.categoryToEdit.idx" >{{ts('Add new category')}}</h2>
|
<h2 ng-if="$ctrl.presentation_order.length == $ctrl.categoryToEdit.idx" >{{ts('Add new category')}}</h2>
|
||||||
|
|
||||||
<div crm-ui-field="{name: 'cc.label', title: ts('Category label'), required: 1}" >
|
<div crm-ui-field="{name: 'cc.label', title: ts('Category label'), required: 1}" >
|
||||||
<input
|
<input
|
||||||
|
|
2
info.xml
2
info.xml
|
@ -15,7 +15,7 @@
|
||||||
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
|
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
|
||||||
</urls>
|
</urls>
|
||||||
<releaseDate>2024-02-27</releaseDate>
|
<releaseDate>2024-02-27</releaseDate>
|
||||||
<version>1.0</version>
|
<version>1.1</version>
|
||||||
<develStage>alpha</develStage>
|
<develStage>alpha</develStage>
|
||||||
<compatibility>
|
<compatibility>
|
||||||
<ver>5.70</ver>
|
<ver>5.70</ver>
|
||||||
|
|
|
@ -57,7 +57,11 @@ return [
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'description' => E::ts('Shows a list of all categories and how many contacts are in each one.'),
|
'description' => E::ts('Shows a list of all categories and how many contacts are in each one.'),
|
||||||
'sort' => [
|
'sort' => [
|
||||||
['label', 'ASC'],
|
[
|
||||||
|
'presentation_order',
|
||||||
|
'ASC',
|
||||||
|
],
|
||||||
|
['color', 'ASC'],
|
||||||
],
|
],
|
||||||
'limit' => 50,
|
'limit' => 50,
|
||||||
'pager' => FALSE,
|
'pager' => FALSE,
|
||||||
|
|
|
@ -33,6 +33,17 @@ return [
|
||||||
'label' => E::ts('Category name'),
|
'label' => E::ts('Category name'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'description' => [
|
||||||
|
'title' => E::ts('Description'),
|
||||||
|
'sql_type' => 'text',
|
||||||
|
'input_type' => 'TextArea',
|
||||||
|
'required' => FALSE,
|
||||||
|
'input_attrs' => [
|
||||||
|
'note_columns' => 60,
|
||||||
|
'note_rows' => 3,
|
||||||
|
'label' => E::ts('Category description'),
|
||||||
|
],
|
||||||
|
],
|
||||||
'search_type' => [
|
'search_type' => [
|
||||||
'title' => E::ts('Definition type'),
|
'title' => E::ts('Definition type'),
|
||||||
'sql_type' => 'varchar(12)',
|
'sql_type' => 'varchar(12)',
|
||||||
|
@ -77,5 +88,12 @@ return [
|
||||||
'default' => '10',
|
'default' => '10',
|
||||||
'required' => TRUE,
|
'required' => TRUE,
|
||||||
],
|
],
|
||||||
|
'presentation_order' => [
|
||||||
|
'title' => E::ts('Presentation order'),
|
||||||
|
'description' => E::ts('What order should these be presented in?'),
|
||||||
|
'sql_type' => 'int(1) unsigned',
|
||||||
|
'default' => '10',
|
||||||
|
'required' => TRUE,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue