Can access entities in SK, begun new admin page

This commit is contained in:
Rich Lott / Artful Robot 2025-02-18 21:42:27 +00:00
parent 06fec5daf7
commit 4978eeb828
8 changed files with 189 additions and 191 deletions

View file

@ -0,0 +1,7 @@
<?php
use CRM_Contactcats_ExtensionUtil as E;
class CRM_Contactcats_BAO_ContactCategoryDefinition extends CRM_Contactcats_DAO_ContactCategoryDefinition {
}

View file

@ -13,12 +13,12 @@
* @property string $category * @property string $category
* @property string $next_category * @property string $next_category
*/ */
class CRM_Contactcats_DAO_ContactCategoryDescription extends CRM_Contactcats_DAO_Base { class CRM_Contactcats_DAO_ContactCategoryDefinition extends CRM_Contactcats_DAO_Base {
/** /**
* Required by older versions of CiviCRM (<5.74). * Required by older versions of CiviCRM (<5.74).
* @var string * @var string
*/ */
public static $_tableName = 'civicrm_contact_category_description'; public static $_tableName = 'civicrm_contact_category_definition';
} }

View file

@ -0,0 +1,63 @@
<?php
namespace Civi\Api4\Action\ContactCategoryDefinition;
use Civi\Api4\Generic\DAOGetAction;
use Civi\Api4\Generic\Result;
use Civi\Api4\Group;
use Civi\Api4\SavedSearch;
class Get extends DAOGetAction {
/**
* Whether to look up and return extra data, helpful for presentations.
*
* @var bool
*/
protected $withLabels = FALSE;
/**
*/
public function _run(Result $result) {
parent::_run($result);
$groupIDs = $searchIDs = [];
foreach ($result as $idx => $row) {
if ($row['search_type'] === 'group') {
$groupIDs[] = $row['search_data']['group_id'] ?? NULL;
}
elseif ($row['search_type'] === 'search') {
$searchIDs[] = $row['search_data']['saved_search_id'] ?? NULL;
}
}
$groupIDs = array_filter($groupIDs);
$searchIDs = array_filter($searchIDs);
if ($groupIDs) {
$groupNames = Group::get()
->addWhere('id', 'IN', $groupIDs)
->addSelect('title')
->execute()->indexBy('id')->column('title');
}
if ($searchIDs) {
$searchNames = SavedSearch::get()
->addWhere('id', 'IN', $searchIDs)
->addSelect('label')
->execute()->indexBy('id')->column('label');
}
if ($searchNames || $groupNames) {
foreach ($result as $row) {
if ($row['search_type'] === 'group'
&& !empty($groupNames[$row['search_data']['group_id'] ?? ''])
) {
$row['search_source'] = $groupNames[$row['search_data']['group_id'] ?? ''];
}
elseif (($row['search_type'] === 'search')
&& !empty($searchNames[$row['search_data']['saved_search_id'] ?? ''])
) {
$row['search_source'] = $searchNames[$row['search_data']['saved_search_id'] ?? ''];
}
}
}
$result[$idx] = $row;
}
}

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Civi\Api4; namespace Civi\Api4;
use Civi\Api4\Action\ContactCategoryDefinition\Get;
/** /**
* ContactCategoryDefinition entity. * ContactCategoryDefinition entity.
* *
@ -10,4 +12,13 @@ namespace Civi\Api4;
*/ */
class ContactCategoryDefinition extends Generic\DAOEntity { class ContactCategoryDefinition extends Generic\DAOEntity {
/**
* @param bool $checkPermissions
* @return DAOGetAction
*/
public static function get($checkPermissions = TRUE) {
return (new Get(static::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
} }

View file

@ -22,133 +22,75 @@
// this.$onInit gets run after the this controller is called, and after the bindings have been applied. // this.$onInit gets run after the this controller is called, and after the bindings have been applied.
this.$onInit = async function() { this.$onInit = async function() {
ctrl.saved = false; ctrl.saved = false;
const various = await crmApi4({ ctrl.categoryDefinitions = null;
settings: ["Setting", "get", { select: ["contact_categories"] }, 0], ctrl.categoryDefinitions = await crmApi4("ContactCategoryDefinition", 'get', { orderBy: { label: 'ASC' }, withLabels: true });
groups: [
"Group",
"get",
{
where: [
["is_active", "=", 1],
["is_hidden", "=", 0]
],
orderBy: { name: "ASC" }
}
],
cats: [
"OptionValue",
"get",
{
select: ["value", "label"],
where: [["option_group_id:name", "=", "contact_categories"]]
}
]
});
ctrl.catmap = [];
if (!various.settings.value || !various.settings.value.groupIDs) {
various.settings.value = {
groupIDs: ["0"],
updateAfter: 0
};
}
various.settings.value.groupIDs.forEach(groupID => {
let cat = various.cats.find(c => c.value == groupID);
ctrl.catmap.push({
groupID,
name: cat ? cat.label : ""
});
});
console.log({ various, catmap: ctrl.catmap });
ctrl.groups = various.groups;
ctrl.nameKeydown = (keyEvt, idx) => {
if (keyEvt.key === "ArrowUp" || keyEvt.key === "ArrowDown") {
keyEvt.preventDefault();
keyEvt.stopPropagation();
if (
keyEvt.key === "ArrowUp" &&
idx > 0 &&
idx < ctrl.catmap.length - 1
) {
ctrl.catmap.splice(idx, 0, ...ctrl.catmap.splice(idx - 1, 1));
console.log("up", { keyEvt });
$timeout(() => keyEvt.target.focus(), 10);
} else if (
keyEvt.key === "ArrowDown" &&
idx < ctrl.catmap.length - 2
) {
ctrl.catmap.splice(idx + 1, 0, ...ctrl.catmap.splice(idx, 1));
console.log(
"down",
ctrl.catmap.map(e => e.name)
);
}
}
};
$scope.$digest(); $scope.$digest();
ctrl.deleteRow = idx => {
ctrl.catmap.splice(idx, 1);
};
ctrl.getGroupsFor = idx => {
let groupsInUse = ctrl.catmap.map(c => c.groupID);
groupsInUse.splice(idx, 1);
return ctrl.groups.filter(
g => !groupsInUse.includes(g.id.toString())
);
};
ctrl.save = async () => { // ctrl.deleteRow = idx => {
console.log("save", ctrl.catmap); // ctrl.catmap.splice(idx, 1);
// reconstruct everything. // };
// ctrl.getGroupsFor = idx => {
// let groupsInUse = ctrl.catmap.map(c => c.groupID);
// groupsInUse.splice(idx, 1);
// return ctrl.groups.filter(
// g => !groupsInUse.includes(g.id.toString())
// );
// };
const optValsRecords = []; // ctrl.save = async () => {
ctrl.catmap.forEach(r => { // console.log("save", ctrl.catmap);
if (!r.name || r.groupID === "") { // // reconstruct everything.
return; //
} // const optValsRecords = [];
// Do we have an option value for this group ID? // ctrl.catmap.forEach(r => {
let c = various.cats.find(cat => cat.value == r.groupID); // if (!r.name || r.groupID === "") {
if (c) { // return;
if (c.label != r.name) { // }
optValsRecords.push({ // // Do we have an option value for this group ID?
id: c.id, // let c = various.cats.find(cat => cat.value == r.groupID);
label: r.name // if (c) {
}); // if (c.label != r.name) {
} // optValsRecords.push({
} else { // id: c.id,
optValsRecords.push({ // label: r.name
label: r.name, // });
value: r.groupID, // }
"option_group_id:name": "contact_categories" // } else {
}); // optValsRecords.push({
} // label: r.name,
}); // value: r.groupID,
console.log("optionValue updates", optValsRecords, ctrl.catmap); // "option_group_id:name": "contact_categories"
const updates = { // });
saveSetting: [ // }
"Setting", // });
"set", // console.log("optionValue updates", optValsRecords, ctrl.catmap);
{ // const updates = {
values: { // saveSetting: [
contact_categories: { // "Setting",
groupIDs: ctrl.catmap.map(i => i.groupID), // "set",
updateAfter: 0 // {
} // values: {
} // contact_categories: {
} // groupIDs: ctrl.catmap.map(i => i.groupID),
] // updateAfter: 0
}; // }
if (optValsRecords.length) { // }
updates.saveOptions = [ // }
"OptionValue", // ]
"save", // };
{ records: optValsRecords } // if (optValsRecords.length) {
]; // updates.saveOptions = [
} // "OptionValue",
await crmApi4(updates); // "save",
console.log("saved", updates); // { records: optValsRecords }
ctrl.saved = true; // ];
$scope.$digest(); // }
}; // await crmApi4(updates);
// console.log("saved", updates);
// ctrl.saved = true;
// $scope.$digest();
// };
}; };
// this.$onChange = function(changes) { // this.$onChange = function(changes) {
// // changes is object keyed by name what '<' binding changed in // // changes is object keyed by name what '<' binding changed in

View file

@ -1,51 +1,34 @@
<div ng-if="!$ctrl.catmap"> <div ng-if="$ctrl.categoryDefinitions === null">
Loading... Loading...
</div> </div>
<form ng-if="$ctrl.catmap" crm-ui-id-scope> <form ng-if="$ctrl.categoryDefinitions" crm-ui-id-scope>
<button ng-click="$ctrl.catmap.unshift({groupID: '', name:''})"> <button ng-click="$ctrl.catmap.unshift({groupID: '', name:''})">
<i class="crm-i fa-plus"></i> Add category <i class="crm-i fa-plus"></i> Add category
</button> </button>
<!-- I can see use of
presentation order/grouping
short title, "amazing"
longer title/description. "regular givers who..."
-->
<ol class="crm-catmap"> <ol class="crm-catmap">
<li ng-repeat="(idx, row) in $ctrl.catmap"> <li ng-repeat="(idx, row) in $ctrl.categoryDefinitions">
<div class="group-input-wrapper">
<select <span style="color: {{row.color ? '#' + row.color : 'inherit'}};" >
ng-if="row.groupID === '' || row.groupID > 0" <i ng-if="row.icon" class="crm-i {{row.icon}}" ></i>
crm-ui-select="{placeholder:'Select group',allowClear:false,label:'Group'}" {{row.label}}
ng-model="$ctrl.catmap[idx].groupID" </span>
style="width: 18rem"
> <span>
<option
ng-repeat="(grpIdx, grp) in $ctrl.getGroupsFor(idx)"
value="{{grp.id}}"
>{{grp.title}}</option
>
</select>
</div>
<div
ng-if="row.groupID !== '' && row.groupID == 0"
style="width: 18rem;display: inline-block;"
>
Default
</div>
<div class="name-input-wrapper">
<label crm-ui-for="name{{idx}}">Label</label>
<input
crm-ui-id="name{{idx}}"
type="text"
ng-model="$ctrl.catmap[idx].name"
ng-keydown="$ctrl.nameKeydown($event, idx)"
/>
<div class="hint description">
{{ts('Use the Up/Down arrow keys to re-order')}}
</div>
</div>
<div>
<button ng-click="$ctrl.deleteRow(idx)"> <button ng-click="$ctrl.deleteRow(idx)">
<i class="fa-trash crm-i"></i> Delete <i class="fa-trash crm-i"></i> Delete
</button> </button>
</div> </span>
</li> </li>
</ol> </ol>
<p> <p>
<button ng-click="$ctrl.save()"><i class="crm-i fa-save"></i> Save</button> <button ng-click="$ctrl.save()"><i class="crm-i fa-save"></i> Save</button>
</p> </p>

View file

@ -12,9 +12,9 @@ return [
'log' => FALSE, 'log' => FALSE,
], ],
'getIndices' => fn() => [ 'getIndices' => fn() => [
'index_category' => [ 'index_category_definition_id' => [
'fields' => [ 'fields' => [
'category' => TRUE, 'category_definition_id' => TRUE,
], ],
], ],
], ],
@ -43,25 +43,24 @@ return [
'on_delete' => 'CASCADE', 'on_delete' => 'CASCADE',
], ],
], ],
'category' => [ 'category_definition_id' => [
'title' => E::ts('Category'), 'title' => E::ts('Category Definition'),
'sql_type' => 'int unsigned', 'sql_type' => 'int unsigned',
'input_type' => 'Select',
'required' => TRUE, 'required' => TRUE,
'default' => 0, 'default' => 0,
'pseudoconstant' => [ 'input_type' => 'EntityRef',
'option_group_name' => 'contact_categories', 'entity_reference' => [
'entity' => 'ContactCategoryDefinition',
'key' => 'id',
'on_delete' => 'CASCADE',
], ],
], ],
'next_category' => [ 'next_category' => [
'title' => E::ts('Next Category'), 'title' => E::ts('Next Category'),
'sql_type' => 'int unsigned', 'sql_type' => 'int unsigned',
'input_type' => 'Select',
'required' => TRUE, 'required' => TRUE,
'default' => 0, 'default' => 0,
'pseudoconstant' => [ 'input_type' => 'Hidden', /* not needed to be exposed */
'option_group_name' => 'contact_categories',
],
], ],
], ],
]; ];

View file

@ -2,29 +2,22 @@
use CRM_Contactcats_ExtensionUtil as E; use CRM_Contactcats_ExtensionUtil as E;
return [ return [
'name' => 'ContactCategoryDescription', 'name' => 'ContactCategoryDefinition',
'table' => 'civicrm_contact_category_description', 'table' => 'civicrm_contact_category_definition',
'class' => 'CRM_Contactcats_DAO_ContactCategoryDescription', 'class' => 'CRM_Contactcats_DAO_ContactCategoryDefinition',
'getInfo' => fn() => [ 'getInfo' => fn() => [
'title' => E::ts('Contact Category Description'), 'title' => E::ts('Contact Category Definition'),
'title_plural' => E::ts('Contact Category Descriptions'), 'title_plural' => E::ts('Contact Category Definitions'),
'description' => E::ts('Holds definition of a "Contact category"'), 'description' => E::ts('Holds definition of a "Contact category"'),
'log' => FALSE, 'log' => FALSE,
], ],
'getIndices' => fn() => [
'index_category' => [
'fields' => [
'category' => TRUE,
],
],
],
'getFields' => fn() => [ 'getFields' => fn() => [
'id' => [ 'id' => [
'title' => E::ts('ID'), 'title' => E::ts('ID'),
'sql_type' => 'int unsigned', 'sql_type' => 'int unsigned',
'input_type' => 'Number', 'input_type' => 'Number',
'required' => TRUE, 'required' => TRUE,
'description' => E::ts('Unique Contact Category Description ID'), 'description' => E::ts('Unique Contact Category Definition ID'),
'primary_key' => TRUE, 'primary_key' => TRUE,
'auto_increment' => TRUE, 'auto_increment' => TRUE,
], ],
@ -33,14 +26,13 @@ return [
'sql_type' => 'varchar(255)', 'sql_type' => 'varchar(255)',
'input_type' => 'Text', 'input_type' => 'Text',
'required' => TRUE, 'required' => TRUE,
'description' => E::ts('Same as id but for FormBuilder'),
'input_attrs' => [ 'input_attrs' => [
'text_length' => 255, 'text_length' => 255,
'label' => E::ts('Category name'), 'label' => E::ts('Category name'),
], ],
], ],
'search_type' => [ 'search_type' => [
'title' => E::ts('What defines this search?'), 'title' => E::ts('Definition type'),
'sql_type' => 'varchar(12)', 'sql_type' => 'varchar(12)',
'input_type' => 'Select', 'input_type' => 'Select',
'required' => TRUE, 'required' => TRUE,
@ -59,11 +51,12 @@ return [
'title' => E::ts('JSON blob specifies particulars to for the search_type'), 'title' => E::ts('JSON blob specifies particulars to for the search_type'),
'description' => E::ts('Holds data specific to the search specification (e.g. a group ID when Group is selected).'), 'description' => E::ts('Holds data specific to the search specification (e.g. a group ID when Group is selected).'),
'sql_type' => 'text', 'sql_type' => 'text',
'serialize' => 'JSON', 'serialize' => CRM_Core_DAO::SERIALIZE_JSON,
], ],
'color' => [ 'color' => [
'title' => E::ts('Colour for category label'), 'title' => E::ts('Colour for category label'),
'sql_type' => 'varchar(7)', 'sql_type' => 'varchar(7)',
'input_type' => 'Color',
'default' => '', 'default' => '',
'required' => TRUE, 'required' => TRUE,
], ],
@ -77,7 +70,7 @@ return [
'execution_order' => [ 'execution_order' => [
'title' => E::ts('Execution order'), 'title' => E::ts('Execution order'),
'description' => E::ts('The first category to match is assigned. Lower numbers are tested first.'), 'description' => E::ts('The first category to match is assigned. Lower numbers are tested first.'),
'sql_type' => 'tinyint unsigned', 'sql_type' => 'int(1) unsigned',
'default' => '10', 'default' => '10',
'required' => TRUE, 'required' => TRUE,
], ],