Merge branch 'tags' into develop-unstable
commit
0eb14195ee
|
@ -29,12 +29,14 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "src/"
|
"App\\": "src/",
|
||||||
|
"Tags\\": "plugins/Tags/src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\Test\\": "tests/",
|
"App\\Test\\": "tests/",
|
||||||
|
"Tags\\Test\\": "plugins/Tags/tests/",
|
||||||
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
|
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
|
||||||
|
class TagSystem extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change() {
|
||||||
|
$tags = $this->table('tags_tags')
|
||||||
|
->addColumn('namespace', 'string', [
|
||||||
|
'default' => null,
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('predicate', 'string', [
|
||||||
|
'default' => null,
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('value', 'string', [
|
||||||
|
'default' => null,
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'default' => null,
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('colour', 'string', [
|
||||||
|
'default' => null,
|
||||||
|
'limit' => 7,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('counter', 'integer', [
|
||||||
|
'default' => 0,
|
||||||
|
'length' => 11,
|
||||||
|
'null' => false,
|
||||||
|
'signed' => false,
|
||||||
|
'comment' => 'Field used by the CounterCache behaviour to count the occurence of tags'
|
||||||
|
])
|
||||||
|
->addColumn('created', 'datetime', [
|
||||||
|
'default' => null,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('modified', 'datetime', [
|
||||||
|
'default' => null,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
|
||||||
|
$tagged = $this->table('tags_tagged')
|
||||||
|
->addColumn('tag_id', 'integer', [
|
||||||
|
'default' => null,
|
||||||
|
'null' => false,
|
||||||
|
'signed' => false,
|
||||||
|
'length' => 10,
|
||||||
|
])
|
||||||
|
->addColumn('fk_id', 'integer', [
|
||||||
|
'default' => null,
|
||||||
|
'null' => true,
|
||||||
|
'signed' => false,
|
||||||
|
'length' => 10,
|
||||||
|
'comment' => 'The ID of the entity being tagged'
|
||||||
|
])
|
||||||
|
->addColumn('fk_model', 'string', [
|
||||||
|
'default' => null,
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'The model name of the entity being tagged'
|
||||||
|
])
|
||||||
|
->addColumn('created', 'datetime', [
|
||||||
|
'default' => null,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('modified', 'datetime', [
|
||||||
|
'default' => null,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
|
||||||
|
$tags->addIndex(['name'], ['unique' => true])
|
||||||
|
->update();
|
||||||
|
|
||||||
|
$tagged->addIndex(['tag_id', 'fk_id', 'fk_table'], ['unique' => true])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
use Cake\Routing\Route\DashedRoute;
|
||||||
|
use Cake\Routing\RouteBuilder;
|
||||||
|
|
||||||
|
$routes->plugin(
|
||||||
|
'Tags',
|
||||||
|
['path' => '/tags'],
|
||||||
|
function ($routes) {
|
||||||
|
$routes->setRouteClass(DashedRoute::class);
|
||||||
|
|
||||||
|
$routes->connect(
|
||||||
|
'/{action}/*',
|
||||||
|
['controller' => 'Tags']
|
||||||
|
);
|
||||||
|
|
||||||
|
// $routes->get('/', ['controller' => 'Tags']);
|
||||||
|
// $routes->get('/{id}', ['controller' => 'Tags', 'action' => 'view']);
|
||||||
|
// $routes->put('/{id}', ['controller' => 'Tags', 'action' => 'edit']);
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags\Controller;
|
||||||
|
|
||||||
|
use App\Controller\AppController as BaseController;
|
||||||
|
|
||||||
|
class AppController extends BaseController
|
||||||
|
{
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
parent::initialize();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags\Controller;
|
||||||
|
|
||||||
|
use Tags\Controller\AppController;
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
use Cake\Utility\Inflector;
|
||||||
|
use Cake\Utility\Text;
|
||||||
|
use Cake\Database\Expression\QueryExpression;
|
||||||
|
use Cake\Http\Exception\NotFoundException;
|
||||||
|
use Cake\Http\Exception\MethodNotAllowedException;
|
||||||
|
use Cake\Http\Exception\ForbiddenException;
|
||||||
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
|
class TagsController extends AppController
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->CRUD->index([
|
||||||
|
'filters' => ['label', 'colour'],
|
||||||
|
'quickFilters' => [['label' => true], 'colour']
|
||||||
|
]);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
$this->CRUD->add();
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->view($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->edit($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
$this->render('add');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->delete($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function tag($model, $id)
|
||||||
|
// {
|
||||||
|
// $controller = $this->getControllerBeingTagged($model);
|
||||||
|
// $controller->CRUD->tag($id);
|
||||||
|
// $responsePayload = $controller->CRUD->getResponsePayload();
|
||||||
|
// if (!empty($responsePayload)) {
|
||||||
|
// return $responsePayload;
|
||||||
|
// }
|
||||||
|
// return $controller->getResponse();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function untag($model, $id)
|
||||||
|
// {
|
||||||
|
// $controller = $this->getControllerBeingTagged($model);
|
||||||
|
// $controller->CRUD->untag($id);
|
||||||
|
// $responsePayload = $controller->CRUD->getResponsePayload();
|
||||||
|
// if (!empty($responsePayload)) {
|
||||||
|
// return $responsePayload;
|
||||||
|
// }
|
||||||
|
// return $controller->getResponse();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public function viewTags($model, $id)
|
||||||
|
// {
|
||||||
|
// $controller = $this->getControllerBeingTagged($model);
|
||||||
|
// $controller->CRUD->viewTags($id);
|
||||||
|
// $responsePayload = $controller->CRUD->getResponsePayload();
|
||||||
|
// if (!empty($responsePayload)) {
|
||||||
|
// return $responsePayload;
|
||||||
|
// }
|
||||||
|
// return $controller->getResponse();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private function getControllerBeingTagged($model)
|
||||||
|
// {
|
||||||
|
// $modelName = Inflector::camelize($model);
|
||||||
|
// $controllerName = "\\App\\Controller\\{$modelName}Controller";
|
||||||
|
// if (!class_exists($controllerName)) {
|
||||||
|
// throw new MethodNotAllowedException(__('Model `{0}` does not exists', $model));
|
||||||
|
// }
|
||||||
|
// $controller = new $controllerName;
|
||||||
|
// // Make sure that the request is correctly assigned to this controller
|
||||||
|
// return $controller;
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,355 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags\Model\Behavior;
|
||||||
|
|
||||||
|
use Cake\ORM\Behavior;
|
||||||
|
use Cake\ORM\Entity;
|
||||||
|
use Cake\ORM\Query;
|
||||||
|
use Cake\ORM\Table;
|
||||||
|
|
||||||
|
class TagBehavior extends Behavior
|
||||||
|
{
|
||||||
|
protected $_defaultConfig = [
|
||||||
|
'finderField' => 'label',
|
||||||
|
'tagsAssoc' => [
|
||||||
|
'className' => 'Tags.Tags',
|
||||||
|
'joinTable' => 'tags_tagged',
|
||||||
|
'foreignKey' => 'fk_id',
|
||||||
|
'targetForeignKey' => 'tag_id',
|
||||||
|
'propertyName' => 'tags',
|
||||||
|
],
|
||||||
|
'tagsCounter' => ['counter'],
|
||||||
|
'taggedAssoc' => [
|
||||||
|
'className' => 'Tags.Tagged',
|
||||||
|
'foreignKey' => 'fk_id'
|
||||||
|
],
|
||||||
|
'implementedEvents' => [
|
||||||
|
'Model.beforeMarshal' => 'beforeMarshal',
|
||||||
|
'Model.beforeFind' => 'beforeFind',
|
||||||
|
'Model.beforeSave' => 'beforeSave',
|
||||||
|
],
|
||||||
|
'implementedMethods' => [
|
||||||
|
'normalizeTags' => 'normalizeTags',
|
||||||
|
],
|
||||||
|
'implementedFinders' => [
|
||||||
|
'tagged' => 'findByTag',
|
||||||
|
'untagged' => 'findUntagged',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function initialize(array $config): void {
|
||||||
|
$this->bindAssociations();
|
||||||
|
$this->attachCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bindAssociations() {
|
||||||
|
$config = $this->getConfig();
|
||||||
|
$tagsAssoc = $config['tagsAssoc'];
|
||||||
|
$taggedAssoc = $config['taggedAssoc'];
|
||||||
|
|
||||||
|
$table = $this->_table;
|
||||||
|
$tableAlias = $this->_table->getAlias();
|
||||||
|
|
||||||
|
$assocConditions = ['Tagged.fk_model' => $tableAlias];
|
||||||
|
|
||||||
|
if (!$table->hasAssociation('Tagged')) {
|
||||||
|
$table->hasMany('Tagged', array_merge(
|
||||||
|
$taggedAssoc,
|
||||||
|
[
|
||||||
|
'conditions' => $assocConditions
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasAssociation('Tags')) {
|
||||||
|
$table->belongsToMany('Tags', array_merge(
|
||||||
|
$tagsAssoc,
|
||||||
|
[
|
||||||
|
'through' => $table->Tagged->getTarget(),
|
||||||
|
'conditions' => $assocConditions,
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->Tags->hasAssociation($tableAlias)) {
|
||||||
|
$table->Tags->belongsToMany($tableAlias, array_merge(
|
||||||
|
$tagsAssoc,
|
||||||
|
[
|
||||||
|
'className' => get_class($table),
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->Tagged->hasAssociation($tableAlias)) {
|
||||||
|
$table->Tagged->belongsTo($tableAlias, [
|
||||||
|
'className' => get_class($table),
|
||||||
|
'foreignKey' => $tagsAssoc['foreignKey'],
|
||||||
|
'conditions' => $assocConditions,
|
||||||
|
'joinType' => 'INNER',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->Tagged->hasAssociation($tableAlias . 'Tags')) {
|
||||||
|
$table->Tagged->belongsTo($tableAlias . 'Tags', [
|
||||||
|
'className' => $tagsAssoc['className'],
|
||||||
|
'foreignKey' => $tagsAssoc['targetForeignKey'],
|
||||||
|
'conditions' => $assocConditions,
|
||||||
|
'joinType' => 'INNER',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachCounters() {
|
||||||
|
$config = $this->getConfig();
|
||||||
|
$taggedTable = $this->_table->Tagged;
|
||||||
|
|
||||||
|
if (!$taggedTable->hasBehavior('CounterCache')) {
|
||||||
|
$taggedTable->addBehavior('CounterCache', [
|
||||||
|
'Tags' => $config['tagsCounter']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function beforeMarshal($event, $data, $options) {
|
||||||
|
$property = $this->getConfig('tagsAssoc.propertyName');
|
||||||
|
$options['accessibleFields'][$property] = true;
|
||||||
|
$options['associated']['Tags']['accessibleFields']['id'] = true;
|
||||||
|
|
||||||
|
if (isset($data['tags'])) {
|
||||||
|
if (!empty($data['tags'])) {
|
||||||
|
$data[$property] = $this->normalizeTags($data['tags']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function beforeSave($event, $entity, $options)
|
||||||
|
{
|
||||||
|
if (empty($entity->tags)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($entity->tags as $k => $tag) {
|
||||||
|
if (!$tag->isNew()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingTag = $this->getExistingTag($tag->label);
|
||||||
|
if (!$existing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$joinData = $tag->_joinData;
|
||||||
|
$tag = $existing;
|
||||||
|
$tag->_joinData = $joinData;
|
||||||
|
$entity->tags[$k] = $tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeTags($tags) {
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
$common = [
|
||||||
|
'_joinData' => [
|
||||||
|
'fk_model' => $modelAlias
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$tagsTable = $this->_table->Tags;
|
||||||
|
$displayField = $tagsTable->getDisplayField();
|
||||||
|
|
||||||
|
$tagIdentifiers = [];
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if (empty($tag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_object($tag)) {
|
||||||
|
$result[] = $tag->toArray();
|
||||||
|
}
|
||||||
|
$tagIdentifier = $this->getTagIdentifier($tag);
|
||||||
|
if (isset($tagIdentifiers[$tagIdentifier])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tagIdentifiers[$tagIdentifier] = true;
|
||||||
|
|
||||||
|
$existingTag = $this->getExistingTag($tagIdentifier);
|
||||||
|
if ($existingTag) {
|
||||||
|
$result[] = array_merge($common, ['id' => $existingTag->id]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = array_merge(
|
||||||
|
$common,
|
||||||
|
[
|
||||||
|
'label' => $tagIdentifier,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTagIdentifier($tag)
|
||||||
|
{
|
||||||
|
if (is_object($tag)) {
|
||||||
|
return $tag->label;
|
||||||
|
} else {
|
||||||
|
return trim($tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getExistingTag($tagName)
|
||||||
|
{
|
||||||
|
$tagsTable = $this->_table->Tags->getTarget();
|
||||||
|
$query = $tagsTable->find()->where([
|
||||||
|
'Tags.label' => $tagName
|
||||||
|
])
|
||||||
|
->select('Tags.id');
|
||||||
|
return $query->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByTag(Query $query, array $options) {
|
||||||
|
$finderField = $optionsKey = $this->getConfig('finderField');
|
||||||
|
if (!$finderField) {
|
||||||
|
$finderField = $optionsKey = 'label';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($options[$optionsKey])) {
|
||||||
|
throw new RuntimeException(__('Expected key `{0}` not present in find(\'tagged\') options argument.', $optionsKey));
|
||||||
|
}
|
||||||
|
$isAndOperator = isset($options['OperatorAND']) ? $options['OperatorAND'] : true;
|
||||||
|
$filterValue = $options[$optionsKey];
|
||||||
|
if (!$filterValue) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterValue = $this->dissectArgs($filterValue);
|
||||||
|
if (!empty($filterValue['NOT']) || !empty($filterValue['LIKE'])) {
|
||||||
|
return $this->findByComplexQueryConditions($query, $filterValue, $finderField, $isAndOperator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$subQuery = $this->buildQuerySnippet($filterValue, $finderField, $isAndOperator);
|
||||||
|
if (is_string($subQuery)) {
|
||||||
|
$query->matching('Tags', function ($q) use ($finderField, $subQuery) {
|
||||||
|
$key = 'Tags.' . $finderField;
|
||||||
|
return $q->where([
|
||||||
|
$key => $subQuery,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
return $query->where([$modelAlias . '.id IN' => $subQuery]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findUntagged(Query $query, array $options) {
|
||||||
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
|
||||||
|
$conditions = ['fk_model' => $modelAlias];
|
||||||
|
$this->_table->hasOne('NoTags', [
|
||||||
|
'className' => $this->getConfig('taggedAssoc.className'),
|
||||||
|
'foreignKey' => $foreignKey,
|
||||||
|
'conditions' => $conditions
|
||||||
|
]);
|
||||||
|
$query = $query->contain(['NoTags'])->where(['NoTags.id IS' => null]);
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function dissectArgs($filterValue): array
|
||||||
|
{
|
||||||
|
if (!is_array($filterValue)) {
|
||||||
|
return $filterValue;
|
||||||
|
}
|
||||||
|
$dissected = [
|
||||||
|
'AND' => [],
|
||||||
|
'NOT' => [],
|
||||||
|
'LIKE' => [],
|
||||||
|
];
|
||||||
|
foreach ($filterValue as $value) {
|
||||||
|
if (substr($value, 0, 1) == '!') {
|
||||||
|
$dissected['NOT'][] = substr($value, 1);
|
||||||
|
}
|
||||||
|
else if (strpos($value, '%') != false) {
|
||||||
|
$dissected['LIKE'][] = $value;
|
||||||
|
} else {
|
||||||
|
$dissected['AND'][] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($dissected['NOT']) && empty($dissected['LIKE'])) {
|
||||||
|
return $dissected['AND'];
|
||||||
|
}
|
||||||
|
return $dissected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildQuerySnippet($filterValue, string $finderField, bool $OperatorAND=true)
|
||||||
|
{
|
||||||
|
if (!is_array($filterValue)) {
|
||||||
|
return $filterValue;
|
||||||
|
}
|
||||||
|
$key = 'Tags.' . $finderField;
|
||||||
|
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
|
||||||
|
$conditions = [
|
||||||
|
$key . ' IN' => $filterValue,
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = $this->_table->Tagged->find();
|
||||||
|
if ($OperatorAND) {
|
||||||
|
$query->contain(['Tags'])
|
||||||
|
->group('Tagged.' . $foreignKey)
|
||||||
|
->having('COUNT(*) = ' . count($filterValue))
|
||||||
|
->select('Tagged.' . $foreignKey)
|
||||||
|
->where($conditions);
|
||||||
|
} else {
|
||||||
|
$query->contain(['Tags'])
|
||||||
|
->select('Tagged.' . $foreignKey)
|
||||||
|
->where($conditions);
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function findByComplexQueryConditions($query, $filterValue, string $finderField, bool $OperatorAND=true)
|
||||||
|
{
|
||||||
|
$key = 'Tags.' . $finderField;
|
||||||
|
$taggedAlias = 'Tagged';
|
||||||
|
$foreignKey = $this->getConfig('tagsAssoc.foreignKey');
|
||||||
|
|
||||||
|
if (!empty($filterValue['AND'])) {
|
||||||
|
$subQuery = $this->buildQuerySnippet($filterValue['AND'], $finderField, $OperatorAND);
|
||||||
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
$query->where([$modelAlias . '.id IN' => $subQuery]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filterValue['NOT'])) {
|
||||||
|
$subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, false);
|
||||||
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
$query->where([$modelAlias . '.id NOT IN' => $subQuery]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filterValue['LIKE'])) {
|
||||||
|
$conditions = ['OR' => []];
|
||||||
|
foreach($filterValue['LIKE'] as $likeValue) {
|
||||||
|
$conditions['OR'][] = [
|
||||||
|
$key . ' LIKE' => $likeValue,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$subQuery = $this->buildQuerySnippet($filterValue['NOT'], $finderField, $OperatorAND);
|
||||||
|
if ($OperatorAND) {
|
||||||
|
$subQuery = $this->_table->Tagged->find()
|
||||||
|
->contain(['Tags'])
|
||||||
|
->group('Tagged.' . $foreignKey)
|
||||||
|
->having('COUNT(*) >= ' . count($filterValue['LIKE']))
|
||||||
|
->select('Tagged.' . $foreignKey)
|
||||||
|
->where($conditions);
|
||||||
|
} else {
|
||||||
|
$subQuery = $this->_table->Tagged->find()
|
||||||
|
->contain(['Tags'])
|
||||||
|
->select('Tagged.' . $foreignKey)
|
||||||
|
->where($conditions);
|
||||||
|
}
|
||||||
|
$modelAlias = $this->_table->getAlias();
|
||||||
|
$query->where([$modelAlias . '.id IN' => $subQuery]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Entity;
|
||||||
|
|
||||||
|
use App\Model\Entity\AppModel;
|
||||||
|
|
||||||
|
class Tag extends AppModel {
|
||||||
|
|
||||||
|
protected $_accessible = [
|
||||||
|
'id' => false,
|
||||||
|
'counter' => false,
|
||||||
|
'*' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $_accessibleOnNew = [
|
||||||
|
'label' => true,
|
||||||
|
'colour' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $_virtual = ['text_colour'];
|
||||||
|
|
||||||
|
protected function _getTextColour()
|
||||||
|
{
|
||||||
|
$textColour = null;
|
||||||
|
if (!empty($this->colour)) {
|
||||||
|
$textColour = $this->getTextColour($this->colour);
|
||||||
|
}
|
||||||
|
return $textColour;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTextColour($RGB) {
|
||||||
|
$r = hexdec(substr($RGB, 1, 2));
|
||||||
|
$g = hexdec(substr($RGB, 3, 2));
|
||||||
|
$b = hexdec(substr($RGB, 5, 2));
|
||||||
|
$average = ((2 * $r) + $b + (3 * $g))/6;
|
||||||
|
if ($average < 127) {
|
||||||
|
return 'white';
|
||||||
|
} else {
|
||||||
|
return 'black';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Entity;
|
||||||
|
|
||||||
|
use Cake\ORM\Entity;
|
||||||
|
|
||||||
|
class Tagged extends AppModel {
|
||||||
|
|
||||||
|
protected $_accessible = [
|
||||||
|
'id' => false,
|
||||||
|
'*' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags\Model\Table;
|
||||||
|
|
||||||
|
use App\Model\Table\AppTable;
|
||||||
|
use Cake\Validation\Validator;
|
||||||
|
|
||||||
|
class TaggedTable extends AppTable
|
||||||
|
{
|
||||||
|
protected $_accessible = [
|
||||||
|
'id' => false
|
||||||
|
];
|
||||||
|
|
||||||
|
public function initialize(array $config): void
|
||||||
|
{
|
||||||
|
$this->setTable('tags_tagged');
|
||||||
|
$this->belongsTo('Tags', [
|
||||||
|
'className' => 'Tags.Tags',
|
||||||
|
'foreignKey' => 'tag_id',
|
||||||
|
'propertyName' => 'tag',
|
||||||
|
]);
|
||||||
|
$this->addBehavior('Timestamp');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validationDefault(Validator $validator): Validator
|
||||||
|
{
|
||||||
|
$validator
|
||||||
|
->notBlank('fk_model')
|
||||||
|
->notBlank('fk_id')
|
||||||
|
->notBlank('tag_id');
|
||||||
|
return $validator;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags\Model\Table;
|
||||||
|
|
||||||
|
use App\Model\Table\AppTable;
|
||||||
|
use Cake\Validation\Validator;
|
||||||
|
|
||||||
|
class TagsTable extends AppTable
|
||||||
|
{
|
||||||
|
protected $_accessible = [
|
||||||
|
'id' => false
|
||||||
|
];
|
||||||
|
|
||||||
|
public function initialize(array $config): void
|
||||||
|
{
|
||||||
|
$this->setTable('tags_tags');
|
||||||
|
$this->setDisplayField('label'); // Change to name?
|
||||||
|
$this->addBehavior('Timestamp');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validationDefault(Validator $validator): Validator
|
||||||
|
{
|
||||||
|
$validator
|
||||||
|
->notBlank('label');
|
||||||
|
return $validator;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags;
|
||||||
|
|
||||||
|
use Cake\Core\BasePlugin;
|
||||||
|
use Cake\Core\PluginApplicationInterface;
|
||||||
|
use Cake\Console\CommandCollection;
|
||||||
|
use Cake\Http\MiddlewareQueue;
|
||||||
|
|
||||||
|
class Plugin extends BasePlugin
|
||||||
|
{
|
||||||
|
public function middleware(MiddlewareQueue $middleware): MiddlewareQueue
|
||||||
|
{
|
||||||
|
// Add middleware here.
|
||||||
|
$middleware = parent::middleware($middleware);
|
||||||
|
|
||||||
|
return $middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function console(CommandCollection $commands): CommandCollection
|
||||||
|
{
|
||||||
|
// Add console commands here.
|
||||||
|
$commands = parent::console($commands);
|
||||||
|
|
||||||
|
return $commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bootstrap(PluginApplicationInterface $app): void
|
||||||
|
{
|
||||||
|
// Add constants, load configuration defaults.
|
||||||
|
// By default will load `config/bootstrap.php` in the plugin.
|
||||||
|
parent::bootstrap($app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function routes($routes): void
|
||||||
|
{
|
||||||
|
// Add routes.
|
||||||
|
// By default will load `config/routes.php` in the plugin.
|
||||||
|
parent::routes($routes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tags\View\Helper;
|
||||||
|
|
||||||
|
use Cake\View\Helper;
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
|
||||||
|
class TagHelper extends Helper
|
||||||
|
{
|
||||||
|
public $helpers = [
|
||||||
|
'Bootstrap',
|
||||||
|
'TextColour',
|
||||||
|
'FontAwesome',
|
||||||
|
'Form',
|
||||||
|
'Url',
|
||||||
|
'Tags.Tag',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $defaultConfig = [
|
||||||
|
'default_colour' => '#924da6',
|
||||||
|
'picker' => false,
|
||||||
|
'editable' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function control(array $options = [])
|
||||||
|
{
|
||||||
|
$field = 'tag_list';
|
||||||
|
$values = !empty($options['allTags']) ? array_map(function($tag) {
|
||||||
|
return [
|
||||||
|
'text' => h($tag['label']),
|
||||||
|
'value' => h($tag['label']),
|
||||||
|
'data-colour' => h($tag['colour']),
|
||||||
|
'data-text-colour' => h($tag['text_colour']),
|
||||||
|
];
|
||||||
|
}, $options['allTags']) : [];
|
||||||
|
$classes = ['tag-input', 'flex-grow-1'];
|
||||||
|
$url = '';
|
||||||
|
if (!empty($this->getConfig('editable'))) {
|
||||||
|
$url = $this->Url->build([
|
||||||
|
'controller' => $this->getView()->getName(),
|
||||||
|
'action' => 'tag',
|
||||||
|
$this->getView()->get('entity')['id']
|
||||||
|
]);
|
||||||
|
$classes[] = 'd-none';
|
||||||
|
}
|
||||||
|
$selectConfig = [
|
||||||
|
'multiple' => true,
|
||||||
|
'class' => $classes,
|
||||||
|
'data-url' => $url,
|
||||||
|
];
|
||||||
|
return $this->Form->select($field, $values, $selectConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function picker(array $options = [])
|
||||||
|
{
|
||||||
|
$html = $this->Tag->control($options);
|
||||||
|
if (!empty($this->getConfig('editable'))) {
|
||||||
|
$html .= $this->Bootstrap->button([
|
||||||
|
'size' => 'sm',
|
||||||
|
'icon' => 'plus',
|
||||||
|
'variant' => 'secondary',
|
||||||
|
'class' => ['badge'],
|
||||||
|
'params' => [
|
||||||
|
'onclick' => 'createTagPicker(this)',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$html .= '<script>$(document).ready(function() { initSelect2Pickers() })</script>';
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tags(array $tags = [], array $options = [])
|
||||||
|
{
|
||||||
|
$this->_config = array_merge($this->defaultConfig, $options);
|
||||||
|
$html = '<div class="tag-container-wrapper">';
|
||||||
|
$html .= '<div class="tag-container my-1 d-flex">';
|
||||||
|
$html .= '<div class="tag-list d-inline-block">';
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if (is_object($tag)) {
|
||||||
|
$html .= $this->tag($tag);
|
||||||
|
} else {
|
||||||
|
$html .= $this->tag([
|
||||||
|
'label' => $tag
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
if (!empty($this->getConfig('picker'))) {
|
||||||
|
$html .= $this->picker($options);
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tag($tag, array $options = [])
|
||||||
|
{
|
||||||
|
if (empty($this->_config)) {
|
||||||
|
$this->_config = array_merge($this->defaultConfig, $options);
|
||||||
|
}
|
||||||
|
$tag['colour'] = !empty($tag['colour']) ? $tag['colour'] : $this->getConfig('default_colour');
|
||||||
|
$textColour = !empty($tag['text_colour']) ? $tag['text_colour'] : $this->TextColour->getTextColour(h($tag['colour']));;
|
||||||
|
|
||||||
|
if (!empty($this->getConfig('editable'))) {
|
||||||
|
$deleteButton = $this->Bootstrap->button([
|
||||||
|
'size' => 'sm',
|
||||||
|
'icon' => 'times',
|
||||||
|
'class' => ['ml-1', 'border-0', "text-${textColour}"],
|
||||||
|
'variant' => 'text',
|
||||||
|
'title' => __('Delete tag'),
|
||||||
|
'params' => [
|
||||||
|
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
||||||
|
$this->Url->build([
|
||||||
|
'controller' => $this->getView()->getName(),
|
||||||
|
'action' => 'untag',
|
||||||
|
$this->getView()->get('entity')['id']
|
||||||
|
]),
|
||||||
|
h($tag['label'])
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$deleteButton = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = $this->Bootstrap->genNode('span', [
|
||||||
|
'class' => [
|
||||||
|
'tag',
|
||||||
|
'badge',
|
||||||
|
'mx-1',
|
||||||
|
'align-middle',
|
||||||
|
],
|
||||||
|
'title' => h($tag['label']),
|
||||||
|
'style' => sprintf('color:%s; background-color:%s', $textColour, h($tag['colour'])),
|
||||||
|
], h($tag['label']) . $deleteButton);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
echo $this->element('genericElements/Form/genericForm', array(
|
||||||
|
'data' => array(
|
||||||
|
'description' => __('Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in genral require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them.'),
|
||||||
|
'model' => 'Organisations',
|
||||||
|
'fields' => array(
|
||||||
|
array(
|
||||||
|
'field' => 'label'
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'field' => 'colour',
|
||||||
|
'type' => 'color',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
|
||||||
|
'submit' => array(
|
||||||
|
'action' => $this->request->getParam('action')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
</div>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
|
'data' => [
|
||||||
|
'data' => $data,
|
||||||
|
'top_bar' => [
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'type' => 'simple',
|
||||||
|
'children' => [
|
||||||
|
'data' => [
|
||||||
|
'type' => 'simple',
|
||||||
|
'text' => __('Add tag'),
|
||||||
|
'popover_url' => '/tags/add'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'context_filters',
|
||||||
|
'context_filters' => $filteringContexts
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'search',
|
||||||
|
'button' => __('Filter'),
|
||||||
|
'placeholder' => __('Enter value to search'),
|
||||||
|
'data' => '',
|
||||||
|
'searchKey' => 'value'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'name' => '#',
|
||||||
|
'sort' => 'id',
|
||||||
|
'data_path' => 'id',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Label'),
|
||||||
|
'sort' => 'label',
|
||||||
|
'element' => 'tag'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Counter'),
|
||||||
|
'sort' => 'couter',
|
||||||
|
'data_path' => 'counter',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Colour'),
|
||||||
|
'sort' => 'colour',
|
||||||
|
'data_path' => 'colour',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Created'),
|
||||||
|
'sort' => 'created',
|
||||||
|
'data_path' => 'created',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'title' => __('Tag index'),
|
||||||
|
'description' => __('The list of all tags existing on this instance'),
|
||||||
|
'actions' => [
|
||||||
|
[
|
||||||
|
'url' => '/tags/view',
|
||||||
|
'url_params_data_paths' => ['id'],
|
||||||
|
'icon' => 'eye'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'open_modal' => '/tags/edit/[onclick_params_data_path]',
|
||||||
|
'modal_params_data_path' => 'id',
|
||||||
|
'icon' => 'edit'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'open_modal' => '/tags/delete/[onclick_params_data_path]',
|
||||||
|
'modal_params_data_path' => 'id',
|
||||||
|
'icon' => 'trash'
|
||||||
|
],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
echo '</div>';
|
||||||
|
?>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
echo $this->element(
|
||||||
|
'/genericElements/SingleViews/single_view',
|
||||||
|
[
|
||||||
|
'data' => $entity,
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'key' => __('ID'),
|
||||||
|
'path' => 'id'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Label'),
|
||||||
|
'path' => '',
|
||||||
|
'type' => 'tag',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Counter'),
|
||||||
|
'path' => 'counter',
|
||||||
|
'type' => 'string',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Colour'),
|
||||||
|
'path' => 'colour',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Created'),
|
||||||
|
'path' => 'created',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'children' => []
|
||||||
|
]
|
||||||
|
);
|
|
@ -0,0 +1,3 @@
|
||||||
|
.tag {
|
||||||
|
filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
function createTagPicker(clicked) {
|
||||||
|
|
||||||
|
function closePicker($select, $container) {
|
||||||
|
$select.appendTo($container)
|
||||||
|
$container.parent().find('.picker-container').remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditableButtons($select, $container) {
|
||||||
|
const $saveButton = $('<button></button>').addClass(['btn btn-primary btn-sm', 'align-self-start']).attr('type', 'button')
|
||||||
|
.append($('<span></span>').text('Save').addClass('text-nowrap').prepend($('<i></i>').addClass('fa fa-save mr-1')))
|
||||||
|
.click(function() {
|
||||||
|
const tags = $select.select2('data').map(tag => tag.text)
|
||||||
|
addTags($select.data('url'), tags, $(this))
|
||||||
|
})
|
||||||
|
const $cancelButton = $('<button></button>').addClass(['btn btn-secondary btn-sm', 'align-self-start']).attr('type', 'button')
|
||||||
|
.append($('<span></span>').text('Cancel').addClass('text-nowrap').prepend($('<i></i>').addClass('fa fa-times mr-1')))
|
||||||
|
.click(function() {
|
||||||
|
closePicker($select, $container)
|
||||||
|
})
|
||||||
|
const $buttons = $('<span></span>').addClass(['picker-action', 'btn-group']).append($saveButton, $cancelButton)
|
||||||
|
return $buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
const $clicked = $(clicked)
|
||||||
|
const $container = $clicked.closest('.tag-container')
|
||||||
|
const $select = $container.parent().find('select.tag-input').removeClass('d-none')
|
||||||
|
closePicker($select, $container)
|
||||||
|
const $pickerContainer = $('<div></div>').addClass(['picker-container', 'd-flex'])
|
||||||
|
|
||||||
|
$select.prependTo($pickerContainer)
|
||||||
|
$pickerContainer.append(getEditableButtons($select, $container))
|
||||||
|
$container.parent().append($pickerContainer)
|
||||||
|
initSelect2Picker($select)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTag(url, tags, clicked) {
|
||||||
|
if (!Array.isArray(tags)) {
|
||||||
|
tags = [tags];
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
tag_list: JSON.stringify(tags)
|
||||||
|
}
|
||||||
|
const $statusNode = $(clicked).closest('.tag')
|
||||||
|
const APIOptions = {
|
||||||
|
statusNode: $statusNode,
|
||||||
|
skipFeedback: true,
|
||||||
|
}
|
||||||
|
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((apiResult) => {
|
||||||
|
let $container = $statusNode.closest('.tag-container-wrapper')
|
||||||
|
refreshTagList(apiResult, $container).then(($tagContainer) => {
|
||||||
|
$container = $tagContainer // old container might not exist anymore since it was replaced after the refresh
|
||||||
|
})
|
||||||
|
const theToast = UI.toast({
|
||||||
|
variant: 'success',
|
||||||
|
title: apiResult.message,
|
||||||
|
bodyHtml: $('<div/>').append(
|
||||||
|
$('<span/>').text('Cancel untag operation.'),
|
||||||
|
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ml-3']).text('Restore tag').click(function() {
|
||||||
|
const split = url.split('/')
|
||||||
|
const controllerName = split[1]
|
||||||
|
const id = split[3]
|
||||||
|
const urlRetag = `/${controllerName}/tag/${id}`
|
||||||
|
addTags(urlRetag, tags, $container.find('.tag-container')).then(() => {
|
||||||
|
theToast.removeToast()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}).catch((e) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTags(url, tags, $statusNode) {
|
||||||
|
const data = {
|
||||||
|
tag_list: JSON.stringify(tags)
|
||||||
|
}
|
||||||
|
const APIOptions = {
|
||||||
|
statusNode: $statusNode
|
||||||
|
}
|
||||||
|
return AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((apiResult) => {
|
||||||
|
const $container = $statusNode.closest('.tag-container-wrapper')
|
||||||
|
refreshTagList(apiResult, $container)
|
||||||
|
}).catch((e) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTagList(apiResult, $container) {
|
||||||
|
const controllerName = apiResult.url.split('/')[1]
|
||||||
|
const entityId = apiResult.data.id
|
||||||
|
const url = `/${controllerName}/viewTags/${entityId}`
|
||||||
|
return UI.reload(url, $container)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSelect2Pickers() {
|
||||||
|
$('select.tag-input').each(function() {
|
||||||
|
if (!$(this).hasClass("select2-hidden-accessible")) {
|
||||||
|
initSelect2Picker($(this))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSelect2Picker($select) {
|
||||||
|
|
||||||
|
function templateTag(state, $select) {
|
||||||
|
if (!state.id) {
|
||||||
|
return state.label;
|
||||||
|
}
|
||||||
|
if (state.colour === undefined) {
|
||||||
|
state.colour = $(state.element).data('colour')
|
||||||
|
}
|
||||||
|
if ($select !== undefined && state.text[0] === '!') {
|
||||||
|
// fetch corresponding tag and set colors?
|
||||||
|
// const baseTag = state.text.slice(1)
|
||||||
|
// const existingBaseTag = $select.find('option').filter(function() {
|
||||||
|
// return $(this).val() === baseTag
|
||||||
|
// })
|
||||||
|
// if (existingBaseTag.length > 0) {
|
||||||
|
// state.colour = existingBaseTag.data('colour')
|
||||||
|
// state.text = baseTag
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return buildTag(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
$select.select2({
|
||||||
|
placeholder: 'Pick a tag',
|
||||||
|
tags: true,
|
||||||
|
width: '100%',
|
||||||
|
templateResult: (state) => templateTag(state),
|
||||||
|
templateSelection: (state) => templateTag(state, $select),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTag(options={}) {
|
||||||
|
if (!options.colour) {
|
||||||
|
options.colour = '#924da6'
|
||||||
|
}
|
||||||
|
const $tag = $('<span/>')
|
||||||
|
.addClass(['tag', 'badge', 'align-text-top'])
|
||||||
|
.css({color: getTextColour(options.colour), 'background-color': options.colour})
|
||||||
|
.text(options.text)
|
||||||
|
|
||||||
|
return $tag
|
||||||
|
}
|
|
@ -29,6 +29,9 @@ use Authentication\AuthenticationServiceInterface;
|
||||||
use Authentication\AuthenticationServiceProviderInterface;
|
use Authentication\AuthenticationServiceProviderInterface;
|
||||||
use Authentication\Middleware\AuthenticationMiddleware;
|
use Authentication\Middleware\AuthenticationMiddleware;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
use Tags\Plugin as TagsPlugin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application setup class.
|
* Application setup class.
|
||||||
*
|
*
|
||||||
|
@ -59,6 +62,7 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
|
||||||
$this->addPlugin('DebugKit');
|
$this->addPlugin('DebugKit');
|
||||||
}
|
}
|
||||||
$this->addPlugin('Authentication');
|
$this->addPlugin('Authentication');
|
||||||
|
$this->addPlugin('Tags', ['routes' => true]);
|
||||||
// Load more plugins here
|
// Load more plugins here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,9 +63,10 @@ class AppController extends Controller
|
||||||
]);
|
]);
|
||||||
$this->loadModel('MetaFields');
|
$this->loadModel('MetaFields');
|
||||||
$this->loadModel('MetaTemplates');
|
$this->loadModel('MetaTemplates');
|
||||||
|
$table = $this->getTableLocator()->get($this->modelClass);
|
||||||
$this->loadComponent('CRUD', [
|
$this->loadComponent('CRUD', [
|
||||||
'request' => $this->request,
|
'request' => $this->request,
|
||||||
'table' => $this->{$this->modelClass},
|
'table' => $table,
|
||||||
'MetaFields' => $this->MetaFields,
|
'MetaFields' => $this->MetaFields,
|
||||||
'MetaTemplates' => $this->MetaTemplates
|
'MetaTemplates' => $this->MetaTemplates
|
||||||
]);
|
]);
|
||||||
|
@ -126,6 +127,10 @@ class AppController extends Controller
|
||||||
$this->request->getParam('prefix');
|
$this->request->getParam('prefix');
|
||||||
$this->set('darkMode', !empty(Configure::read('Cerebrate')['ui.dark']));
|
$this->set('darkMode', !empty(Configure::read('Cerebrate')['ui.dark']));
|
||||||
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
|
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
|
||||||
|
|
||||||
|
if ($this->modelClass == 'Tags.Tags') {
|
||||||
|
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function authApiUser(): void
|
private function authApiUser(): void
|
||||||
|
|
|
@ -747,6 +747,30 @@ class ACLComponent extends Component
|
||||||
'label' => __('List Outbox Processors'),
|
'label' => __('List Outbox Processors'),
|
||||||
'skipTopMenu' => 1
|
'skipTopMenu' => 1
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
'skipTopMenu' => true,
|
||||||
|
],
|
||||||
|
'Tags' => [
|
||||||
|
'label' => __('Tags'),
|
||||||
|
'url' => '/tags/index',
|
||||||
|
'children' => [
|
||||||
|
'index' => [
|
||||||
|
'url' => '/tags/index',
|
||||||
|
'label' => __('List Tags'),
|
||||||
|
],
|
||||||
|
'view' => [
|
||||||
|
'url' => '/tags/view/{{id}}',
|
||||||
|
'label' => __('View Tags'),
|
||||||
|
'actions' => ['delete', 'edit', 'view'],
|
||||||
|
'skipTopMenu' => true,
|
||||||
|
],
|
||||||
|
'delete' => [
|
||||||
|
'url' => '/tags/delete/{{id}}',
|
||||||
|
'label' => __('Delete Tag'),
|
||||||
|
'actions' => ['delete', 'edit', 'view'],
|
||||||
|
'skipTopMenu' => true,
|
||||||
|
'popup' => 1
|
||||||
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'MetaTemplates' => [
|
'MetaTemplates' => [
|
||||||
|
|
|
@ -7,6 +7,7 @@ use Cake\Error\Debugger;
|
||||||
use Cake\Utility\Hash;
|
use Cake\Utility\Hash;
|
||||||
use Cake\Utility\Inflector;
|
use Cake\Utility\Inflector;
|
||||||
use Cake\View\ViewBuilder;
|
use Cake\View\ViewBuilder;
|
||||||
|
use Cake\ORM\TableRegistry;
|
||||||
use Cake\Http\Exception\MethodNotAllowedException;
|
use Cake\Http\Exception\MethodNotAllowedException;
|
||||||
use Cake\Http\Exception\NotFoundException;
|
use Cake\Http\Exception\NotFoundException;
|
||||||
|
|
||||||
|
@ -34,6 +35,9 @@ class CRUDComponent extends Component
|
||||||
$options['filters'][] = 'quickFilter';
|
$options['filters'][] = 'quickFilter';
|
||||||
}
|
}
|
||||||
$options['filters'][] = 'filteringLabel';
|
$options['filters'][] = 'filteringLabel';
|
||||||
|
if ($this->taggingSupported()) {
|
||||||
|
$options['filters'][] = 'filteringTags';
|
||||||
|
}
|
||||||
|
|
||||||
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
|
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
|
||||||
foreach ($optionFilters as $i => $filter) {
|
foreach ($optionFilters as $i => $filter) {
|
||||||
|
@ -49,6 +53,9 @@ class CRUDComponent extends Component
|
||||||
if (!empty($options['contain'])) {
|
if (!empty($options['contain'])) {
|
||||||
$query->contain($options['contain']);
|
$query->contain($options['contain']);
|
||||||
}
|
}
|
||||||
|
if ($this->taggingSupported()) {
|
||||||
|
$query->contain('Tags');
|
||||||
|
}
|
||||||
if (!empty($options['fields'])) {
|
if (!empty($options['fields'])) {
|
||||||
$query->select($options['fields']);
|
$query->select($options['fields']);
|
||||||
}
|
}
|
||||||
|
@ -72,15 +79,17 @@ class CRUDComponent extends Component
|
||||||
$data = $this->Table->{$options['afterFind']}($data);
|
$data = $this->Table->{$options['afterFind']}($data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty($options['contextFilters'])) {
|
$this->setFilteringContext($options['contextFilters'] ?? [], $params);
|
||||||
$this->setFilteringContext($options['contextFilters'], $params);
|
|
||||||
}
|
|
||||||
$this->Controller->set('data', $data);
|
$this->Controller->set('data', $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filtering(): void
|
public function filtering(): void
|
||||||
{
|
{
|
||||||
|
if ($this->taggingSupported()) {
|
||||||
|
$this->Controller->set('taggingEnabled', true);
|
||||||
|
$this->setAllTags();
|
||||||
|
}
|
||||||
$filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
|
$filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
|
||||||
$this->Controller->set('filters', $filters);
|
$this->Controller->set('filters', $filters);
|
||||||
$this->Controller->viewBuilder()->setLayout('ajax');
|
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||||
|
@ -243,7 +252,11 @@ class CRUDComponent extends Component
|
||||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||||
}
|
}
|
||||||
$this->getMetaTemplates();
|
$this->getMetaTemplates();
|
||||||
$data = $this->Table->get($id, isset($params['get']) ? $params['get'] : []);
|
if ($this->taggingSupported()) {
|
||||||
|
$params['contain'][] = 'Tags';
|
||||||
|
$this->setAllTags();
|
||||||
|
}
|
||||||
|
$data = $this->Table->get($id, isset($params['get']) ? $params['get'] : $params);
|
||||||
$data = $this->getMetaFields($id, $data);
|
$data = $this->getMetaFields($id, $data);
|
||||||
if (!empty($params['fields'])) {
|
if (!empty($params['fields'])) {
|
||||||
$this->Controller->set('fields', $params['fields']);
|
$this->Controller->set('fields', $params['fields']);
|
||||||
|
@ -350,6 +363,11 @@ class CRUDComponent extends Component
|
||||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->taggingSupported()) {
|
||||||
|
$params['contain'][] = 'Tags';
|
||||||
|
$this->setAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
$data = $this->Table->get($id, $params);
|
$data = $this->Table->get($id, $params);
|
||||||
$data = $this->attachMetaData($id, $data);
|
$data = $this->attachMetaData($id, $data);
|
||||||
if (isset($params['afterFind'])) {
|
if (isset($params['afterFind'])) {
|
||||||
|
@ -404,6 +422,149 @@ class CRUDComponent extends Component
|
||||||
$this->Controller->render('/genericTemplates/delete');
|
$this->Controller->render('/genericTemplates/delete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tag($id=false): void
|
||||||
|
{
|
||||||
|
if (!$this->taggingSupported()) {
|
||||||
|
throw new Exception("Table {$this->TableAlias} does not support tagging");
|
||||||
|
}
|
||||||
|
if ($this->request->is('get')) {
|
||||||
|
$this->setAllTags();
|
||||||
|
if(!empty($id)) {
|
||||||
|
$params = [
|
||||||
|
'contain' => 'Tags',
|
||||||
|
];
|
||||||
|
$entity = $this->Table->get($id, $params);
|
||||||
|
$this->Controller->set('id', $entity->id);
|
||||||
|
$this->Controller->set('data', $entity);
|
||||||
|
$this->Controller->set('bulkEnabled', false);
|
||||||
|
} else {
|
||||||
|
$this->Controller->set('bulkEnabled', true);
|
||||||
|
}
|
||||||
|
} else if ($this->request->is('post') || $this->request->is('delete')) {
|
||||||
|
$ids = $this->getIdsOrFail($id);
|
||||||
|
$isBulk = count($ids) > 1;
|
||||||
|
$bulkSuccesses = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$params = [
|
||||||
|
'contain' => 'Tags',
|
||||||
|
];
|
||||||
|
$entity = $this->Table->get($id, $params);
|
||||||
|
$input = $this->request->getData();
|
||||||
|
$tagsToAdd = json_decode($input['tag_list']);
|
||||||
|
// patching will mirror tag in the DB, however, we only want to add tags
|
||||||
|
$input['tags'] = array_merge($tagsToAdd, $entity->tags);
|
||||||
|
$patchEntityParams = [
|
||||||
|
'fields' => ['tags'],
|
||||||
|
];
|
||||||
|
$entity = $this->Table->patchEntity($entity, $input, $patchEntityParams);
|
||||||
|
$savedData = $this->Table->save($entity);
|
||||||
|
$success = true;
|
||||||
|
if ($success) {
|
||||||
|
$bulkSuccesses++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$message = $this->getMessageBasedOnResult(
|
||||||
|
$bulkSuccesses == count($ids),
|
||||||
|
$isBulk,
|
||||||
|
__('{0} tagged with `{1}`.', $this->ObjectAlias, $input['tag_list']),
|
||||||
|
__('All {0} have been tagged.', Inflector::pluralize($this->ObjectAlias)),
|
||||||
|
__('Could not tag {0} with `{1}`.', $this->ObjectAlias, $input['tag_list']),
|
||||||
|
__('{0} / {1} {2} have been tagged.',
|
||||||
|
$bulkSuccesses,
|
||||||
|
count($ids),
|
||||||
|
Inflector::pluralize($this->ObjectAlias)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->setResponseForController('tag', $bulkSuccesses, $message, $savedData);
|
||||||
|
}
|
||||||
|
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||||
|
$this->Controller->render('/genericTemplates/tagForm');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function untag($id=false): void
|
||||||
|
{
|
||||||
|
if (!$this->taggingSupported()) {
|
||||||
|
throw new Exception("Table {$this->TableAlias} does not support tagging");
|
||||||
|
}
|
||||||
|
if ($this->request->is('get')) {
|
||||||
|
$this->setAllTags();
|
||||||
|
if(!empty($id)) {
|
||||||
|
$params = [
|
||||||
|
'contain' => 'Tags',
|
||||||
|
];
|
||||||
|
$entity = $this->Table->get($id, $params);
|
||||||
|
$this->Controller->set('id', $entity->id);
|
||||||
|
$this->Controller->set('data', $entity);
|
||||||
|
$this->Controller->set('bulkEnabled', false);
|
||||||
|
} else {
|
||||||
|
$this->Controller->set('bulkEnabled', true);
|
||||||
|
}
|
||||||
|
} else if ($this->request->is('post') || $this->request->is('delete')) {
|
||||||
|
$ids = $this->getIdsOrFail($id);
|
||||||
|
$isBulk = count($ids) > 1;
|
||||||
|
$bulkSuccesses = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$params = [
|
||||||
|
'contain' => 'Tags',
|
||||||
|
];
|
||||||
|
$entity = $this->Table->get($id, $params);
|
||||||
|
$input = $this->request->getData();
|
||||||
|
$tagsToRemove = json_decode($input['tag_list']);
|
||||||
|
// patching will mirror tag in the DB, however, we only want to remove tags
|
||||||
|
$input['tags'] = array_filter($entity->tags, function ($existingTag) use ($tagsToRemove) {
|
||||||
|
return !in_array($existingTag->label, $tagsToRemove);
|
||||||
|
});
|
||||||
|
$patchEntityParams = [
|
||||||
|
'fields' => ['tags'],
|
||||||
|
];
|
||||||
|
$entity = $this->Table->patchEntity($entity, $input, $patchEntityParams);
|
||||||
|
$savedData = $this->Table->save($entity);
|
||||||
|
$success = true;
|
||||||
|
if ($success) {
|
||||||
|
$bulkSuccesses++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$message = $this->getMessageBasedOnResult(
|
||||||
|
$bulkSuccesses == count($ids),
|
||||||
|
$isBulk,
|
||||||
|
__('{0} untagged with `{1}`.', $this->ObjectAlias, implode(', ', $tagsToRemove)),
|
||||||
|
__('All {0} have been untagged.', Inflector::pluralize($this->ObjectAlias)),
|
||||||
|
__('Could not untag {0} with `{1}`.', $this->ObjectAlias, $input['tag_list']),
|
||||||
|
__('{0} / {1} {2} have been untagged.',
|
||||||
|
$bulkSuccesses,
|
||||||
|
count($ids),
|
||||||
|
Inflector::pluralize($this->ObjectAlias)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->setResponseForController('tag', $bulkSuccesses, $message, $entity);
|
||||||
|
}
|
||||||
|
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||||
|
$this->Controller->render('/genericTemplates/tagForm');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewTags(int $id, array $params = []): void
|
||||||
|
{
|
||||||
|
if (!$this->taggingSupported()) {
|
||||||
|
throw new Exception("Table {$this->TableAlias} does not support tagging");
|
||||||
|
}
|
||||||
|
if (empty($id)) {
|
||||||
|
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||||
|
}
|
||||||
|
|
||||||
|
$params['contain'][] = 'Tags';
|
||||||
|
$data = $this->Table->get($id, $params);
|
||||||
|
if (isset($params['afterFind'])) {
|
||||||
|
$data = $params['afterFind']($data);
|
||||||
|
}
|
||||||
|
if ($this->Controller->ParamHandler->isRest()) {
|
||||||
|
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||||
|
}
|
||||||
|
$this->Controller->set('entity', $data);
|
||||||
|
$this->setAllTags();
|
||||||
|
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||||
|
$this->Controller->render('/genericTemplates/tag');
|
||||||
|
}
|
||||||
|
|
||||||
public function setResponseForController($action, $success, $message, $data=[], $errors=null)
|
public function setResponseForController($action, $success, $message, $data=[], $errors=null)
|
||||||
{
|
{
|
||||||
if ($success) {
|
if ($success) {
|
||||||
|
@ -522,6 +683,8 @@ class CRUDComponent extends Component
|
||||||
{
|
{
|
||||||
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';
|
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';
|
||||||
unset($params['filteringLabel']);
|
unset($params['filteringLabel']);
|
||||||
|
$filteringTags = !empty($params['filteringTags']) && $this->taggingSupported() ? $params['filteringTags'] : '';
|
||||||
|
unset($params['filteringTags']);
|
||||||
$customFilteringFunction = '';
|
$customFilteringFunction = '';
|
||||||
$chosenFilter = '';
|
$chosenFilter = '';
|
||||||
if (!empty($options['contextFilters']['custom'])) {
|
if (!empty($options['contextFilters']['custom'])) {
|
||||||
|
@ -565,10 +728,26 @@ class CRUDComponent extends Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->taggingSupported() && !empty($filteringTags)) {
|
||||||
|
$activeFilters['filteringTags'] = $filteringTags;
|
||||||
|
$query = $this->setTagFilters($query, $filteringTags);
|
||||||
|
}
|
||||||
|
|
||||||
$this->Controller->set('activeFilters', $activeFilters);
|
$this->Controller->set('activeFilters', $activeFilters);
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function setTagFilters($query, $tags)
|
||||||
|
{
|
||||||
|
$modelAlias = $this->Table->getAlias();
|
||||||
|
$subQuery = $this->Table->find('tagged', [
|
||||||
|
'label' => $tags,
|
||||||
|
'forceAnd' => true
|
||||||
|
])->select($modelAlias . '.id');
|
||||||
|
return $query->where([$modelAlias . '.id IN' => $subQuery]);
|
||||||
|
}
|
||||||
|
|
||||||
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
|
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
|
||||||
{
|
{
|
||||||
$modelName = $filterParts[0];
|
$modelName = $filterParts[0];
|
||||||
|
@ -683,6 +862,18 @@ class CRUDComponent extends Component
|
||||||
return $prefixedConditions;
|
return $prefixedConditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function taggingSupported()
|
||||||
|
{
|
||||||
|
return $this->Table->behaviors()->has('Tag');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAllTags()
|
||||||
|
{
|
||||||
|
$this->Tags = TableRegistry::getTableLocator()->get('Tags.Tags');
|
||||||
|
$allTags = $this->Tags->find()->all()->toList();
|
||||||
|
$this->Controller->set('allTags', $allTags);
|
||||||
|
}
|
||||||
|
|
||||||
public function toggle(int $id, string $fieldName = 'enabled', array $params = []): void
|
public function toggle(int $id, string $fieldName = 'enabled', array $params = []): void
|
||||||
{
|
{
|
||||||
if (empty($id)) {
|
if (empty($id)) {
|
||||||
|
|
|
@ -9,13 +9,16 @@ use Cake\Database\Expression\QueryExpression;
|
||||||
use Cake\Http\Exception\NotFoundException;
|
use Cake\Http\Exception\NotFoundException;
|
||||||
use Cake\Http\Exception\MethodNotAllowedException;
|
use Cake\Http\Exception\MethodNotAllowedException;
|
||||||
use Cake\Http\Exception\ForbiddenException;
|
use Cake\Http\Exception\ForbiddenException;
|
||||||
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
class IndividualsController extends AppController
|
class IndividualsController extends AppController
|
||||||
{
|
{
|
||||||
|
public $filters = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'filters' => ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'],
|
'filters' => $this->filters,
|
||||||
'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'],
|
'quickFilters' => ['uuid', 'email', 'first_name', 'last_name', 'position'],
|
||||||
'contextFilters' => [
|
'contextFilters' => [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
|
@ -32,6 +35,11 @@ class IndividualsController extends AppController
|
||||||
$this->set('metaGroup', 'ContactDB');
|
$this->set('metaGroup', 'ContactDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function filtering()
|
||||||
|
{
|
||||||
|
$this->CRUD->filtering();
|
||||||
|
}
|
||||||
|
|
||||||
public function add()
|
public function add()
|
||||||
{
|
{
|
||||||
$this->CRUD->add();
|
$this->CRUD->add();
|
||||||
|
@ -72,4 +80,31 @@ class IndividualsController extends AppController
|
||||||
}
|
}
|
||||||
$this->set('metaGroup', 'ContactDB');
|
$this->set('metaGroup', 'ContactDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tag($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->tag($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function untag($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->untag($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewTags($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->viewTags($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,4 +112,31 @@ class OrganisationsController extends AppController
|
||||||
}
|
}
|
||||||
$this->set('metaGroup', 'ContactDB');
|
$this->set('metaGroup', 'ContactDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tag($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->tag($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function untag($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->untag($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewTags($id)
|
||||||
|
{
|
||||||
|
$this->CRUD->viewTags($id);
|
||||||
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
if (!empty($responsePayload)) {
|
||||||
|
return $responsePayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ class IndividualsTable extends AppTable
|
||||||
{
|
{
|
||||||
parent::initialize($config);
|
parent::initialize($config);
|
||||||
$this->addBehavior('UUID');
|
$this->addBehavior('UUID');
|
||||||
|
$this->addBehavior('Tags.Tag');
|
||||||
$this->hasMany(
|
$this->hasMany(
|
||||||
'Alignments',
|
'Alignments',
|
||||||
[
|
[
|
||||||
|
|
|
@ -9,6 +9,8 @@ use Migrations\Migrations;
|
||||||
|
|
||||||
class InstanceTable extends AppTable
|
class InstanceTable extends AppTable
|
||||||
{
|
{
|
||||||
|
protected $activePlugins = ['Tags'];
|
||||||
|
|
||||||
public function initialize(array $config): void
|
public function initialize(array $config): void
|
||||||
{
|
{
|
||||||
parent::initialize($config);
|
parent::initialize($config);
|
||||||
|
@ -23,6 +25,16 @@ class InstanceTable extends AppTable
|
||||||
{
|
{
|
||||||
$migrations = new Migrations();
|
$migrations = new Migrations();
|
||||||
$status = $migrations->status();
|
$status = $migrations->status();
|
||||||
|
foreach ($this->activePlugins as $pluginName) {
|
||||||
|
$pluginStatus = $migrations->status([
|
||||||
|
'plugin' => $pluginName
|
||||||
|
]);
|
||||||
|
$pluginStatus = array_map(function ($entry) use ($pluginName) {
|
||||||
|
$entry['plugin'] = $pluginName;
|
||||||
|
return $entry;
|
||||||
|
}, $pluginStatus);
|
||||||
|
$status = array_merge($status, $pluginStatus);
|
||||||
|
}
|
||||||
$status = array_reverse($status);
|
$status = array_reverse($status);
|
||||||
|
|
||||||
$updateAvailables = array_filter($status, function ($update) {
|
$updateAvailables = array_filter($status, function ($update) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ class OrganisationsTable extends AppTable
|
||||||
public function initialize(array $config): void
|
public function initialize(array $config): void
|
||||||
{
|
{
|
||||||
parent::initialize($config);
|
parent::initialize($config);
|
||||||
|
$this->addBehavior('Tags.Tag');
|
||||||
$this->hasMany(
|
$this->hasMany(
|
||||||
'Alignments',
|
'Alignments',
|
||||||
[
|
[
|
||||||
|
|
|
@ -43,5 +43,6 @@ class AppView extends View
|
||||||
$this->loadHelper('PrettyPrint');
|
$this->loadHelper('PrettyPrint');
|
||||||
$this->loadHelper('FormFieldMassage');
|
$this->loadHelper('FormFieldMassage');
|
||||||
$this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']);
|
$this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']);
|
||||||
|
$this->loadHelper('Tags.Tag');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -834,6 +834,7 @@ class BoostrapButton extends BootstrapGeneric {
|
||||||
'class' => [],
|
'class' => [],
|
||||||
'type' => 'button',
|
'type' => 'button',
|
||||||
'nodeType' => 'button',
|
'nodeType' => 'button',
|
||||||
|
'title' => '',
|
||||||
'params' => [],
|
'params' => [],
|
||||||
'badge' => false
|
'badge' => false
|
||||||
];
|
];
|
||||||
|
@ -842,7 +843,7 @@ class BoostrapButton extends BootstrapGeneric {
|
||||||
|
|
||||||
function __construct($options) {
|
function __construct($options) {
|
||||||
$this->allowedOptionValues = [
|
$this->allowedOptionValues = [
|
||||||
'variant' => BootstrapGeneric::$variants,
|
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
|
||||||
'size' => ['', 'sm', 'lg'],
|
'size' => ['', 'sm', 'lg'],
|
||||||
'type' => ['button', 'submit', 'reset']
|
'type' => ['button', 'submit', 'reset']
|
||||||
];
|
];
|
||||||
|
@ -865,11 +866,15 @@ class BoostrapButton extends BootstrapGeneric {
|
||||||
$this->bsClasses[] = "btn-{$this->options['variant']}";
|
$this->bsClasses[] = "btn-{$this->options['variant']}";
|
||||||
}
|
}
|
||||||
if (!empty($this->options['size'])) {
|
if (!empty($this->options['size'])) {
|
||||||
$this->bsClasses[] = "btn-$this->options['size']";
|
$this->bsClasses[] = "btn-{$this->options['size']}";
|
||||||
}
|
}
|
||||||
if ($this->options['block']) {
|
if ($this->options['block']) {
|
||||||
$this->bsClasses[] = 'btn-block';
|
$this->bsClasses[] = 'btn-block';
|
||||||
}
|
}
|
||||||
|
if ($this->options['variant'] == 'text') {
|
||||||
|
$this->bsClasses[] = 'p-0';
|
||||||
|
$this->bsClasses[] = 'lh-1';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function button()
|
public function button()
|
||||||
|
@ -882,7 +887,8 @@ class BoostrapButton extends BootstrapGeneric {
|
||||||
$html = $this->openNode($this->options['nodeType'], array_merge($this->options['params'], [
|
$html = $this->openNode($this->options['nodeType'], array_merge($this->options['params'], [
|
||||||
'class' => array_merge($this->options['class'], $this->bsClasses),
|
'class' => array_merge($this->options['class'], $this->bsClasses),
|
||||||
'role' => "alert",
|
'role' => "alert",
|
||||||
'type' => $this->options['type']
|
'type' => $this->options['type'],
|
||||||
|
'title' => h($this->options['title']),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$html .= $this->genIcon();
|
$html .= $this->genIcon();
|
||||||
|
@ -914,7 +920,8 @@ class BoostrapBadge extends BootstrapGeneric {
|
||||||
'text' => '',
|
'text' => '',
|
||||||
'variant' => 'primary',
|
'variant' => 'primary',
|
||||||
'pill' => false,
|
'pill' => false,
|
||||||
'title' => ''
|
'title' => '',
|
||||||
|
'class' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
function __construct($options) {
|
function __construct($options) {
|
||||||
|
@ -938,11 +945,11 @@ class BoostrapBadge extends BootstrapGeneric {
|
||||||
private function genBadge()
|
private function genBadge()
|
||||||
{
|
{
|
||||||
$html = $this->genNode('span', [
|
$html = $this->genNode('span', [
|
||||||
'class' => [
|
'class' => array_merge($this->options['class'], [
|
||||||
'badge',
|
'badge',
|
||||||
"badge-{$this->options['variant']}",
|
"badge-{$this->options['variant']}",
|
||||||
$this->options['pill'] ? 'badge-pill' : '',
|
$this->options['pill'] ? 'badge-pill' : '',
|
||||||
],
|
]),
|
||||||
'title' => $this->options['title']
|
'title' => $this->options['title']
|
||||||
], h($this->options['text']));
|
], h($this->options['text']));
|
||||||
return $html;
|
return $html;
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
namespace App\View\Helper;
|
||||||
|
|
||||||
|
use Cake\View\Helper;
|
||||||
|
|
||||||
|
// This helper helps determining the brightness of a colour (initially only used for the tagging) in order to decide
|
||||||
|
// what text colour to use against the background (black or white)
|
||||||
|
class TextColourHelper extends Helper {
|
||||||
|
|
||||||
|
public function getTextColour($RGB) {
|
||||||
|
$r = hexdec(substr($RGB, 1, 2));
|
||||||
|
$g = hexdec(substr($RGB, 3, 2));
|
||||||
|
$b = hexdec(substr($RGB, 5, 2));
|
||||||
|
$average = ((2 * $r) + $b + (3 * $g))/6;
|
||||||
|
if ($average < 127) {
|
||||||
|
return 'white';
|
||||||
|
} else {
|
||||||
|
return 'black';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,11 @@
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'field' => 'position'
|
'field' => 'position'
|
||||||
)
|
),
|
||||||
|
array(
|
||||||
|
'field' => 'tag_list',
|
||||||
|
'type' => 'tags'
|
||||||
|
),
|
||||||
),
|
),
|
||||||
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
|
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
|
||||||
'submit' => array(
|
'submit' => array(
|
||||||
|
|
|
@ -23,7 +23,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'button' => __('Filter'),
|
'button' => __('Filter'),
|
||||||
'placeholder' => __('Enter value to search'),
|
'placeholder' => __('Enter value to search'),
|
||||||
'data' => '',
|
'data' => '',
|
||||||
'searchKey' => 'value'
|
'searchKey' => 'value',
|
||||||
|
'allowFilering' => true
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
@ -54,6 +55,11 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'element' => 'alignments',
|
'element' => 'alignments',
|
||||||
'scope' => $alignmentScope
|
'scope' => $alignmentScope
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Tags'),
|
||||||
|
'data_path' => 'tags',
|
||||||
|
'element' => 'tags',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => __('UUID'),
|
'name' => __('UUID'),
|
||||||
'sort' => 'uuid',
|
'sort' => 'uuid',
|
||||||
|
|
|
@ -28,6 +28,10 @@ echo $this->element(
|
||||||
'key' => __('Position'),
|
'key' => __('Position'),
|
||||||
'path' => 'position'
|
'path' => 'position'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Tags'),
|
||||||
|
'type' => 'tags',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'key' => __('Alignments'),
|
'key' => __('Alignments'),
|
||||||
'type' => 'alignment',
|
'type' => 'alignment',
|
||||||
|
|
|
@ -27,6 +27,10 @@ foreach ($status as $i => &$update) {
|
||||||
} else if ($update['status'] == 'down') {
|
} else if ($update['status'] == 'down') {
|
||||||
$update['_rowVariant'] = 'danger';
|
$update['_rowVariant'] = 'danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($update['plugin'])) {
|
||||||
|
$update['name'] = "{$update['plugin']}.{$update['name']}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo $this->Bootstrap->table([], [
|
echo $this->Bootstrap->table([], [
|
||||||
|
|
|
@ -71,7 +71,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
[
|
[
|
||||||
'name' => __('Type'),
|
'name' => __('Type'),
|
||||||
'data_path' => 'type',
|
'data_path' => 'type',
|
||||||
]
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Tags'),
|
||||||
|
'data_path' => 'tags',
|
||||||
|
'element' => 'tags',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'title' => __('ContactDB Organisation Index'),
|
'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.'),
|
'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.'),
|
||||||
|
|
|
@ -37,6 +37,10 @@ echo $this->element(
|
||||||
'key' => __('Contacts'),
|
'key' => __('Contacts'),
|
||||||
'path' => 'contacts'
|
'path' => 'contacts'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Tags'),
|
||||||
|
'type' => 'tags',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'key' => __('Alignments'),
|
'key' => __('Alignments'),
|
||||||
'type' => 'alignment',
|
'type' => 'alignment',
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
$tagsHtml = $this->Tag->tags($entity['tags'], [
|
||||||
|
'allTags' => [],
|
||||||
|
'picker' => true,
|
||||||
|
'editable' => true,
|
||||||
|
]);
|
||||||
|
?>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-sm-2 col-form-label"><?= __('Tags') ?></div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<?= $tagsHtml ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
$tag = $row;
|
||||||
|
echo $this->Tag->tag($tag, [
|
||||||
|
]);
|
||||||
|
|
||||||
|
?>
|
|
@ -1,17 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
$tags = $this->Hash->extract($row, $field['data_path']);
|
$tags = $this->Hash->extract($row, $field['data_path']);
|
||||||
if (!empty($tags)) {
|
echo $this->Tag->tags($tags, [
|
||||||
if (empty($tags[0])) {
|
'tags'
|
||||||
$tags = array($tags);
|
]);
|
||||||
}
|
|
||||||
echo $this->element(
|
|
||||||
'ajaxTags',
|
|
||||||
array(
|
|
||||||
'attributeId' => 0,
|
|
||||||
'tags' => $tags,
|
|
||||||
'tagAccess' => false,
|
|
||||||
'static_tags_only' => 1
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
$value = Cake\Utility\Hash::get($data, $field['path']);
|
||||||
|
echo h($value);
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
echo $this->Tag->tag($data, [
|
||||||
|
]);
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
// $tags = Cake\Utility\Hash::extract($data, $field['path']);
|
||||||
|
$tags = Cake\Utility\Hash::get($data, 'tags');
|
||||||
|
echo $this->Tag->tags($tags, [
|
||||||
|
'allTags' => $allTags,
|
||||||
|
'picker' => true,
|
||||||
|
'editable' => true,
|
||||||
|
]);
|
|
@ -15,7 +15,7 @@ $filteringForm = $this->Bootstrap->table(
|
||||||
[
|
[
|
||||||
'labelHtml' => sprintf('%s %s',
|
'labelHtml' => sprintf('%s %s',
|
||||||
__('Value'),
|
__('Value'),
|
||||||
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict match and LIKE match with the `%` character. Example: `%.com`'))
|
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict matches and LIKE matches with the `%` character. Example: `%.com`'))
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
__('Action')
|
__('Action')
|
||||||
|
@ -23,12 +23,28 @@ $filteringForm = $this->Bootstrap->table(
|
||||||
'items' => []
|
'items' => []
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($taggingEnabled) {
|
||||||
|
$helpText = $this->Bootstrap->genNode('sup', [
|
||||||
|
'class' => ['ml-1 fa fa-info'],
|
||||||
|
'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character). Example: `!exportable`, `%able`'),
|
||||||
|
]);
|
||||||
|
$filteringTags = $this->Bootstrap->genNode('h5', [], __('Tags') . $helpText);
|
||||||
|
$filteringTags .= $this->Tag->tags([], [
|
||||||
|
'allTags' => $allTags,
|
||||||
|
'picker' => true,
|
||||||
|
'editable' => false,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$filteringTags = '';
|
||||||
|
}
|
||||||
|
$modalBody = sprintf('%s%s', $filteringForm, $filteringTags);
|
||||||
|
|
||||||
|
|
||||||
echo $this->Bootstrap->modal([
|
echo $this->Bootstrap->modal([
|
||||||
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
|
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
|
||||||
'size' => 'lg',
|
'size' => 'lg',
|
||||||
'type' => 'confirm',
|
'type' => 'confirm',
|
||||||
'bodyHtml' => $filteringForm,
|
'bodyHtml' => $modalBody,
|
||||||
'confirmText' => __('Filter'),
|
'confirmText' => __('Filter'),
|
||||||
'confirmFunction' => 'filterIndex'
|
'confirmFunction' => 'filterIndex'
|
||||||
]);
|
]);
|
||||||
|
@ -54,7 +70,9 @@ echo $this->Bootstrap->modal([
|
||||||
}
|
}
|
||||||
activeFilters[fullFilter] = rowData['value']
|
activeFilters[fullFilter] = rowData['value']
|
||||||
})
|
})
|
||||||
const searchParam = (new URLSearchParams(activeFilters)).toString();
|
$select = modalObject.$modal.find('select.tag-input')
|
||||||
|
activeFilters['filteringTags'] = $select.select2('data').map(tag => tag.text)
|
||||||
|
const searchParam = jQuery.param(activeFilters);
|
||||||
const url = `/${controller}/${action}?${searchParam}`
|
const url = `/${controller}/${action}?${searchParam}`
|
||||||
|
|
||||||
const randomValue = getRandomValue()
|
const randomValue = getRandomValue()
|
||||||
|
@ -69,7 +87,9 @@ echo $this->Bootstrap->modal([
|
||||||
$filteringTable.find('tbody').empty()
|
$filteringTable.find('tbody').empty()
|
||||||
addControlRow($filteringTable)
|
addControlRow($filteringTable)
|
||||||
const randomValue = getRandomValue()
|
const randomValue = getRandomValue()
|
||||||
const activeFilters = $(`#toggleFilterButton-${randomValue}`).data('activeFilters')
|
const activeFilters = Object.assign({}, $(`#toggleFilterButton-${randomValue}`).data('activeFilters'))
|
||||||
|
const tags = activeFilters['filteringTags'] !== undefined ? Object.assign({}, activeFilters)['filteringTags'] : []
|
||||||
|
delete activeFilters['filteringTags']
|
||||||
for (let [field, value] of Object.entries(activeFilters)) {
|
for (let [field, value] of Object.entries(activeFilters)) {
|
||||||
const fieldParts = field.split(' ')
|
const fieldParts = field.split(' ')
|
||||||
let operator = '='
|
let operator = '='
|
||||||
|
@ -81,6 +101,20 @@ echo $this->Bootstrap->modal([
|
||||||
}
|
}
|
||||||
addFilteringRow($filteringTable, field, value, operator)
|
addFilteringRow($filteringTable, field, value, operator)
|
||||||
}
|
}
|
||||||
|
$select = $filteringTable.closest('.modal-body').find('select.tag-input')
|
||||||
|
let passedTags = []
|
||||||
|
tags.forEach(tagname => {
|
||||||
|
const existingOption = $select.find('option').filter(function() {
|
||||||
|
return $(this).val() === tagname
|
||||||
|
})
|
||||||
|
if (existingOption.length == 0) {
|
||||||
|
passedTags.push(new Option(tagname, tagname, true, true))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$select
|
||||||
|
.append(passedTags)
|
||||||
|
.val(tags)
|
||||||
|
.trigger('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
function addControlRow($filteringTable) {
|
function addControlRow($filteringTable) {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
echo $this->Tag->tags($entity->tags, [
|
||||||
|
'allTags' => $allTags,
|
||||||
|
'picker' => true,
|
||||||
|
'editable' => true,
|
||||||
|
]);
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
$form = $this->element('genericElements/Form/genericForm', [
|
||||||
|
'entity' => null,
|
||||||
|
'ajax' => false,
|
||||||
|
'raw' => true,
|
||||||
|
'data' => [
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'type' => 'text',
|
||||||
|
'field' => 'ids',
|
||||||
|
'default' => !empty($id) ? json_encode([$id]) : ''
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'text',
|
||||||
|
'field' => 'tag_list',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'submit' => [
|
||||||
|
'action' => $this->request->getParam('action')
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
|
||||||
|
echo $formHTML;
|
|
@ -60,6 +60,10 @@ $cakeDescription = 'Cerebrate';
|
||||||
<?= $this->fetch('css') ?>
|
<?= $this->fetch('css') ?>
|
||||||
<?= $this->fetch('script') ?>
|
<?= $this->fetch('script') ?>
|
||||||
<?= $this->Html->css('bootstrap-additional.css') ?>
|
<?= $this->Html->css('bootstrap-additional.css') ?>
|
||||||
|
|
||||||
|
<?= $this->Html->script('Tags.tagging') ?>
|
||||||
|
<?= $this->Html->css('Tags.tagging') ?>
|
||||||
|
|
||||||
<?= $this->Html->meta('favicon.ico', '/img/favicon.ico', ['type' => 'icon']); ?>
|
<?= $this->Html->meta('favicon.ico', '/img/favicon.ico', ['type' => 'icon']); ?>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -86,6 +90,5 @@ $cakeDescription = 'Cerebrate';
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
const darkMode = (<?= empty($darkMode) ? 'false' : 'true' ?>)
|
const darkMode = (<?= empty($darkMode) ? 'false' : 'true' ?>)
|
||||||
$.fn.select2.defaults.set('theme', 'bootstrap4');
|
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -115,6 +115,13 @@
|
||||||
.text-black {color:black;}
|
.text-black {color:black;}
|
||||||
.text-white {color:white;}
|
.text-white {color:white;}
|
||||||
|
|
||||||
|
.lh-1 {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.lh-2 {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.link-unstyled, .link-unstyled:link, .link-unstyled:hover {
|
.link-unstyled, .link-unstyled:link, .link-unstyled:hover {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
|
@ -145,3 +152,15 @@ input[type="checkbox"]:disabled.change-cursor {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select2-selection__choice { /* Our app do not have the same font size */
|
||||||
|
padding-left: 1.5em !important;
|
||||||
|
}
|
||||||
|
.picker-container .select2-selection {
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
border-bottom-right-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.picker-container .picker-action .btn:first-child {
|
||||||
|
border-top-left-radius: 0 !important;
|
||||||
|
border-bottom-left-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
|
@ -217,8 +217,9 @@ class UIFactory {
|
||||||
return AJAXApi.quickFetchURL(url, {
|
return AJAXApi.quickFetchURL(url, {
|
||||||
statusNode: $statusNode[0],
|
statusNode: $statusNode[0],
|
||||||
}).then((theHTML) => {
|
}).then((theHTML) => {
|
||||||
$container.replaceWith(theHTML)
|
var $tmp = $(theHTML);
|
||||||
return $container
|
$container.replaceWith($tmp)
|
||||||
|
return $tmp;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
otherStatusNodes.forEach(overlay => {
|
otherStatusNodes.forEach(overlay => {
|
||||||
overlay.hide()
|
overlay.hide()
|
||||||
|
@ -267,9 +268,9 @@ class Toaster {
|
||||||
* @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the toast
|
* @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the toast
|
||||||
* @property {boolean} autohide - If the toast show be hidden after some time defined by the delay
|
* @property {boolean} autohide - If the toast show be hidden after some time defined by the delay
|
||||||
* @property {number} delay - The number of milliseconds the toast should stay visible before being hidden
|
* @property {number} delay - The number of milliseconds the toast should stay visible before being hidden
|
||||||
* @property {string} titleHtml - The raw HTML title's content of the toast
|
* @property {(jQuery|string)} titleHtml - The raw HTML title's content of the toast
|
||||||
* @property {string} mutedHtml - The raw HTML muted's content of the toast
|
* @property {(jQuery|string)} mutedHtml - The raw HTML muted's content of the toast
|
||||||
* @property {string} bodyHtml - The raw HTML body's content of the toast
|
* @property {(jQuery|string)} bodyHtml - The raw HTML body's content of the toast
|
||||||
* @property {boolean} closeButton - If the toast's title should include a close button
|
* @property {boolean} closeButton - If the toast's title should include a close button
|
||||||
*/
|
*/
|
||||||
static defaultOptions = {
|
static defaultOptions = {
|
||||||
|
@ -290,6 +291,7 @@ class Toaster {
|
||||||
makeToast() {
|
makeToast() {
|
||||||
if (this.isValid()) {
|
if (this.isValid()) {
|
||||||
this.$toast = Toaster.buildToast(this.options)
|
this.$toast = Toaster.buildToast(this.options)
|
||||||
|
this.$toast.data('toastObject', this)
|
||||||
$('#mainToastContainer').append(this.$toast)
|
$('#mainToastContainer').append(this.$toast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,6 +302,19 @@ class Toaster {
|
||||||
var that = this
|
var that = this
|
||||||
this.$toast.toast(this.bsToastOptions)
|
this.$toast.toast(this.bsToastOptions)
|
||||||
.toast('show')
|
.toast('show')
|
||||||
|
.on('hide.bs.toast', function (evt) {
|
||||||
|
const $toast = $(this)
|
||||||
|
const hoveredElements = $(':hover').filter(function() {
|
||||||
|
return $(this).is($toast)
|
||||||
|
});
|
||||||
|
if (hoveredElements.length > 0) {
|
||||||
|
console.log('Toast hovered. Not hidding')
|
||||||
|
evt.preventDefault()
|
||||||
|
setTimeout(() => {
|
||||||
|
$toast.toast('hide')
|
||||||
|
}, that.options.delay);
|
||||||
|
}
|
||||||
|
})
|
||||||
.on('hidden.bs.toast', function () {
|
.on('hidden.bs.toast', function () {
|
||||||
that.removeToast()
|
that.removeToast()
|
||||||
})
|
})
|
||||||
|
@ -354,7 +369,10 @@ class Toaster {
|
||||||
$toastHeader.append($toastHeaderMuted)
|
$toastHeader.append($toastHeaderMuted)
|
||||||
}
|
}
|
||||||
if (options.closeButton) {
|
if (options.closeButton) {
|
||||||
var $closeButton = $('<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close"><span aria-hidden="true">×</span></button>')
|
var $closeButton = $('<button type="button" class="ml-2 mb-1 close" aria-label="Close"><span aria-hidden="true">×</span></button>')
|
||||||
|
.click(function() {
|
||||||
|
$(this).closest('.toast').data('toastObject').removeToast()
|
||||||
|
})
|
||||||
$toastHeader.append($closeButton)
|
$toastHeader.append($closeButton)
|
||||||
}
|
}
|
||||||
$toast.append($toastHeader)
|
$toast.append($toastHeader)
|
||||||
|
@ -860,7 +878,8 @@ class OverlayFactory {
|
||||||
spinnerVariant: '',
|
spinnerVariant: '',
|
||||||
spinnerSmall: false,
|
spinnerSmall: false,
|
||||||
spinnerType: 'border',
|
spinnerType: 'border',
|
||||||
fallbackBoostrapVariant: ''
|
fallbackBoostrapVariant: '',
|
||||||
|
wrapperCSSDisplay: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
static overlayWrapper = '<div aria-busy="true" class="position-relative"/>'
|
static overlayWrapper = '<div aria-busy="true" class="position-relative"/>'
|
||||||
|
@ -875,6 +894,14 @@ class OverlayFactory {
|
||||||
/** Create the HTML of the overlay */
|
/** Create the HTML of the overlay */
|
||||||
buildOverlay() {
|
buildOverlay() {
|
||||||
this.$overlayWrapper = $(OverlayFactory.overlayWrapper)
|
this.$overlayWrapper = $(OverlayFactory.overlayWrapper)
|
||||||
|
if (this.options.wrapperCSSDisplay) {
|
||||||
|
this.$overlayWrapper.css('display', this.options.wrapperCSSDisplay)
|
||||||
|
}
|
||||||
|
if (this.$node[0]) {
|
||||||
|
const boundingRect = this.$node[0].getBoundingClientRect()
|
||||||
|
this.$overlayWrapper.css('min-height', boundingRect.height)
|
||||||
|
this.$overlayWrapper.css('min-width', boundingRect.width)
|
||||||
|
}
|
||||||
this.$overlayContainer = $(OverlayFactory.overlayContainer)
|
this.$overlayContainer = $(OverlayFactory.overlayContainer)
|
||||||
this.$overlayBg = $(OverlayFactory.overlayBg)
|
this.$overlayBg = $(OverlayFactory.overlayBg)
|
||||||
.addClass([`bg-${this.options.variant}`, (this.options.rounded ? 'rounded' : '')])
|
.addClass([`bg-${this.options.variant}`, (this.options.rounded ? 'rounded' : '')])
|
||||||
|
@ -941,6 +968,7 @@ class OverlayFactory {
|
||||||
if (this.$node.is('input[type="checkbox"]') || this.$node.css('border-radius') !== '0px') {
|
if (this.$node.is('input[type="checkbox"]') || this.$node.css('border-radius') !== '0px') {
|
||||||
this.options.rounded = true
|
this.options.rounded = true
|
||||||
}
|
}
|
||||||
|
this.options.wrapperCSSDisplay = this.$node.css('display')
|
||||||
let classes = this.$node.attr('class')
|
let classes = this.$node.attr('class')
|
||||||
if (classes !== undefined) {
|
if (classes !== undefined) {
|
||||||
classes = classes.split(' ')
|
classes = classes.split(' ')
|
||||||
|
@ -1108,4 +1136,5 @@ class HtmlHelper {
|
||||||
}
|
}
|
||||||
return $table
|
return $table
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -99,6 +99,22 @@ function syntaxHighlightJson(json, indent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTextColour(hex) {
|
||||||
|
if (hex === undefined || hex.length == 0) {
|
||||||
|
return 'black'
|
||||||
|
}
|
||||||
|
hex = hex.slice(1)
|
||||||
|
var r = parseInt(hex.substring(0,2), 16)
|
||||||
|
var g = parseInt(hex.substring(2,4), 16)
|
||||||
|
var b = parseInt(hex.substring(4,6), 16)
|
||||||
|
var avg = ((2 * r) + b + (3 * g))/6
|
||||||
|
if (avg < 128) {
|
||||||
|
return 'white'
|
||||||
|
} else {
|
||||||
|
return 'black'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var UI
|
var UI
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
if (typeof UIFactory !== "undefined") {
|
if (typeof UIFactory !== "undefined") {
|
||||||
|
|
Loading…
Reference in New Issue