chg: [mailinglist] Improved feature

Previously, emails were stored as json encoded string. To add more flexibility and prevent inconsistencies (such as propagating email changes to the mailing list), it has been moved to a table.
pull/93/head
Sami Mokaddem 2021-10-28 09:00:20 +02:00
parent fe9fbe2e99
commit 4ef6738053
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
14 changed files with 406 additions and 93 deletions

View File

@ -112,11 +112,43 @@ class MailingLists extends AbstractMigration
'null' => true,
'signed' => false,
'length' => 10,
]);
])
->addColumn('include_primary_email', 'boolean', [
'default' => 1,
'null' => false,
'comment' => 'Should the primary email address by included in the mailing list'
])
->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->addForeignKey('individual_id', 'individuals', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']);
$mailinglists_individuals->addIndex(['mailing_list_id', 'individual_id'], ['unique' => true]);
$mailinglists_individuals->create();
$mailinglists_metafields = $this->table('mailing_lists_meta_fields', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci',
]);
$mailinglists_metafields
->addColumn('mailing_list_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10,
])
->addColumn('meta_field_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10,
])
->addPrimaryKey(['mailing_list_id', 'meta_field_id'])
->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])
->addForeignKey('meta_field_id', 'meta_fields', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
$mailinglists_metafields->create();
}
}

View File

@ -0,0 +1,16 @@
{
"name": "Cerebrate Individuals extended",
"namespace": "cerebrate",
"description": "Template to extend fields of individuals",
"version": 1,
"scope": "individual",
"uuid": "3bc374c8-3cdd-4900-823e-cc9100ad5179",
"source": "Cerebrate",
"metaFields": [
{
"field": "alternate_email",
"type": "text",
"multiple": true
}
]
}

View File

@ -676,16 +676,7 @@ class CRUDComponent extends Component
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
$this->Controller->set('quickFilterValue', $params['quickFilter']);
foreach ($quickFilterFields as $filterField) {
$likeCondition = false;
if (is_array($filterField)) {
$likeCondition = reset($filterField);
$filterFieldName = array_key_first($filterField);
$queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] .'%';
} else {
$queryConditions[$filterField] = $params['quickFilter'];
}
}
$queryConditions = $this->genQuickFilterConditions($params, $query, $quickFilterFields);
$query->where(['OR' => $queryConditions]);
} else {
$this->Controller->set('quickFilterValue', '');
@ -693,6 +684,28 @@ class CRUDComponent extends Component
return $query;
}
public function genQuickFilterConditions(array $params, \Cake\ORM\Query $query, array $quickFilterFields): array
{
$queryConditions = [];
foreach ($quickFilterFields as $filterField) {
$likeCondition = false;
if (is_array($filterField)) {
reset($filterField);
$filterFieldName = array_key_first($filterField);
if (!empty($filterField[$filterFieldName])) {
$queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] . '%';
} else {
$queryConditions[$filterField] = $params['quickFilter'];
}
}
$query->where(['OR' => $queryConditions]);
} else {
$queryConditions[$filterField] = $params['quickFilter'];
}
}
return $queryConditions;
}
protected function setFilters($params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
{
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';

View File

@ -26,15 +26,15 @@ class ParamHandlerComponent extends Component
$queryString = str_replace('.', '_', $filter);
$queryString = str_replace(' ', '_', $queryString);
if ($this->request->getQuery($queryString) !== null) {
$parsedParams[$filter] = $this->request->getQuery($queryString);
$parsedParams[$filter] = trim($this->request->getQuery($queryString));
continue;
}
if (($this->request->getQuery($filter)) !== null) {
$parsedParams[$filter] = $this->request->getQuery($filter);
$parsedParams[$filter] = trim($this->request->getQuery($filter));
continue;
}
if (($this->request->is('post') || $this->request->is('put')) && $this->request->getData($filter) !== null) {
$parsedParams[$filter] = $this->request->getData($filter);
$parsedParams[$filter] = trim($this->request->getData($filter));
}
}
return $parsedParams;

View File

@ -6,13 +6,15 @@ use Cake\Utility\Inflector;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\Error\Debugger;
use Cake\ORM\Query;
use Cake\ORM\Entity;
use Exception;
class MailingListsController extends AppController
{
public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability'];
public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => true], ['description' => true], ['releasability' => true]];
public $containFields = ['Users', 'Individuals'];
public $containFields = ['Users', 'Individuals', 'MetaFields'];
public function index()
{
@ -72,45 +74,151 @@ class MailingListsController extends AppController
public function listIndividuals($mailinglist_id)
{
$individuals = $this->MailingLists->get($mailinglist_id, [
'contain' => 'Individuals'
])->individuals;
$params = $this->ParamHandler->harvestParams(['quickFilter']);
if (!empty($params['quickFilter'])) {
// foreach ($sharingGroup['sharing_group_orgs'] as $k => $org) {
// if (strpos($org['name'], $params['quickFilter']) === false) {
// unset($sharingGroup['sharing_group_orgs'][$k]);
// }
// }
// $sharingGroup['sharing_group_orgs'] = array_values($sharingGroup['sharing_group_orgs']);
$quickFilter = [
'uuid',
['first_name' => true],
['last_name' => true],
];
$quickFilterUI = array_merge($quickFilter, [
['Registered emails' => true],
]);
$filters = ['uuid', 'first_name', 'last_name', 'quickFilter'];
$queryParams = $this->ParamHandler->harvestParams($filters);
$activeFilters = $queryParams['quickFilter'] ?? [];
$mailingList = $this->MailingLists->find()
->where(['MailingLists.id' => $mailinglist_id])
->contain('MetaFields')
->first();
$matchingMetaFieldParentIDs = [];
// Collect individuals having a matching meta_field
foreach ($mailingList->meta_fields as $metaField) {
if (
empty($queryParams['quickFilter']) ||
(
str_contains($metaField->field, 'email') &&
str_contains($metaField->value, $queryParams['quickFilter'])
)
) {
$matchingMetaFieldParentIDs[$metaField->parent_id] = true;
}
}
$matchingMetaFieldParentIDs = array_keys($matchingMetaFieldParentIDs);
$mailingList = $this->MailingLists->loadInto($mailingList, [
'Individuals' => function (Query $q) use ($queryParams, $quickFilter, $matchingMetaFieldParentIDs) {
$conditions = [];
if (!empty($queryParams)) {
$conditions = $this->CRUD->genQuickFilterConditions($queryParams, $q, $quickFilter);
}
if (!empty($matchingMetaFieldParentIDs)) {
$conditions[] = function (QueryExpression $exp) use ($matchingMetaFieldParentIDs) {
return $exp->in('Individuals.id', $matchingMetaFieldParentIDs);
};
}
if (!empty($queryParams['quickFilter'])) {
$conditions[] = [
'MailingListsIndividuals.include_primary_email' => true,
'Individuals.email LIKE' => "%{$queryParams['quickFilter']}%"
];
}
$q->where([
'OR' => $conditions
]);
return $q;
}
]);
$mailingList->injectRegisteredEmailsIntoIndividuals();
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($individuals, 'json');
return $this->RestResponse->viewData($mailingList->individuals, 'json');
}
$individuals = $this->CustomPagination->paginate($mailingList->individuals);
$this->set('mailing_list_id', $mailinglist_id);
$this->set('quickFilter', $quickFilterUI);
$this->set('activeFilters', $activeFilters);
$this->set('quickFilterValue', $queryParams['quickFilter'] ?? '');
$this->set('individuals', $individuals);
}
public function addIndividual($mailinglist_id)
{
$mailingList = $this->MailingLists->get($mailinglist_id, [
'contain' => 'Individuals'
'contain' => ['Individuals', 'MetaFields']
]);
$conditions = [];
$linkedIndividualsIDs = Hash::extract($mailingList, 'individuals.{n}.id');
$conditions = [
'id NOT IN' => $linkedIndividualsIDs
];
$dropdownData = [
'individuals' => $this->MailingLists->Individuals->getTarget()->find('list', [
'sort' => ['name' => 'asc'],
'conditions' => $conditions
])
'individuals' => $this->MailingLists->Individuals->getTarget()->find()
->order(['first_name' => 'asc'])
->where($conditions)
->all()
->combine('id', 'full_name')
->toArray()
];
if ($this->request->is('post') || $this->request->is('put')) {
$memberIDs = $this->request->getData()['individuals'];
$chosen_emails = $this->request->getData()['chosen_emails'];
if (!empty($chosen_emails)) {
$chosen_emails = json_decode($chosen_emails, true);
$chosen_emails = !is_null($chosen_emails) ? $chosen_emails : [];
} else {
$chosen_emails = [];
}
$members = $this->MailingLists->Individuals->getTarget()->find()->where([
'id IN' => $memberIDs
])->all()->toArray();
$success = (bool)$this->MailingLists->Individuals->link($mailingList, $members);
$memberToLink = [];
$memberToUpdate = [];
foreach ($members as $i => $member) {
$includePrimary = in_array('primary', $chosen_emails[$member->id]);
$chosen_emails[$member->id] = array_filter($chosen_emails[$member->id], function($entry) {
return $entry != 'primary';
});
$members[$i]->_joinData = new Entity(['include_primary_email' => $includePrimary]);
if (in_array($member->id, $linkedIndividualsIDs)) { // individual already in the list
// $memberToUpdate[] = $members[$i];
} else { // new individual to add to the list
$memberToLink[] = $members[$i];
}
}
// save new individuals
if (!empty($memberToLink)) {
$success = (bool)$this->MailingLists->Individuals->link($mailingList, $memberToLink);
if ($success && !empty($chosen_emails[$member->id])) { // Include any remaining emails from the metaFields
$emailsFromMetaFields = $this->MailingLists->MetaFields->find()->where([
'id IN' => $chosen_emails[$member->id]
])->all()->toArray();
$success = (bool)$this->MailingLists->MetaFields->link($mailingList, $emailsFromMetaFields);
}
}
// update existing individuals
if (!empty($memberToUpdate)) {
$memberToUpdateIDs = Hash::extract($memberToUpdate, '{n}.id');
$metaFieldsToRemove = array_filter($mailingList->meta_fields, function($metaField) use ($memberToUpdateIDs) {
return in_array($metaField->parent_id, $memberToUpdateIDs);
});
// Trying to update `include_primary`...
// $success = (bool)$this->MailingLists->Individuals->unlink($mailingList, $memberToUpdate, ['atomic' => false]);
// $success = $success && (bool)$this->MailingLists->Individuals->link($mailingList, $memberToUpdate);
// Remove and add relevant meta fields
// $success = (bool)$this->MailingLists->MetaFields->unlink($mailingList, $metaFieldsToRemove, ['atomic' => false]);
// if ($success && !empty($chosen_emails[$member->id])) { // Include any remaining emails from the metaFields
// $emailsFromMetaFields = $this->MailingLists->MetaFields->find()->where([
// 'id IN' => $chosen_emails[$member->id]
// ])->all()->toArray();
// $success = (bool)$this->MailingLists->MetaFields->link($mailingList, $emailsFromMetaFields);
// }
}
if ($success) {
$message = __n('%s individual added to the mailing list.', '%s Individuals added to the mailing list.', count($members), count($members));
$message = __n('{0} individual added to the mailing list.', '{0} Individuals added to the mailing list.', count($members), count($members));
$mailingList = $this->MailingLists->get($mailingList->id);
} else {
$message = __n('The individual could not be added to the mailing list.', 'The Individuals could not be added to the mailing list.', count($members));
@ -129,7 +237,7 @@ class MailingListsController extends AppController
public function removeIndividual($mailinglist_id, $individual_id=null)
{
$mailingList = $this->MailingLists->get($mailinglist_id, [
'contain' => 'Individuals'
'contain' => ['Individuals', 'MetaFields']
]);
$individual = [];
if (!is_null($individual_id)) {
@ -138,13 +246,20 @@ class MailingListsController extends AppController
if ($this->request->is('post') || $this->request->is('delete')) {
$success = false;
if (!is_null($individual_id)) {
$individual = $this->MailingLists->Individuals->get($individual_id);
$success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individual]);
$individualToRemove = $this->MailingLists->Individuals->get($individual_id);
$metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([
'id IN' => Hash::extract($mailingList, 'meta_fields.{n}.id'),
'parent_id' => $individual_id,
])->all()->toArray();
$success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]);
if ($success && !empty($metaFieldsToRemove)) {
$success = (bool)$this->MailingLists->MetaFields->unlink($mailingList, $metaFieldsToRemove);
}
if ($success) {
$message = __('Individual removed from the mailing list.');
$message = __('{0} removed from the mailing list.', $individualToRemove->full_name);
$mailingList = $this->MailingLists->get($mailingList->id);
} else {
$message = __n('Individual could not be removed from the mailing list.');
$message = __n('{0} could not be removed from the mailing list.', $individual->full_name);
}
$this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, $mailingList->getErrors());
} else {
@ -155,22 +270,27 @@ class MailingListsController extends AppController
if (empty($params['ids'])) {
throw new NotFoundException(__('Invalid {0}.', Inflector::singularize($this->MailingLists->Individuals->getAlias())));
}
$individuals = $this->MailingLists->Individuals->find()->where([
$individualsToRemove = $this->MailingLists->Individuals->find()->where([
'id IN' => array_map('intval', $params['ids'])
])->all()->toArray();
$metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([
'id IN' => Hash::extract($mailingList, 'meta_fields.{n}.id'),
'parent_id IN' => Hash::extract($mailingList, 'meta_fields.{n}.id')
])->all()->toArray();
dd($metaFieldsToRemove);
$unlinkSuccesses = 0;
foreach ($individuals as $individual) {
$success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individual]);
foreach ($individualsToRemove as $individualToRemove) {
$success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]);
$results[] = $success;
if ($success) {
$unlinkSuccesses++;
}
}
$mailingList = $this->MailingLists->get($mailingList->id);
$success = $unlinkSuccesses == count($individuals);
$success = $unlinkSuccesses == count($individualsToRemove);
$message = __(
'{0} {1} have been removed.',
$unlinkSuccesses == count($individuals) ? __('All') : sprintf('%s / %s', $unlinkSuccesses, count($individuals)),
$unlinkSuccesses == count($individualsToRemove) ? __('All') : sprintf('%s / %s', $unlinkSuccesses, count($individualsToRemove)),
Inflector::singularize($this->MailingLists->Individuals->getAlias())
);
$this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, []);

View File

@ -3,7 +3,6 @@
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class MailingList extends AppModel
{
@ -18,4 +17,35 @@ class MailingList extends AppModel
'uuid' => true,
'user_id' => true,
];
private $metaFieldsByParentId = [];
public function injectRegisteredEmailsIntoIndividuals()
{
if (empty($this->individuals)) {
return;
}
if (!empty($this->meta_fields)) {
foreach ($this->meta_fields as $meta_field) {
$this->metaFieldsByParentId[$meta_field->parent_id][] = $meta_field;
}
}
foreach ($this->individuals as $i => $individual) {
$this->individuals[$i]->mailinglist_emails = $this->collectEmailsForMailingList($individual);
}
}
protected function collectEmailsForMailingList($individual)
{
$emails = [];
if (!empty($individual['_joinData']) && !empty($individual['_joinData']['include_primary_email'])) {
$emails[] = $individual->email;
}
if (!empty($this->metaFieldsByParentId[$individual->id])) {
foreach ($this->metaFieldsByParentId[$individual->id] as $metaField) {
$emails[] = $metaField->value;
}
}
return $emails;
}
}

View File

@ -36,6 +36,15 @@ class IndividualsTable extends AppTable
$this->belongsToMany('Organisations', [
'through' => 'Alignments',
]);
$this->hasMany('MetaFields')
->setForeignKey('parent_id')
->setBindingKey('id')
->setConditions([
'MetaFields.scope' => 'individual'
])
->setDependent(true);
$this->belongsToMany('MailingLists');
$this->setDisplayField('email');
}

View File

@ -18,19 +18,12 @@ class MailingListsTable extends AppTable
$this->belongsTo(
'Users'
);
// $this->belongsToMany(
// 'Individuals',
// [
// 'className' => 'Individuals',
// 'foreignKey' => 'individual_id',
// 'joinTable' => 'sgo',
// 'targetForeignKey' => 'organisation_id'
// ]
// );
$this->belongsToMany('Individuals', [
'joinTable' => 'mailing_lists_individuals',
]);
// Change to HasMany?
$this->belongsToMany('MetaFields');
$this->setDisplayField('name');
}

View File

@ -15,6 +15,12 @@ class MetaFieldsTable extends AppTable
$this->setDisplayField('field');
$this->belongsTo('MetaTemplates');
$this->belongsTo('MetaTemplateFields');
$this->belongsTo('Individuals')
->setForeignKey('parent_id')
->setBindingKey('id')
->setConditions([
'scope' => 'individual'
]);
}
public function validationDefault(Validator $validator): Validator

View File

@ -1,24 +1,121 @@
<?php
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'title' => __('Add members to mailing list {0} [{1}]', h($mailingList->name), h($mailingList->id)),
// 'description' => __('Mailing list are email distribution lists containing individuals.'),
'model' => 'MailingLists',
'fields' => [
[
'field' => 'individuals',
'type' => 'dropdown',
'multiple' => true,
'select2' => true,
'label' => __('Members'),
'class' => 'select2-input',
'options' => $dropdownData['individuals']
],
echo $this->element('genericElements/Form/genericForm', [
'data' => [
'title' => __('Add members to `{0}` [{1}]', h($mailingList->name), h($mailingList->id)),
'model' => 'MailingLists',
'fields' => [
[
'field' => 'individuals',
'type' => 'dropdown',
'multiple' => true,
'select2' => true,
'label' => __('Members'),
'class' => 'select2-input',
'options' => $dropdownData['individuals']
],
'submit' => [
'action' => $this->request->getParam('action')
[
'field' => 'chosen_emails',
'type' => 'text',
'templates' => ['inputContainer' => '<div class="row mb-3 d-none">{{content}}</div>'],
],
'<div class="alternate-emails-container panel d-none"></div>'
],
]);
'submit' => [
'action' => $this->request->getParam('action')
],
],
]);
?>
</div>
<script>
(function() {
let individuals = {}
$('#individuals-field').on('select2:select select2:unselect', function(e) {
const selected = e.params.data;
fetchIndividual(selected.id).then(() => {
udpateAvailableEmails($(e.target).select2('data'));
})
});
function udpateAvailableEmails(selected) {
const $container = $('.alternate-emails-container')
$container.empty()
$container.toggleClass('d-none', selected.length == 0)
selected.forEach(selectData => {
const individual = individuals[selectData.id]
let formContainers = [genForContainer(`primary-${individual.id}`, individual.email, '<?= __('primary email') ?>', true, true)]
if (individual.alternate_emails !== undefined) {
individual.alternate_emails.forEach(alternateEmail => {
formContainers.push(
genForContainer(alternateEmail.id, alternateEmail.value, `${alternateEmail.metaTemplate.namespace} :: ${alternateEmail.metaTemplate.name}`, false)
)
})
}
const $individualFullName = $('<div/>').addClass('fw-light fs-5 mt-2').text(individual.full_name)
const $individualContainer = $('<div/>').addClass('individual-container').data('individualid', individual.id)
.append($individualFullName).append(formContainers)
$container.append($individualContainer)
registerChangeListener()
injectSelectedEmailsIntoForm()
});
}
function genForContainer(id, email, email_source, is_primary = true, checked = false) {
const $formContainer = $('<div/>').addClass('form-check ms-2')
$formContainer.append(
$('<input/>').addClass('form-check-input').attr('type', 'checkbox').attr('id', `individual-${id}`)
.attr('value', is_primary ? 'primary' : id).prop('checked', checked),
$('<label/>').addClass('form-check-label').attr('for', `individual-${id}`).append(
$('<span/>').text(email),
$('<span/>').addClass('fw-light fs-8 align-middle ms-2').text(`${email_source}`)
)
)
return $formContainer
}
function registerChangeListener() {
$('.alternate-emails-container .individual-container input')
.off('change.udpate')
.on('change.udpate', injectSelectedEmailsIntoForm)
}
function injectSelectedEmailsIntoForm() {
const selectedEmails = getSelectedEmails()
$('#chosen_emails-field').val(JSON.stringify(selectedEmails))
}
function getSelectedEmails() {
selectedEmails = {}
$('.alternate-emails-container .individual-container').each(function() {
const $individualContainer = $(this)
const individualId = $individualContainer.data('individualid')
selectedEmails[individualId] = []
const $inputs = $individualContainer.find('input:checked').each(function() {
selectedEmails[individualId].push($(this).val())
})
})
return selectedEmails
}
function fetchIndividual(id) {
const urlGet = `/individuals/view/${id}`
const options = {
statusNode: $('.alternate-emails-container')
}
if (individuals[id] !== undefined) {
return Promise.resolve(individuals[id])
}
return AJAXApi.quickFetchJSON(urlGet, options)
.then(individual => {
individuals[individual.id] = individual
})
.catch((e) => {
UI.toast({
variant: 'danger',
text: '<?= __('Could not fetch individual') ?>'
})
})
}
})()
</script>

View File

@ -2,7 +2,7 @@
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $individuals,
'skip_pagination' => 1,
// 'skip_pagination' => 1,
'top_bar' => [
'children' => [
[
@ -33,11 +33,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'type' => 'search',
'button' => __('Filter'),
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
]
'searchKey' => 'value',
'additionalUrlParams' => h($mailing_list_id)
],
]
],
'fields' => [
@ -45,18 +46,25 @@ echo $this->element('genericElements/IndexTable/index_table', [
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
'url' => '/individuals/view/{{0}}',
'url_vars' => ['id'],
],
[
'name' => __('First name'),
'data_path' => 'first_name',
'url' => '/individuals/view/{{0}}',
'url_vars' => ['id'],
],
[
'name' => __('Last name'),
'data_path' => 'last_name',
'url' => '/individuals/view/{{0}}',
'url_vars' => ['id'],
],
[
'name' => __('Email'),
'data_path' => 'email',
'name' => __('Registered Email'),
'data_path' => 'mailinglist_emails',
'element' => 'list',
],
[
'name' => __('UUID'),
@ -65,11 +73,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
]
],
'actions' => [
[
'url' => '/individuals/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye'
],
[
'open_modal' => '/mailingLists/removeIndividual/' . h($mailing_list_id) . '/[onclick_params_data_path]',
'modal_params_data_path' => 'id',

View File

@ -45,7 +45,8 @@ echo $this->element(
[
'url' => '/mailingLists/listIndividuals/{{0}}',
'url_params' => ['id'],
'title' => __('Individuals')
'title' => __('Individuals'),
'collapsed' => 'show',
]
]
]

View File

@ -84,7 +84,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
'title' => __('ContactDB Organisation Index'),
'description' => __('A list of organisations known by your Cerebrate instance. This list can get populated either directly, by adding new organisations or by fetching them from trusted remote sources.'),
'pull' => 'right',
'actions' => [
[
'url' => '/organisations/view',

View File

@ -92,9 +92,6 @@ echo $this->Breadcrumbs->render(
<?php if (!empty($breadcrumbLinks) || !empty($breadcrumbAction)) : ?>
<div class="breadcrumb-link-container position-absolute end-0 d-flex">
<?php endif; ?>
<?php if (!empty($breadcrumbLinks)) : ?>
<div class="header-breadcrumb-children d-none d-md-flex btn-group">
<?= $breadcrumbLinks ?>
<?php if (!empty($breadcrumbAction)) : ?>
@ -106,8 +103,5 @@ echo $this->Breadcrumbs->render(
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($breadcrumbLinks) || !empty($breadcrumbAction)) : ?>
</div>
<?php endif; ?>