Main edit interface functinonal except save and execution order

This commit is contained in:
Rich Lott / Artful Robot 2025-02-20 19:38:42 +00:00
parent 4978eeb828
commit b0983603e5
6 changed files with 346 additions and 72 deletions

View file

@ -2,10 +2,44 @@
crm-contact-category-settings ol {
padding: 0;
}
crm-contact-category-settings ol li {
crm-contact-category-settings li.deleted {
opacity: 0.3;
}
/* @keyframes crm-contact-cats-move { */
/* 0%, 90% {transform: none;} */
/* 95% {transform: translateX(0.2em);} */
/* 100% {transform: none;} */
/* } */
crm-contact-category-settings .crm-contact-cats-move-target {
transition: opacity 0.3s ease, transform 0.3s ease, display 0.001s allow-discrete, max-height 0.3s ease;
transform-origin: top left;
display: none;
}
crm-contact-category-settings .moving .crm-contact-cats-move-target {
display: block;
transform: none;
opacity: 1;
max-height: 5rem;
@starting-style {
transform: scaleY(0);
opacity:0;
max-height: 0;
}
}
crm-contact-category-settings li.being-moved .panel {
opacity: 0.8;
}
crm-contact-category-settings .panel {
margin-bottom: 0;
}
crm-contact-category-settings .panel-body {
display: flex;
box-sizing: border-box;
padding: 0.5em 0;
align-items: flex-end;
align-items: center;
justify-content: space-between;
gap: 1em;
}
crm-contact-category-settings li label {

View file

@ -22,11 +22,109 @@
// this.$onInit gets run after the this controller is called, and after the bindings have been applied.
this.$onInit = async function() {
ctrl.saved = false;
ctrl.view = 'list';
ctrl.moveIdx = null;
ctrl.categoryToEdit = null;
ctrl.categoryDefinitions = null;
ctrl.categoryDefinitions = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { label: 'ASC' }, withLabels: true });
$scope.$digest();
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 = {
idx: 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({idx}, 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;
};
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;
ctrl.view = 'list';
};
ctrl.updateEditedThing = () => {
// @todo validate, e.g.
if (!ctrl.categoryToEdit.label) {
alert("No name");
return;
}
const search_data = ctrl.categoryToEdit.search_data;
console.log("search_data", search_data);
if (ctrl.categoryToEdit.search_type === 'group') {
// Only store what we need.
const {group_id} = search_data;
console.log("group_id", group_id, search_data);
ctrl.categoryToEdit.search_data = {group_id};
}
else if (ctrl.categoryToEdit.search_type === 'search') {
const {saved_search_id} = search_data;
ctrl.categoryToEdit.search_data = {saved_search_id};
}
const edited = ctrl.categoryToEdit;
const idx = edited.idx;
delete(edited.idx);
ctrl.categoryDefinitions[idx] = edited;
ctrl.categoryToEdit = null;
ctrl.view = 'list';
console.log("done editing");
}
// 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 = async () => {
if (!confirm(ts("Confirm saving changes to categories? Note that categories will not be fully applied until tomorrow."))) { return; }
};
// ctrl.deleteRow = idx => {
// ctrl.catmap.splice(idx, 1);
// };

View file

@ -1,11 +1,9 @@
<div ng-if="$ctrl.categoryDefinitions === null">
Loading...
</div>
<form ng-if="$ctrl.categoryDefinitions" crm-ui-id-scope>
<button ng-click="$ctrl.catmap.unshift({groupID: '', name:''})">
<i class="crm-i fa-plus"></i> Add category
</button>
<form ng-if="$ctrl.categoryDefinitions"
ng-show="$ctrl.view === 'list'"
crm-ui-id-scope>
<!-- I can see use of
presentation order/grouping
@ -13,27 +11,139 @@
longer title/description. "regular givers who..."
-->
<ol class="crm-catmap">
<li ng-repeat="(idx, row) in $ctrl.categoryDefinitions">
<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' : ''}}">
<!-- higher idx : {{idx}} moveIdx {{$ctrl.moveIdx}} -->
<div class="crm-contact-cats-move-target" ng-show="(idx === 0 && $ctrl.moveIdx > 0)" >
<button ng-click="$ctrl.moveTo(0)" >{{ts('Move to here')}}</button>
</div>
<div class=panel>
<div class=panel-body>
<span style="color: {{row.color ? row.color : 'inherit'}};" >
<i ng-if="row.icon" class="crm-i {{row.icon}}" ></i>
{{row.label}}
</span>
<span style="color: {{row.color ? '#' + row.color : 'inherit'}};" >
<i ng-if="row.icon" class="crm-i {{row.icon}}" ></i>
{{row.label}}
</span>
<span>
<button ng-click="$ctrl.deleteRow(idx)">
<i class="fa-trash crm-i"></i> Delete
</button>
</span>
<!-- button group? -->
<span>
<button ng-if="$ctrl.moveIdx === null && !row.deleted" ng-click="$ctrl.edit(idx)" >
<i class="fa-pencil crm-i"></i> {{ts('Edit')}}
</button>
<button ng-if="$ctrl.moveIdx === null && !row.deleted" ng-click="$ctrl.deleteCategory(idx)">
<i class="fa-trash crm-i"></i> {{ts('Delete')}}
</button>
<button ng-if="$ctrl.moveIdx === null && row.deleted" ng-click="row.deleted = false;">
<i class="fa-trash crm-i"></i> {{ts('Un-Delete')}}
</button>
<button ng-if="$ctrl.moveIdx === null && !row.deleted" ng-click="$ctrl.moveIdx = idx" >
<i class="fa-pencil crm-i"></i> {{ts('Move')}}
</button>
<button ng-if="$ctrl.moveIdx !== null && idx === $ctrl.moveIdx" ng-click="$ctrl.moveIdx = null" >
<i class="fa-pencil crm-i"></i> {{ts('Cancel Move')}}
</button>
</span>
</div>
</div>
<!-- lower idx : {{idx}} moveIdx {{$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)" >{{ts('Move to here')}}</button>
</div>
</li>
</ol>
<p>
<button ng-click="$ctrl.edit(-1)"><i class="crm-i fa-add"></i> Add new category</button>
</p>
<p>
<button ng-click="$ctrl.save()"><i class="crm-i fa-save"></i> Save</button>
</p>
<div ng-if="$ctrl.saved" class="help">
Categories saved. Contacts will be updated shortly (when the next Scheduled
Job run happens).
</div>
</form>
<form ng-show="$ctrl.view === 'edit'"
crm-ui-id-scope>
<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.categoryDefinitions.length == $ctrl.categoryToEdit.idx">{{ts('Add new category')}}</h2>
<div crm-ui-field="{name: 'cc.label', title: ts('Category label'), required: 1}" >
<input
crm-ui-id="ci.label"
name="label"
ng-model="$ctrl.categoryToEdit.label"
class="crm-form-text"
placeholder="{{ts('0123 Super lovely supporters')}}"
style="width: 100%"
/>
</div>
<div crm-ui-field="{name: 'cc.color', title: ts('Color')}" >
<input
crm-ui-id="ci.color"
name="color"
type="color"
ng-model="$ctrl.categoryToEdit.color"
class="crm-form-color"
/>
</div>
<div crm-ui-field="{name: 'cc.icon', title: ts('Icon')}" >
<input
crm-ui-id="cc.icon"
crm-ui-icon-picker
ng-model="$ctrl.categoryToEdit.icon"
/>
</div>
<div crm-ui-field="{name: 'cc.description', title: ts('Description')}" >
<input
crm-ui-id="ci.description"
name="description"
ng-model="$ctrl.categoryToEdit.description"
class="crm-form-textarea"
style="width: 100%"
/>
</div>
<div crm-ui-field="{name: 'cc.search_type', title: ts('How are contacts identified?')}" >
<select
crm-ui-id="ci.search_type"
name="search_type"
ng-model="$ctrl.categoryToEdit.search_type"
ng-change="$ctrl.fixSearchData()"
>
<option value="search" >{{ts('Search Kit search')}}</option>
<option value="group" >{{ts('Group')}}</option>
<!-- future? <option value="sql_template" >{{ts('SQL template')}}</option> -->
</select>
</div>
<!-- fields for search kit -->
<div ng-if="$ctrl.categoryToEdit.search_type === 'search'" crm-ui-field="{name:'cc.saved_search_id', title: ts('Search Kit search')}" >
<input crm-ui-id='cc.saved_search_id' crm-entityref="{entity: 'SavedSearch', select: {allowClear:false}}"
ng-model="$ctrl.categoryToEdit.search_data.saved_search_id" />
</div>
<!-- fields for groups -->
<div ng-if="$ctrl.categoryToEdit.search_type === 'group'" crm-ui-field="{name:'cc.group_id', title: ts('Group')}" >
<input crm-ui-id='cc.group_id' crm-entityref="{entity: 'Group', select: {allowClear:false}}"
ng-model="$ctrl.categoryToEdit.search_data.group_id" />
</div>
<!-- type=button means when you hit Enter in an input above this button is skipped while it looks for the first submit button. -->
<button type=button ng-click="$ctrl.view = 'list'" >Cancel</button>
<button ng-click="$ctrl.updateEditedThing()" >Done</button>
</div>
</form>
<!-- <pre>{{ $ctrl.categoryToEdit }}</pre> -->

View file

@ -0,0 +1,83 @@
<?php
use CRM_Contactcats_ExtensionUtil as E;
return [
[
'name' => 'CustomGroup_Category_changes',
'entity' => 'CustomGroup',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'name' => 'Category_changes',
'title' => E::ts('Category changes'),
'extends' => 'Activity',
'extends_entity_column_value:name' => ['changed_contact_category'],
'style' => 'Inline',
'help_pre' => E::ts(''),
'help_post' => E::ts(''),
'weight' => 18,
'collapse_adv_display' => TRUE,
'created_date' => '2025-02-20 13:59:06',
'is_public' => FALSE,
'icon' => '',
],
'match' => ['name'],
],
],
[
'name' => 'CustomGroup_Category_changes_CustomField_Previous_Category',
'entity' => 'CustomField',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'custom_group_id.name' => 'Category_changes',
'name' => 'Previous_Category',
'label' => E::ts('Previous Category'),
'data_type' => 'EntityReference',
'html_type' => 'Autocomplete-Select',
'is_searchable' => TRUE,
'is_view' => TRUE,
'text_length' => 255,
'note_columns' => 60,
'note_rows' => 4,
'column_name' => 'previous_category_117',
'fk_entity' => 'ContactCategoryDefinition',
],
'match' => [
'name',
'custom_group_id',
],
],
],
[
'name' => 'CustomGroup_Category_changes_CustomField_New_Category',
'entity' => 'CustomField',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'custom_group_id.name' => 'Category_changes',
'name' => 'New_Category',
'label' => E::ts('New Category'),
'data_type' => 'EntityReference',
'html_type' => 'Autocomplete-Select',
'is_searchable' => TRUE,
'is_view' => TRUE,
'text_length' => 255,
'note_columns' => 60,
'note_rows' => 4,
'column_name' => 'new_category_118',
'fk_entity' => 'ContactCategoryDefinition',
],
'match' => [
'name',
'custom_group_id',
],
],
],
];

View file

@ -3,14 +3,14 @@ use CRM_Contactcats_ExtensionUtil as E;
return [
[
'name' => 'OptionValue_Changed_Contact_Category',
'name' => 'OptionValue_changed_contact_category',
'entity' => 'OptionValue',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'option_group_id:name' => 'activity_type',
'option_group_id.name' => 'activity_type',
'label' => E::ts('Changed Contact Category'),
'name' => 'changed_contact_category',
'icon' => 'fa-tags',

View file

@ -1,51 +0,0 @@
<?php
use CRM_Contactcats_ExtensionUtil as E;
return [
[
'name' => 'OptionGroup_contact_categories',
'entity' => 'OptionGroup',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'name' => 'contact_categories',
'title' => E::ts('Contact Categories'),
'data_type' => 'Integer',
'is_reserved' => FALSE,
'is_active' => TRUE,
'option_value_fields' => [
'name',
'label',
'description',
'weight',
],
],
'match' => [
'name',
],
],
],
[
'name' => 'OptionGroup_contact_categories_OptionValue_Default',
'entity' => 'OptionValue',
'cleanup' => 'unused',
'update' => 'unmodified',
'params' => [
'version' => 4,
'values' => [
'option_group_id.name' => 'contact_categories',
'label' => E::ts('Default'),
'is_active' => TRUE,
'value' => '0',
'name' => 'default',
],
'match' => [
'option_group_id',
'name',
'value',
],
],
],
];