Merge branch 'index' into main
commit
f08bf54f28
|
@ -0,0 +1,477 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Cake\Console\Command;
|
||||
use Cake\Console\Arguments;
|
||||
use Cake\Console\ConsoleIo;
|
||||
use Cake\Console\ConsoleOptionParser;
|
||||
use Cake\Filesystem\File;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Http\Client;
|
||||
|
||||
class FieldSquasherCommand extends Command
|
||||
{
|
||||
protected $modelClass = 'Organisations';
|
||||
private $targetModel = 'Organisations';
|
||||
|
||||
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
|
||||
{
|
||||
$parser->setDescription('Squash field value from external data source');
|
||||
$parser->addArgument('config', [
|
||||
'help' => 'JSON configuration file path for the importer.',
|
||||
'required' => true
|
||||
]);
|
||||
return $parser;
|
||||
}
|
||||
|
||||
public function execute(Arguments $args, ConsoleIo $io)
|
||||
{
|
||||
$this->io = $io;
|
||||
$configPath = $args->getArgument('config');
|
||||
$config = $this->getConfigFromFile($configPath);
|
||||
$this->processConfig($config);
|
||||
$this->modelClass = $config['target']['model'];
|
||||
$source = $config['source'];
|
||||
|
||||
$table = $this->modelClass;
|
||||
$this->loadModel($table);
|
||||
$sourceData = $this->getDataFromSource($source);
|
||||
$candidateResult = $this->findCanditates($this->{$table}, $config, $sourceData);
|
||||
$entitiesSample = array_slice($candidateResult['candidates'], 0, min(10, count($candidateResult['candidates'])));
|
||||
$entitiesSample = array_slice($candidateResult['candidates'], 0, min(100, count($candidateResult['candidates'])));
|
||||
$noCandidatesSample = array_slice($candidateResult['noCandidatesFound'], 0, min(10, count($candidateResult['noCandidatesFound'])));
|
||||
$notExactCandidates = array_slice($candidateResult['notExactCandidates'], 0, min(10, count($candidateResult['notExactCandidates'])));
|
||||
$totalNotFound = count($candidateResult['noCandidatesFound']);
|
||||
$totalClosestFound = count($candidateResult['notExactCandidates']);
|
||||
$totalFound = count($candidateResult['candidates']);
|
||||
|
||||
$this->io->out("Sample of no candidates found (total: {$totalNotFound}):");
|
||||
$ioTable = $this->transformEntitiesIntoTable($noCandidatesSample);
|
||||
$io->helper('Table')->output($ioTable);
|
||||
$filename = 'no_candidates_found_' . time() . '.json';
|
||||
$selection = $io->askChoice("Would you like to save these entries on the disk as `{$filename}`", ['Y', 'N'], 'Y');
|
||||
if ($selection == 'Y') {
|
||||
$this->saveDataOnDisk($filename, $candidateResult['noCandidatesFound']);
|
||||
}
|
||||
$this->io->out('');
|
||||
|
||||
if (!empty($notExactCandidates)) {
|
||||
$this->io->out("Sample of closest candidates found (total not strictly matching: {$totalClosestFound}):");
|
||||
$ioTable = $this->transformEntitiesIntoTable($notExactCandidates);
|
||||
$io->helper('Table')->output($ioTable);
|
||||
$filename = 'closest_candidates_found_' . time() . '.json';
|
||||
$selection = $io->askChoice("Would you like to save these entries on the disk as `{$filename}`", ['Y', 'N'], 'Y');
|
||||
if ($selection == 'Y') {
|
||||
$this->saveDataOnDisk($filename, $candidateResult['notExactCandidates']);
|
||||
}
|
||||
$this->io->out('');
|
||||
}
|
||||
|
||||
$this->io->out("Sample of exact candidates found (total striclty matching: {$totalFound}):");
|
||||
$ioTable = $this->transformEntitiesIntoTable($entitiesSample, [
|
||||
'id',
|
||||
$config['finder']['joinFields']['squashed'],
|
||||
$config['target']['squashedField'],
|
||||
"{$config['target']['squashedField']}_original_value",
|
||||
'based_on_best_match',
|
||||
'best_candidates_found',
|
||||
'match_score'
|
||||
|
||||
]);
|
||||
$io->helper('Table')->output($ioTable);
|
||||
$filename = 'replacement_done_' . time() . '.json';
|
||||
$selection = $io->askChoice("Would you like to save these entries on the disk as `{$filename}`", ['Y', 'N'], 'Y');
|
||||
if ($selection == 'Y') {
|
||||
$this->saveDataOnDisk($filename, $candidateResult['candidates']);
|
||||
}
|
||||
die(1);
|
||||
|
||||
$selection = $io->askChoice('A sample of the data you about to be saved is provided above. Would you like to proceed?', ['Y', 'N'], 'N');
|
||||
if ($selection == 'Y') {
|
||||
// $this->saveData($this->{$table}, $entities);
|
||||
}
|
||||
}
|
||||
|
||||
private function saveData($table, $entities)
|
||||
{
|
||||
$this->loadModel('MetaFields');
|
||||
$this->io->verbose('Saving data');
|
||||
$progress = $this->io->helper('Progress');
|
||||
|
||||
$entities = $table->saveMany($entities);
|
||||
if ($entities === false) {
|
||||
$this->io->error('Error while saving data');
|
||||
}
|
||||
}
|
||||
|
||||
private function findCanditates($table, $config, $source)
|
||||
{
|
||||
$this->io->verbose('Finding candidates');
|
||||
if ($config['finder']['type'] == 'exact') {
|
||||
$candidateResult = $this->findCanditatesByStrictMatching($table, $config, $source);
|
||||
} else if ($config['finder']['type'] == 'closest') {
|
||||
$candidateResult = $this->findCanditatesByClosestMatch($table, $config, $source);
|
||||
} else {
|
||||
$this->io->error('Unsupported search type');
|
||||
die(1);
|
||||
}
|
||||
return $candidateResult;
|
||||
}
|
||||
|
||||
private function findCanditatesByStrictMatching($table, $config, $source)
|
||||
{
|
||||
$squashingObjects = Hash::extract($source, $config['finder']['path']);
|
||||
if (empty($squashingObjects)) {
|
||||
$this->io->error('finder.path returned nothing');
|
||||
return [];
|
||||
}
|
||||
$values = Hash::extract($squashingObjects, "{n}.{$config['finder']['joinFields']['squashing']}");
|
||||
$query = $table->find('list', [
|
||||
'keyField' => $config['finder']['joinFields']['squashed'],
|
||||
'valueField' => function ($entry) {
|
||||
return $entry;
|
||||
}
|
||||
])->where([
|
||||
"{$config['finder']['joinFields']['squashed']} IN" => $values
|
||||
]);
|
||||
$potentialCanditates = $query->toArray();
|
||||
$candidates = [];
|
||||
$noCandidatesFound = [];
|
||||
|
||||
foreach ($squashingObjects as $squashingObject) {
|
||||
$squashingJoinField = Hash::get($squashingObject, $config['finder']['joinFields']['squashing']);
|
||||
if (empty($potentialCanditates[$squashingJoinField])) {
|
||||
$noCandidatesFound[] = $squashingObject;
|
||||
} else {
|
||||
$squashingData = Hash::get($squashingObject, $config['squashingData']['squashingField']);
|
||||
if (isset($this->{$config['squashingData']['massage']})) {
|
||||
$squashingData = $this->{$config['squashingData']['massage']}($squashingData);
|
||||
}
|
||||
$squashedTarget = $potentialCanditates[$squashingJoinField];
|
||||
$squashedTarget->{"{$config['target']['squashedField']}_original_value"} = $squashedTarget->{$config['target']['squashedField']};
|
||||
$squashedTarget->{$config['target']['squashedField']} = $squashingData;
|
||||
$candidates[] = $squashedTarget;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'candidates' => $candidates,
|
||||
'notExactCandidates' => [],
|
||||
'noCandidatesFound' => $noCandidatesFound,
|
||||
];
|
||||
}
|
||||
|
||||
private function findCanditatesByClosestMatch($table, $config, $source)
|
||||
{
|
||||
$squashingObjects = Hash::extract($source, $config['finder']['path']);
|
||||
if (empty($squashingObjects)) {
|
||||
$this->io->error('finder.path returned nothing');
|
||||
return [];
|
||||
}
|
||||
$query = $table->find();
|
||||
$allCanditates = $query->toArray();
|
||||
$squashingJoinField = $config['finder']['joinFields']['squashing'];
|
||||
$squashedJoinField = $config['finder']['joinFields']['squashed'];
|
||||
$closestMatchResults = [];
|
||||
|
||||
$squashingObjects = $this->getBestOccurenceSet($squashingObjects, $allCanditates, $squashingJoinField, $squashedJoinField);
|
||||
|
||||
// pick the best match
|
||||
foreach ($squashingObjects as $i => $squashingObject) {
|
||||
if (empty($squashingObjects[$i]['__scores'])) {
|
||||
continue;
|
||||
}
|
||||
ksort($squashingObjects[$i]['__scores'], SORT_NUMERIC);
|
||||
$squashingObjects[$i]['__scores'] = array_slice($squashingObjects[$i]['__scores'], 0, 1, true);
|
||||
$bestScore = array_key_first($squashingObjects[$i]['__scores']);
|
||||
$squashingObjects[$i]['__scores'][$bestScore] = array_values($squashingObjects[$i]['__scores'][$bestScore])[0];
|
||||
}
|
||||
|
||||
$candidates = [];
|
||||
$noCandidatesFound = [];
|
||||
$notExactCandidates = [];
|
||||
$scoreThreshold = !empty($config['finder']['levenshteinScore']) ? $config['finder']['levenshteinScore'] : 10;
|
||||
|
||||
foreach ($squashingObjects as $i => $squashingObject) {
|
||||
if (empty($squashingObjects[$i]['__scores'])) {
|
||||
$noCandidatesFound[] = $squashingObject;
|
||||
continue;
|
||||
}
|
||||
$bestScore = array_key_first($squashingObject['__scores']);
|
||||
$bestMatch = $squashingObject['__scores'][$bestScore];
|
||||
|
||||
$squashingData = Hash::get($squashingObject, $config['squashingData']['squashingField']);
|
||||
if (isset($this->{$config['squashingData']['massage']})) {
|
||||
$squashingData = $this->{$config['squashingData']['massage']}($squashingData);
|
||||
}
|
||||
|
||||
$squashedTarget = $bestMatch;
|
||||
if ($bestScore <= $scoreThreshold) {
|
||||
$squashedTarget["{$config['target']['squashedField']}_original_value"] = $squashedTarget[$config['target']['squashedField']];
|
||||
$squashedTarget['match_score'] = $bestScore;
|
||||
$squashedTarget['based_on_best_match_joinFields'] = Hash::get($squashingObject, $squashingJoinField);
|
||||
// $squashedTarget['based_on_best_match'] = json_encode($squashingObject);
|
||||
$squashedTarget[$config['target']['squashedField']] = $squashingData;
|
||||
if ($bestScore > 0) {
|
||||
$notExactCandidates[] = $squashedTarget;
|
||||
} else {
|
||||
$candidates[] = $squashedTarget;
|
||||
}
|
||||
} else {
|
||||
$squashingObjectBestMatchInfo = "[{$bestMatch[$squashingJoinField]}, {$bestScore}]";
|
||||
$squashingObject['__scores'] = $squashingObjectBestMatchInfo;
|
||||
$noCandidatesFound[] = $squashingObject;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'candidates' => $candidates,
|
||||
'notExactCandidates' => $notExactCandidates,
|
||||
'noCandidatesFound' => $noCandidatesFound
|
||||
];
|
||||
}
|
||||
|
||||
private function removeCandidatesFromSquashingSet($squashingObjects, $bestMatch, $candidateID)
|
||||
{
|
||||
foreach ($squashingObjects as $i => $squashingObject) {
|
||||
if (Hash::remove($squashingObject, '__scores') == $bestMatch) {
|
||||
continue;
|
||||
} else {
|
||||
foreach ($squashingObject['__scores'] as $score => $candidates) {
|
||||
foreach ($candidates as $j => $candidate) {
|
||||
if ($candidate['id'] == $candidateID) {
|
||||
unset($squashingObjects[$i]['__scores'][$score][$j]);
|
||||
}
|
||||
}
|
||||
if (empty($squashingObjects[$i]['__scores'][$score])) {
|
||||
unset($squashingObjects[$i]['__scores'][$score]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $squashingObjects;
|
||||
}
|
||||
|
||||
private function getBestOccurenceSet($squashingObjects, $allCanditates, $squashingJoinField, $squashedJoinField)
|
||||
{
|
||||
// Compute proximity score
|
||||
foreach ($squashingObjects as $i => $squashingObject) {
|
||||
$squashingJoinValue = Hash::get($squashingObject, $squashingJoinField);
|
||||
foreach ($allCanditates as $candidate) {
|
||||
$squashedJoinValue = Hash::get($candidate, $squashedJoinField);
|
||||
$proximityScore = $this->getProximityScore($squashingJoinValue, $squashedJoinValue);
|
||||
$closestMatchResults[$candidate['id']][$proximityScore][] = $squashingObject;
|
||||
$squashingObjects[$i]['__scores'][$proximityScore][] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// sort by score
|
||||
foreach ($squashingObjects as $i => $squashingObject) {
|
||||
ksort($squashingObjects[$i]['__scores'], SORT_NUMERIC);
|
||||
}
|
||||
foreach ($closestMatchResults as $i => $proximityScore) {
|
||||
ksort($closestMatchResults[$i], SORT_NUMERIC);
|
||||
}
|
||||
|
||||
// remove best occurence in other matching sets
|
||||
foreach ($allCanditates as $candidate) {
|
||||
$bestScore = array_key_first($closestMatchResults[$candidate['id']]);
|
||||
$bestMatch = $closestMatchResults[$candidate['id']][$bestScore][0];
|
||||
$squashingObjects = $this->removeCandidatesFromSquashingSet($squashingObjects, $bestMatch, $candidate['id']);
|
||||
}
|
||||
return $squashingObjects;
|
||||
}
|
||||
|
||||
private function getProximityScore($value1, $value2)
|
||||
{
|
||||
if ($value1 == $value2) {
|
||||
return -1;
|
||||
} else {
|
||||
return levenshtein(strtolower($value1), strtolower($value2));
|
||||
}
|
||||
}
|
||||
|
||||
private function getDataFromSource($source)
|
||||
{
|
||||
$data = $this->getDataFromFile($source);
|
||||
if ($data === false) {
|
||||
$data = $this->getDataFromURL($source);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getDataFromURL($url)
|
||||
{
|
||||
$validator = new Validator();
|
||||
$validator
|
||||
->requirePresence('url')
|
||||
->notEmptyString('url', 'Please provide a valid source')
|
||||
->url('url');
|
||||
$errors = $validator->validate(['url' => $url]);
|
||||
if (!empty($errors)) {
|
||||
$this->io->error(json_encode(Hash::extract($errors, '{s}'), JSON_PRETTY_PRINT));
|
||||
die(1);
|
||||
}
|
||||
$http = new Client();
|
||||
$this->io->verbose('Downloading file');
|
||||
$response = $http->get($url);
|
||||
return $response->getJson();
|
||||
}
|
||||
|
||||
private function getDataFromFile($path)
|
||||
{
|
||||
$file = new File($path);
|
||||
if ($file->exists()) {
|
||||
$this->io->verbose('Reading file');
|
||||
$data = $file->read();
|
||||
$file->close();
|
||||
if (!empty($data)) {
|
||||
$data = json_decode($data, true);
|
||||
if (is_null($data)) {
|
||||
$this->io->error('Error while parsing the source file');
|
||||
die(1);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function saveDataOnDisk($filename, $data)
|
||||
{
|
||||
$file = new File($filename, true);
|
||||
$file->write(json_encode($data));
|
||||
$this->io->out("File saved at: {$file->pwd()}");
|
||||
$file->close();
|
||||
}
|
||||
|
||||
private function getConfigFromFile($configPath)
|
||||
{
|
||||
$file = new File($configPath);
|
||||
if ($file->exists()) {
|
||||
$config = $file->read();
|
||||
$file->close();
|
||||
if (!empty($config)) {
|
||||
$config = json_decode($config, true);
|
||||
if (is_null($config)) {
|
||||
$this->io->error('Error while parsing the configuration file');
|
||||
die(1);
|
||||
}
|
||||
return $config;
|
||||
} else {
|
||||
$this->io->error('Configuration file cound not be read');
|
||||
}
|
||||
} else {
|
||||
$this->io->error('Configuration file not found');
|
||||
}
|
||||
}
|
||||
|
||||
private function processConfig($config)
|
||||
{
|
||||
$allowedModels = ['Organisations', 'Individuals'];
|
||||
$allowedFinderType = ['exact', 'closest'];
|
||||
if (empty($config['source']) || empty($config['finder']) || empty($config['target']) || empty($config['squashingData'])) {
|
||||
$this->io->error('Error while parsing the configuration file, some of these fields are missing: `source`, `finder`, `target`, `squashingData`');
|
||||
die(1);
|
||||
}
|
||||
if (!empty($config['target']['model'])) {
|
||||
if (!in_array($config['target']['model'], $allowedModels)) {
|
||||
$this->io->error('Error while parsing the configuration file, target.model configuration must be one of: ' . implode(', ', $allowedModels));
|
||||
die(1);
|
||||
}
|
||||
} else {
|
||||
$this->io->error('Error while parsing the configuration file, target.model configuration is missing');
|
||||
die(1);
|
||||
}
|
||||
|
||||
if (empty($config['finder']['path']) || empty($config['finder']['joinFields'])) {
|
||||
$this->io->error('Error while parsing the configuration file, some finder fields are missing');
|
||||
die(1);
|
||||
}
|
||||
if (!empty($config['finder']['type'])) {
|
||||
if (!in_array($config['finder']['type'], $allowedFinderType)) {
|
||||
$this->io->error('Error while parsing the configuration file, finder.type configuration must be one of: ' . implode(', ', $allowedFinderType));
|
||||
die(1);
|
||||
}
|
||||
} else {
|
||||
$this->io->error('Error while parsing the configuration file, finder.type configuration is missing');
|
||||
die(1);
|
||||
}
|
||||
}
|
||||
|
||||
private function transformResultSetsIntoTable($result, $header=[])
|
||||
{
|
||||
$table = [[]];
|
||||
if (!empty($result)) {
|
||||
$tableHeader = empty($header) ? array_keys($result[0]) : $header;
|
||||
$tableContent = [];
|
||||
foreach ($result as $item) {
|
||||
if (empty($header)) {
|
||||
$tableContent[] = array_map('strval', array_values($item));
|
||||
} else {
|
||||
$row = [];
|
||||
foreach ($tableHeader as $key) {
|
||||
$row[] = (string) $item[$key];
|
||||
}
|
||||
$tableContent[] = $row;
|
||||
}
|
||||
}
|
||||
$table = array_merge([$tableHeader], $tableContent);
|
||||
}
|
||||
return $table;
|
||||
}
|
||||
|
||||
private function transformEntitiesIntoTable($entities, $header=[])
|
||||
{
|
||||
$table = [[]];
|
||||
if (!empty($entities)) {
|
||||
if (empty($header)) {
|
||||
if (!is_array($entities[0])) {
|
||||
$tableHeader = array_keys(Hash::flatten($entities[0]->toArray()));
|
||||
} else {
|
||||
$tableHeader = array_keys($entities[0]);
|
||||
}
|
||||
} else {
|
||||
$tableHeader = $header;
|
||||
}
|
||||
$tableContent = [];
|
||||
foreach ($entities as $entity) {
|
||||
$row = [];
|
||||
foreach ($tableHeader as $key) {
|
||||
$subKeys = explode('.', $key);
|
||||
if (is_array($entity[$key])) {
|
||||
$row[] = json_encode($entity[$key]);
|
||||
} else {
|
||||
$row[] = (string) $entity[$key];
|
||||
}
|
||||
}
|
||||
$tableContent[] = $row;
|
||||
}
|
||||
$table = array_merge([$tableHeader], $tableContent);
|
||||
}
|
||||
return $table;
|
||||
}
|
||||
|
||||
private function invertArray($data)
|
||||
{
|
||||
$inverted = [];
|
||||
foreach ($data as $key => $values) {
|
||||
foreach ($values as $i => $value) {
|
||||
$inverted[$i][$key] = $value;
|
||||
}
|
||||
}
|
||||
return $inverted;
|
||||
}
|
||||
|
||||
private function genUUID($value)
|
||||
{
|
||||
return Text::uuid();
|
||||
}
|
||||
|
||||
private function nullToEmptyString($value)
|
||||
{
|
||||
return is_null($value) ? '' : $value;
|
||||
}
|
||||
}
|
|
@ -42,6 +42,9 @@ class ImporterCommand extends Command
|
|||
protected $modelClass = 'Organisations';
|
||||
private $fieldsNoOverride = [];
|
||||
private $format = 'json';
|
||||
private $noMetaTemplate = false;
|
||||
private $autoYes = false;
|
||||
private $updateOnly = false;
|
||||
|
||||
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
|
||||
{
|
||||
|
@ -65,6 +68,18 @@ class ImporterCommand extends Command
|
|||
'default' => 'Organisations',
|
||||
'choices' => ['Organisations', 'Individuals', 'AuthKeys']
|
||||
]);
|
||||
$parser->addOption('yes', [
|
||||
'short' => 'y',
|
||||
'help' => 'Automatically assume yes to any prompts',
|
||||
'default' => false,
|
||||
'boolean' => true
|
||||
]);
|
||||
$parser->addOption('update-only', [
|
||||
'short' => 'u',
|
||||
'help' => 'Only update existing record. No new record will be created. primary_key MUST be supplied',
|
||||
'default' => false,
|
||||
'boolean' => true
|
||||
]);
|
||||
return $parser;
|
||||
}
|
||||
|
||||
|
@ -78,27 +93,38 @@ class ImporterCommand extends Command
|
|||
if (!is_null($model_class)) {
|
||||
$this->modelClass = $model_class;
|
||||
}
|
||||
$this->autoYes = $args->getOption('yes');
|
||||
$this->updateOnly = $args->getOption('update-only');
|
||||
if ($this->updateOnly && is_null($primary_key)) {
|
||||
$io->error('A `primary_key` must be supplied when using `--update-only` mode.');
|
||||
die(1);
|
||||
}
|
||||
|
||||
$table = $this->modelClass;
|
||||
$this->loadModel($table);
|
||||
$config = $this->getConfigFromFile($configPath);
|
||||
$this->processConfig($config);
|
||||
$sourceData = $this->getDataFromSource($source);
|
||||
$sourceData = $this->getDataFromSource($source, $config);
|
||||
$data = $this->extractData($this->{$table}, $config, $sourceData);
|
||||
$entities = $this->marshalData($this->{$table}, $data, $config, $primary_key);
|
||||
|
||||
$entitiesSample = array_slice($entities, 0, min(10, count($entities)));
|
||||
$entitiesSample = array_slice($entities, 0, min(5, count($entities)));
|
||||
$ioTable = $this->transformEntitiesIntoTable($entitiesSample);
|
||||
$io->helper('Table')->output($ioTable);
|
||||
|
||||
$selection = $io->askChoice('A sample of the data you are about to save is provided above. Would you like to proceed?', ['Y', 'N'], 'N');
|
||||
if ($this->autoYes) {
|
||||
$this->saveData($this->{$table}, $entities);
|
||||
} else {
|
||||
$selection = $io->askChoice('A sample of the data you about to be saved is provided above. Would you like to proceed?', ['Y', 'N'], 'N');
|
||||
if ($selection == 'Y') {
|
||||
$this->saveData($this->{$table}, $entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function marshalData($table, $data, $config, $primary_key=null)
|
||||
{
|
||||
$this->loadModel('MetaTemplates');
|
||||
$this->loadModel('MetaFields');
|
||||
$entities = [];
|
||||
if (is_null($primary_key)) {
|
||||
|
@ -112,27 +138,48 @@ class ImporterCommand extends Command
|
|||
$entity = $query->first();
|
||||
}
|
||||
if (is_null($entity)) {
|
||||
if (!$this->updateOnly) {
|
||||
$entity = $table->newEmptyEntity();
|
||||
}
|
||||
} else {
|
||||
$this->lockAccess($entity);
|
||||
}
|
||||
if (!is_null($entity)) {
|
||||
$entity = $table->patchEntity($entity, $item);
|
||||
$entities[] = $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
$hasErrors = false;
|
||||
if (!$this->noMetaTemplate) {
|
||||
$metaTemplate = $this->MetaTemplates->find()
|
||||
->where(['uuid' => $config['metaTemplateUUID']])
|
||||
->first();
|
||||
if (!is_null($metaTemplate)) {
|
||||
$metaTemplateFieldsMapping = $this->MetaTemplates->MetaTemplateFields->find('list', [
|
||||
'keyField' => 'field',
|
||||
'valueField' => 'id'
|
||||
])->where(['meta_template_id' => $metaTemplate->id])->toArray();
|
||||
} else {
|
||||
$this->io->error("Unkown template for UUID $metaTemplateUUID");
|
||||
die(1);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entities as $i => $entity) {
|
||||
if ($entity->hasErrors()) {
|
||||
$hasErrors = true;
|
||||
$this->io->error(json_encode(['entity' => $entity, 'errors' => $entity->getErrors()], JSON_PRETTY_PRINT));
|
||||
} else {
|
||||
if (!$this->noMetaTemplate && !is_null($metaTemplate)) {
|
||||
$metaFields = [];
|
||||
foreach ($entity['metaFields'] as $fieldName => $fieldValue) {
|
||||
$metaEntity = null;
|
||||
if (!$entity->isNew()) {
|
||||
$query = $this->MetaFields->find('all')->where([
|
||||
'parent_id' => $entity->id,
|
||||
'field' => $fieldName
|
||||
'field' => $fieldName,
|
||||
'meta_template_id' => $metaTemplate->id
|
||||
]);
|
||||
$metaEntity = $query->first();
|
||||
}
|
||||
|
@ -140,7 +187,14 @@ class ImporterCommand extends Command
|
|||
$metaEntity = $this->MetaFields->newEmptyEntity();
|
||||
$metaEntity->field = $fieldName;
|
||||
$metaEntity->scope = $table->metaFields;
|
||||
$metaEntity->parent_id = $entity->id;
|
||||
$metaEntity->meta_template_id = $metaTemplate->id;
|
||||
if (isset($metaTemplateFieldsMapping[$fieldName])) { // a meta field template must exists
|
||||
$metaEntity->meta_template_field_id = $metaTemplateFieldsMapping[$fieldName];
|
||||
} else {
|
||||
$hasErrors = true;
|
||||
$this->io->error("Field $fieldName is unkown for template {$metaTemplate->name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($this->canBeOverriden($metaEntity)) {
|
||||
$metaEntity->value = $fieldValue;
|
||||
|
@ -150,6 +204,7 @@ class ImporterCommand extends Command
|
|||
$entities[$i]->metaFields = $metaFields;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$hasErrors) {
|
||||
$this->io->verbose('No validation errors');
|
||||
} else {
|
||||
|
@ -176,7 +231,9 @@ class ImporterCommand extends Command
|
|||
'length' => 20
|
||||
]);
|
||||
foreach ($entities as $i => $entity) {
|
||||
if (!$this->noMetaTemplate) {
|
||||
$this->saveMetaFields($entity);
|
||||
}
|
||||
$progress->increment(1);
|
||||
$progress->draw();
|
||||
}
|
||||
|
@ -188,6 +245,7 @@ class ImporterCommand extends Command
|
|||
foreach ($entity->metaFields as $i => $metaEntity) {
|
||||
$metaEntity->parent_id = $entity->id;
|
||||
if ($metaEntity->hasErrors() || is_null($metaEntity->value)) {
|
||||
$this->io->error(json_encode(['entity' => $metaEntity, 'errors' => $metaEntity->getErrors()], JSON_PRETTY_PRINT));
|
||||
unset($entity->metaFields[$i]);
|
||||
}
|
||||
}
|
||||
|
@ -270,16 +328,16 @@ class ImporterCommand extends Command
|
|||
return !in_array($metaEntity->field, $this->fieldsNoOverride);
|
||||
}
|
||||
|
||||
private function getDataFromSource($source)
|
||||
private function getDataFromSource($source, $config)
|
||||
{
|
||||
$data = $this->getDataFromFile($source);
|
||||
if ($data === false) {
|
||||
$data = $this->getDataFromURL($source);
|
||||
$data = $this->getDataFromURL($source, $config);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getDataFromURL($url)
|
||||
private function getDataFromURL($url, $config)
|
||||
{
|
||||
$validator = new Validator();
|
||||
$validator
|
||||
|
@ -293,7 +351,11 @@ class ImporterCommand extends Command
|
|||
}
|
||||
$http = new Client();
|
||||
$this->io->verbose('Downloading file');
|
||||
$response = $http->get($url);
|
||||
$httpConfig = [
|
||||
'headers' => !empty($config['sourceHeaders']) ? $config['sourceHeaders'] : []
|
||||
];
|
||||
$query = [];
|
||||
$response = $http->get($url, $query, $httpConfig);
|
||||
if ($this->format == 'json') {
|
||||
return $response->getJson();
|
||||
} else if ($this->format == 'csv') {
|
||||
|
@ -355,6 +417,10 @@ class ImporterCommand extends Command
|
|||
$this->io->error('Error while parsing the configuration file, mapping missing');
|
||||
die(1);
|
||||
}
|
||||
if (empty($config['metaTemplateUUID'])) {
|
||||
$this->io->warning('No `metaTemplateUUID` provided. No meta fields will be created.');
|
||||
$this->noMetaTemplate = true;
|
||||
}
|
||||
if (!empty($config['format'])) {
|
||||
$this->format = $config['format'];
|
||||
}
|
||||
|
@ -454,4 +520,9 @@ class ImporterCommand extends Command
|
|||
{
|
||||
return Text::uuid();
|
||||
}
|
||||
|
||||
private function nullToEmptyString($value)
|
||||
{
|
||||
return is_null($value) ? '' : $value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"source": "/var/www/cerebrate/src/Command/misp_org.json",
|
||||
"finder": {
|
||||
"joinFields": {
|
||||
"squashed": "name",
|
||||
"squashing": "name"
|
||||
},
|
||||
"path": "{n}.Organisation",
|
||||
"type": "closest",
|
||||
"levenshteinScore": 1
|
||||
},
|
||||
"target": {
|
||||
"model": "Organisations",
|
||||
"squashedField": "uuid"
|
||||
},
|
||||
"squashingData": {
|
||||
"squashingField": "uuid",
|
||||
"massage": "validateUUID"
|
||||
}
|
||||
}
|
|
@ -30,9 +30,15 @@ class CRUDComponent extends Component
|
|||
}
|
||||
$options['filters'][] = 'quickFilter';
|
||||
}
|
||||
$params = $this->Controller->ParamHandler->harvestParams(empty($options['filters']) ? [] : $options['filters']);
|
||||
$options['filters'][] = 'filteringLabel';
|
||||
|
||||
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
|
||||
foreach ($optionFilters as $i => $filter) {
|
||||
$optionFilters[] = "{$filter} !=";
|
||||
}
|
||||
$params = $this->Controller->ParamHandler->harvestParams($optionFilters);
|
||||
$query = $this->Table->find();
|
||||
$query = $this->setFilters($params, $query);
|
||||
$query = $this->setFilters($params, $query, $options);
|
||||
$query = $this->setQuickFilters($params, $query, empty($options['quickFilters']) ? [] : $options['quickFilters']);
|
||||
if (!empty($options['contain'])) {
|
||||
$query->contain($options['contain']);
|
||||
|
@ -50,6 +56,14 @@ class CRUDComponent extends Component
|
|||
}
|
||||
}
|
||||
|
||||
public function filtering(): void
|
||||
{
|
||||
$filters = !empty($this->Controller->filters) ? $this->Controller->filters : [];
|
||||
$this->Controller->set('filters', $filters);
|
||||
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||
$this->Controller->render('/genericTemplates/filters');
|
||||
}
|
||||
|
||||
/**
|
||||
* getResponsePayload Returns the adaquate response payload based on the request context
|
||||
*
|
||||
|
@ -243,21 +257,35 @@ class CRUDComponent extends Component
|
|||
if (empty($this->Table->metaFields)) {
|
||||
return $data;
|
||||
}
|
||||
$query = $this->MetaFields->MetaTemplates->find();
|
||||
$metaFields = $this->Table->metaFields;
|
||||
$query->contain('MetaTemplateFields', function ($q) use ($id, $metaFields) {
|
||||
return $q->innerJoinWith('MetaFields')
|
||||
->where(['MetaFields.scope' => $metaFields, 'MetaFields.parent_id' => $id]);
|
||||
});
|
||||
$query->innerJoinWith('MetaTemplateFields', function ($q) {
|
||||
return $q->contain('MetaFields')->innerJoinWith('MetaFields');
|
||||
});
|
||||
$query->group(['MetaTemplates.id'])->order(['MetaTemplates.is_default' => 'DESC']);
|
||||
$metaTemplates = $query->all();
|
||||
$metaFieldScope = $this->Table->metaFields;
|
||||
$query = $this->MetaTemplates->find()->where(['MetaTemplates.scope' => $metaFieldScope]);
|
||||
$query->contain(['MetaTemplateFields.MetaFields' => function ($q) use ($id, $metaFieldScope) {
|
||||
return $q->where(['MetaFields.scope' => $metaFieldScope, 'MetaFields.parent_id' => $id]);
|
||||
}]);
|
||||
$query
|
||||
->order(['MetaTemplates.is_default' => 'DESC'])
|
||||
->order(['MetaTemplates.name' => 'ASC']);
|
||||
$metaTemplates = $query->all()->toArray();
|
||||
$metaTemplates = $this->pruneEmptyMetaTemplates($metaTemplates);
|
||||
$data['metaTemplates'] = $metaTemplates;
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function pruneEmptyMetaTemplates($metaTemplates)
|
||||
{
|
||||
foreach ($metaTemplates as $i => $metaTemplate) {
|
||||
foreach ($metaTemplate['meta_template_fields'] as $j => $metaTemplateField) {
|
||||
if (empty($metaTemplateField['meta_fields'])) {
|
||||
unset($metaTemplates[$i]['meta_template_fields'][$j]);
|
||||
}
|
||||
}
|
||||
if (empty($metaTemplates[$i]['meta_template_fields'])) {
|
||||
unset($metaTemplates[$i]);
|
||||
}
|
||||
}
|
||||
return $metaTemplates;
|
||||
}
|
||||
|
||||
public function getMetaFields($id, $data)
|
||||
{
|
||||
if (empty($this->Table->metaFields)) {
|
||||
|
@ -360,41 +388,90 @@ class CRUDComponent extends Component
|
|||
return $query;
|
||||
}
|
||||
|
||||
protected function setFilters($params, \Cake\ORM\Query $query): \Cake\ORM\Query
|
||||
protected function setFilters($params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
|
||||
{
|
||||
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';
|
||||
unset($params['filteringLabel']);
|
||||
$customFilteringFunction = '';
|
||||
$chosenFilter = '';
|
||||
if (!empty($options['contextFilters']['custom'])) {
|
||||
foreach ($options['contextFilters']['custom'] as $filter) {
|
||||
if ($filter['label'] == $filteringLabel) {
|
||||
$customFilteringFunction = $filter;
|
||||
$chosenFilter = $filter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$activeFilters = [];
|
||||
if (!empty($customFilteringFunction['filterConditionFunction'])) {
|
||||
$query = $customFilteringFunction['filterConditionFunction']($query);
|
||||
$activeFilters['filteringLabel'] = $filteringLabel;
|
||||
} else {
|
||||
if (!empty($chosenFilter)) {
|
||||
$params = $this->massageFilters($chosenFilter['filterCondition']);
|
||||
} else {
|
||||
$params = $this->massageFilters($params);
|
||||
$conditions = array();
|
||||
}
|
||||
if (!empty($params['simpleFilters'])) {
|
||||
foreach ($params['simpleFilters'] as $filter => $filterValue) {
|
||||
$activeFilters[$filter] = $filterValue;
|
||||
if ($filter === 'quickFilter') {
|
||||
continue;
|
||||
}
|
||||
if (is_array($filterValue)) {
|
||||
$query->where([($filter . ' IN') => $filterValue]);
|
||||
} else {
|
||||
if (strlen(trim($filterValue, '%')) === strlen($filterValue)) {
|
||||
$query->where([$filter => $filterValue]);
|
||||
} else {
|
||||
$query->like([$filter => $filterValue]);
|
||||
}
|
||||
$query = $this->setValueCondition($query, $filter, $filterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($params['relatedFilters'])) {
|
||||
foreach ($params['relatedFilters'] as $filter => $filterValue) {
|
||||
$activeFilters[$filter] = $filterValue;
|
||||
$filterParts = explode('.', $filter);
|
||||
$query->matching($filterParts[0], function(\Cake\ORM\Query $q) use ($filterValue, $filter) {
|
||||
if (strlen(trim($filterValue, '%')) === strlen($filterValue)) {
|
||||
return $q->where([$filter => $filterValue]);
|
||||
} else {
|
||||
return $q->like([$filter => $filterValue]);
|
||||
$query = $this->setNestedRelatedCondition($query, $filterParts, $filterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->Controller->set('activeFilters', $activeFilters);
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
|
||||
{
|
||||
$modelName = $filterParts[0];
|
||||
if (count($filterParts) == 2) {
|
||||
$fieldName = implode('.', $filterParts);
|
||||
$query = $this->setRelatedCondition($query, $modelName, $fieldName, $filterValue);
|
||||
} else {
|
||||
$filterParts = array_slice($filterParts, 1);
|
||||
$query = $query->matching($modelName, function(\Cake\ORM\Query $q) use ($filterParts, $filterValue) {
|
||||
return $this->setNestedRelatedCondition($q, $filterParts, $filterValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function setRelatedCondition($query, $modelName, $fieldName, $filterValue)
|
||||
{
|
||||
return $query->matching($modelName, function(\Cake\ORM\Query $q) use ($fieldName, $filterValue) {
|
||||
return $this->setValueCondition($q, $fieldName, $filterValue);
|
||||
});
|
||||
}
|
||||
|
||||
protected function setValueCondition($query, $fieldName, $value)
|
||||
{
|
||||
if (strlen(trim($value, '%')) === strlen($value)) {
|
||||
return $query->where([$fieldName => $value]);
|
||||
} else {
|
||||
return $query->where(function ($exp, \Cake\ORM\Query $q) use ($fieldName, $value) {
|
||||
return $exp->like($fieldName, $value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected function setFilteringContext($contextFilters, $params)
|
||||
{
|
||||
$filteringContexts = [];
|
||||
|
@ -425,6 +502,57 @@ class CRUDComponent extends Component
|
|||
$this->Controller->set('filteringContexts', $filteringContexts);
|
||||
}
|
||||
|
||||
public function setParentConditionsForMetaFields($query, array $metaConditions)
|
||||
{
|
||||
$metaTemplates = $this->MetaFields->MetaTemplates->find('list', [
|
||||
'keyField' => 'name',
|
||||
'valueField' => 'id'
|
||||
])->where(['name IN' => array_keys($metaConditions)])->all()->toArray();
|
||||
$fieldsConditions = [];
|
||||
foreach ($metaConditions as $templateName => $templateConditions) {
|
||||
$metaTemplateID = isset($metaTemplates[$templateName]) ? $metaTemplates[$templateName] : -1;
|
||||
foreach ($templateConditions as $conditions) {
|
||||
$conditions['meta_template_id'] = $metaTemplateID;
|
||||
$fieldsConditions[] = $conditions;
|
||||
}
|
||||
}
|
||||
$matchingMetaQuery = $this->getParentIDQueryForMetaANDConditions($fieldsConditions);
|
||||
return $query->where(['id IN' => $matchingMetaQuery]);
|
||||
}
|
||||
|
||||
private function getParentIDQueryForMetaANDConditions(array $metaANDConditions)
|
||||
{
|
||||
if (empty($metaANDConditions)) {
|
||||
throw new Exception('Invalid passed conditions');
|
||||
}
|
||||
foreach ($metaANDConditions as $i => $conditions) {
|
||||
$metaANDConditions[$i]['scope'] = $this->Table->metaFields;
|
||||
}
|
||||
$firstCondition = $this->prefixConditions('MetaFields', $metaANDConditions[0]);
|
||||
$conditionsToJoin = array_slice($metaANDConditions, 1);
|
||||
$query = $this->MetaFields->find()
|
||||
->select('parent_id')
|
||||
->where($firstCondition);
|
||||
foreach ($conditionsToJoin as $i => $conditions) {
|
||||
$joinedConditions = $this->prefixConditions("m{$i}", $conditions);
|
||||
$joinedConditions[] = "m{$i}.parent_id = MetaFields.parent_id";
|
||||
$query->rightJoin(
|
||||
["m{$i}" => 'meta_fields'],
|
||||
$joinedConditions
|
||||
);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function prefixConditions(string $prefix, array $conditions)
|
||||
{
|
||||
$prefixedConditions = [];
|
||||
foreach ($conditions as $condField => $condValue) {
|
||||
$prefixedConditions["${prefix}.${condField}"] = $condValue;
|
||||
}
|
||||
return $prefixedConditions;
|
||||
}
|
||||
|
||||
public function toggle(int $id, string $fieldName = 'enabled', array $params = []): void
|
||||
{
|
||||
if (empty($id)) {
|
||||
|
|
|
@ -24,6 +24,7 @@ class ParamHandlerComponent extends Component
|
|||
$parsedParams = array();
|
||||
foreach ($filterList as $k => $filter) {
|
||||
$queryString = str_replace('.', '_', $filter);
|
||||
$queryString = str_replace(' ', '_', $queryString);
|
||||
if ($this->request->getQuery($queryString) !== null) {
|
||||
$parsedParams[$filter] = $this->request->getQuery($queryString);
|
||||
continue;
|
||||
|
|
|
@ -12,11 +12,51 @@ use Cake\Http\Exception\ForbiddenException;
|
|||
|
||||
class OrganisationsController extends AppController
|
||||
{
|
||||
|
||||
public $filters = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->CRUD->index([
|
||||
'filters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id'],
|
||||
'quickFilters' => ['name', 'uuid', 'nationality', 'sector', 'type', 'url'],
|
||||
'filters' => $this->filters,
|
||||
'quickFilters' => [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'],
|
||||
'contextFilters' => [
|
||||
'custom' => [
|
||||
[
|
||||
'label' => __('ENISA Accredited'),
|
||||
'filterCondition' => [
|
||||
'MetaFields.field' => 'enisa-tistatus',
|
||||
'MetaFields.value' => 'Accredited',
|
||||
'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
|
||||
]
|
||||
],
|
||||
[
|
||||
'label' => __('ENISA not-Accredited'),
|
||||
'filterCondition' => [
|
||||
'MetaFields.field' => 'enisa-tistatus',
|
||||
'MetaFields.value !=' => 'Accredited',
|
||||
'MetaFields.MetaTemplates.name' => 'ENISA CSIRT Network'
|
||||
]
|
||||
],
|
||||
[
|
||||
'label' => __('ENISA CSIRT Network (GOV)'),
|
||||
'filterConditionFunction' => function($query) {
|
||||
return $this->CRUD->setParentConditionsForMetaFields($query, [
|
||||
'ENISA CSIRT Network' => [
|
||||
[
|
||||
'field' => 'constituency',
|
||||
'value LIKE' => '%Government%',
|
||||
],
|
||||
[
|
||||
'field' => 'csirt-network-status',
|
||||
'value' => 'Member',
|
||||
],
|
||||
]
|
||||
]);
|
||||
}
|
||||
]
|
||||
],
|
||||
],
|
||||
'contain' => ['Alignments' => 'Individuals']
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
|
@ -27,6 +67,11 @@ class OrganisationsController extends AppController
|
|||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
|
||||
public function filtering()
|
||||
{
|
||||
$this->CRUD->filtering();
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add();
|
||||
|
|
|
@ -27,6 +27,8 @@ class MetaFieldsTable extends AppTable
|
|||
->notEmptyString('meta_template_id')
|
||||
->notEmptyString('meta_template_field_id')
|
||||
->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
|
||||
|
||||
// add validation regex
|
||||
return $validator;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class OrganisationsTable extends AppTable
|
|||
[
|
||||
'dependent' => true,
|
||||
'foreignKey' => 'parent_id',
|
||||
'conditions' => ['scope' => 'organisation']
|
||||
'conditions' => ['MetaFields.scope' => 'organisation']
|
||||
]
|
||||
);
|
||||
$this->setDisplayField('name');
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
namespace App\View\Helper;
|
||||
|
||||
use Cake\View\Helper;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Utility\Security;
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
@ -70,11 +71,23 @@ class BootstrapHelper extends Helper
|
|||
$bsButton = new BoostrapButton($options);
|
||||
return $bsButton->button();
|
||||
}
|
||||
|
||||
public function badge($options)
|
||||
{
|
||||
$bsBadge = new BoostrapBadge($options);
|
||||
return $bsBadge->badge();
|
||||
}
|
||||
|
||||
public function modal($options)
|
||||
{
|
||||
$bsButton = new BoostrapModal($options);
|
||||
return $bsButton->modal();
|
||||
}
|
||||
}
|
||||
|
||||
class BootstrapGeneric
|
||||
{
|
||||
public static $variants = ['primary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent'];
|
||||
public static $variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent'];
|
||||
protected $allowedOptionValues = [];
|
||||
protected $options = [];
|
||||
|
||||
|
@ -121,6 +134,18 @@ class BootstrapGeneric
|
|||
}
|
||||
return sprintf('%s="%s"', $paramName, implode(' ', $values));
|
||||
}
|
||||
|
||||
protected static function genericCloseButton($dismissTarget)
|
||||
{
|
||||
return BootstrapGeneric::genNode('button', [
|
||||
'type' => 'button',
|
||||
'class' => 'close',
|
||||
'data-dismiss' => $dismissTarget,
|
||||
'arial-label' => __('Close')
|
||||
], BootstrapGeneric::genNode('span', [
|
||||
'arial-hidden' => 'true'
|
||||
], '×'));
|
||||
}
|
||||
}
|
||||
|
||||
class BootstrapTabs extends BootstrapGeneric
|
||||
|
@ -392,16 +417,7 @@ class BoostrapAlert extends BootstrapGeneric {
|
|||
{
|
||||
$html = '';
|
||||
if ($this->options['dismissible']) {
|
||||
$html .= $this->openNode('button', [
|
||||
'type' => 'button',
|
||||
'class' => 'close',
|
||||
'data-dismiss' => 'alert',
|
||||
'arial-label' => 'close'
|
||||
]);
|
||||
$html .= $this->genNode('span', [
|
||||
'arial-hidden' => 'true'
|
||||
], '×');
|
||||
$html .= $this->closeNode('button');
|
||||
$html .= $this->genericCloseButton('alert');
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
@ -460,7 +476,7 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
$this->options['hover'] ? 'table-hover' : '',
|
||||
$this->options['small'] ? 'table-sm' : '',
|
||||
!empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
|
||||
!empty($this->options['tableClass']) ? $this->options['tableClass'] : ''
|
||||
!empty($this->options['tableClass']) ? (is_array($this->options['tableClass']) ? implode(' ', $this->options['tableClass']) : $this->options['tableClass']) : ''
|
||||
],
|
||||
]);
|
||||
|
||||
|
@ -482,11 +498,17 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
$head .= $this->openNode('tr');
|
||||
foreach ($this->fields as $i => $field) {
|
||||
if (is_array($field)) {
|
||||
if (!empty($field['labelHtml'])) {
|
||||
$label = $field['labelHtml'];
|
||||
} else {
|
||||
$label = !empty($field['label']) ? $field['label'] : Inflector::humanize($field['key']);
|
||||
$label = h($label);
|
||||
}
|
||||
} else {
|
||||
$label = Inflector::humanize($field);
|
||||
$label = h($label);
|
||||
}
|
||||
$head .= $this->genNode('th', [], h($label));
|
||||
$head .= $this->genNode('th', [], $label);
|
||||
}
|
||||
$head .= $this->closeNode('tr');
|
||||
$head .= $this->closeNode('thead');
|
||||
|
@ -497,7 +519,7 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
{
|
||||
$body = $this->openNode('tbody', [
|
||||
'class' => [
|
||||
!empty($this->options['bodyClass']) ? $this->options['bodyClass'] : ''
|
||||
!empty($this->options['bodyClass']) ? (is_array($this->options['bodyClass']) ? implode(' ', $this->options['bodyClass']) : $this->options['bodyClass']) : ''
|
||||
],
|
||||
]);
|
||||
foreach ($this->items as $i => $row) {
|
||||
|
@ -566,7 +588,8 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
'class' => [],
|
||||
'type' => 'button',
|
||||
'nodeType' => 'button',
|
||||
'params' => []
|
||||
'params' => [],
|
||||
'badge' => false
|
||||
];
|
||||
|
||||
private $bsClasses = [];
|
||||
|
@ -577,6 +600,9 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
'size' => ['', 'sm', 'lg'],
|
||||
'type' => ['button', 'submit', 'reset']
|
||||
];
|
||||
if (empty($options['class'])) {
|
||||
$options['class'] = '';
|
||||
}
|
||||
$options['class'] = !is_array($options['class']) ? [$options['class']] : $options['class'];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
@ -615,6 +641,10 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
|
||||
$html .= $this->genIcon();
|
||||
$html .= $this->genContent();
|
||||
if (!empty($this->options['badge'])) {
|
||||
$bsBadge = new BoostrapBadge($this->options['badge']);
|
||||
$html .= $bsBadge->badge();
|
||||
}
|
||||
$html .= $this->closeNode($this->options['nodeType']);
|
||||
return $html;
|
||||
}
|
||||
|
@ -631,3 +661,197 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
return !is_null($this->options['html']) ? $this->options['html'] : $this->options['text'];
|
||||
}
|
||||
}
|
||||
|
||||
class BoostrapBadge extends BootstrapGeneric {
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'variant' => 'primary',
|
||||
'pill' => false,
|
||||
'title' => ''
|
||||
];
|
||||
|
||||
function __construct($options) {
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function badge()
|
||||
{
|
||||
return $this->genBadge();
|
||||
}
|
||||
|
||||
private function genBadge()
|
||||
{
|
||||
$html = $this->genNode('span', [
|
||||
'class' => [
|
||||
'badge',
|
||||
"badge-{$this->options['variant']}",
|
||||
$this->options['pill'] ? 'badge-pill' : '',
|
||||
],
|
||||
'title' => $this->options['title']
|
||||
], h($this->options['text']));
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
class BoostrapModal extends BootstrapGeneric {
|
||||
private $defaultOptions = [
|
||||
'size' => '',
|
||||
'centered' => true,
|
||||
'scrollable' => true,
|
||||
'backdropStatic' => false,
|
||||
'title' => '',
|
||||
'titleHtml' => false,
|
||||
'body' => '',
|
||||
'bodyHtml' => false,
|
||||
'footerHtml' => false,
|
||||
'confirmText' => 'Confirm',
|
||||
'cancelText' => 'Cancel',
|
||||
'modalClass' => [''],
|
||||
'headerClass' => [''],
|
||||
'bodyClass' => [''],
|
||||
'footerClass' => [''],
|
||||
'type' => 'ok-only',
|
||||
'variant' => '',
|
||||
'confirmFunction' => '',
|
||||
'cancelFunction' => ''
|
||||
];
|
||||
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options) {
|
||||
$this->allowedOptionValues = [
|
||||
'size' => ['sm', 'lg', 'xl', ''],
|
||||
'type' => ['ok-only','confirm','confirm-success','confirm-warning','confirm-danger'],
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
];
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
public function modal()
|
||||
{
|
||||
return $this->genModal();
|
||||
}
|
||||
|
||||
private function genModal()
|
||||
{
|
||||
$dialog = $this->openNode('div', [
|
||||
'class' => array_merge(
|
||||
['modal-dialog', (!empty($this->options['size'])) ? "modal-{$this->options['size']}" : ''],
|
||||
$this->options['modalClass']
|
||||
),
|
||||
]);
|
||||
$content = $this->openNode('div', [
|
||||
'class' => ['modal-content'],
|
||||
]);
|
||||
$header = $this->genHeader();
|
||||
$body = $this->genBody();
|
||||
$footer = $this->genFooter();
|
||||
$closedDiv = $this->closeNode('div');
|
||||
|
||||
$html = "{$dialog}{$content}{$header}{$body}{$footer}{$closedDiv}{$closedDiv}";
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genHeader()
|
||||
{
|
||||
$header = $this->openNode('div', ['class' => array_merge(['modal-header'], $this->options['headerClass'])]);
|
||||
if (!empty($this->options['titleHtml'])) {
|
||||
$header .= $this->options['titleHtml'];
|
||||
} else {
|
||||
$header .= $this->genNode('h5', ['class' => ['modal-title']], h($this->options['title']));
|
||||
}
|
||||
if (empty($this->options['backdropStatic'])) {
|
||||
$header .= $this->genericCloseButton('modal');
|
||||
}
|
||||
$header .= $this->closeNode('div');
|
||||
return $header;
|
||||
}
|
||||
|
||||
private function genBody()
|
||||
{
|
||||
$body = $this->openNode('div', ['class' => array_merge(['modal-body'], $this->options['bodyClass'])]);
|
||||
if (!empty($this->options['bodyHtml'])) {
|
||||
$body .= $this->options['bodyHtml'];
|
||||
} else {
|
||||
$body .= h($this->options['body']);
|
||||
}
|
||||
$body .= $this->closeNode('div');
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function genFooter()
|
||||
{
|
||||
$footer = $this->openNode('div', ['class' => array_merge(['modal-footer'], $this->options['footerClass'])]);
|
||||
if (!empty($this->options['footerHtml'])) {
|
||||
$footer .= $this->options['footerHtml'];
|
||||
} else {
|
||||
$footer .= $this->getFooterBasedOnType();
|
||||
}
|
||||
$footer .= $this->closeNode('div');
|
||||
return $footer;
|
||||
}
|
||||
|
||||
private function getFooterBasedOnType() {
|
||||
if ($this->options['type'] == 'ok-only') {
|
||||
return $this->getFooterOkOnly();
|
||||
} else if (str_contains($this->options['type'], 'confirm')) {
|
||||
return $this->getFooterConfirm();
|
||||
} else {
|
||||
return $this->getFooterOkOnly();
|
||||
}
|
||||
}
|
||||
|
||||
private function getFooterOkOnly()
|
||||
{
|
||||
return (new BoostrapButton([
|
||||
'variant' => 'primary',
|
||||
'text' => __('Ok'),
|
||||
'params' => [
|
||||
'data-dismiss' => 'modal',
|
||||
'onclick' => $this->options['confirmFunction']
|
||||
]
|
||||
]))->button();
|
||||
}
|
||||
|
||||
private function getFooterConfirm()
|
||||
{
|
||||
if ($this->options['type'] == 'confirm') {
|
||||
$variant = 'primary';
|
||||
} else {
|
||||
$variant = explode('-', $this->options['type'])[1];
|
||||
}
|
||||
$buttonCancel = (new BoostrapButton([
|
||||
'variant' => 'secondary',
|
||||
'text' => h($this->options['cancelText']),
|
||||
'params' => [
|
||||
'data-dismiss' => 'modal',
|
||||
'onclick' => $this->options['cancelFunction']
|
||||
]
|
||||
]))->button();
|
||||
|
||||
$buttonConfirm = (new BoostrapButton([
|
||||
'variant' => $variant,
|
||||
'text' => h($this->options['confirmText']),
|
||||
'params' => [
|
||||
'data-dismiss' => 'modal',
|
||||
'onclick' => $this->options['confirmFunction']
|
||||
]
|
||||
]))->button();
|
||||
return $buttonCancel . $buttonConfirm;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,17 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'context_filters',
|
||||
'context_filters' => $filteringContexts
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
|
||||
function submitForm(api, url) {
|
||||
const reloadUrl = '<?= isset($field['toggle_data']['reload_url']) ? $field['toggle_data']['reload_url'] : $this->Url->build(['action' => 'index']) ?>'
|
||||
return api.fetchAndPostForm(url, {})
|
||||
return api.fetchAndPostForm(url, {}, false, true)
|
||||
.then(() => {
|
||||
<?php if (!empty($field['toggle_data']['skip_full_reload'])): ?>
|
||||
const isChecked = $('#<?= $checkboxId ?>').prop('checked')
|
||||
|
|
|
@ -5,25 +5,37 @@
|
|||
$urlParams = [
|
||||
'controller' => $this->request->getParam('controller'),
|
||||
'action' => 'index',
|
||||
'?' => $filteringContext['filterCondition']
|
||||
'?' => array_merge($filteringContext['filterCondition'], ['filteringLabel' => $filteringContext['label']])
|
||||
];
|
||||
$currentQuery = $this->request->getQuery();
|
||||
unset($currentQuery['page'], $currentQuery['limit'], $currentQuery['sort']);
|
||||
$filteringLabel = !empty($currentQuery['filteringLabel']) ? $currentQuery['filteringLabel'] : '';
|
||||
unset($currentQuery['page'], $currentQuery['limit'], $currentQuery['sort'], $currentQuery['filteringLabel']);
|
||||
if (!empty($filteringContext['filterCondition'])) { // PHP replaces `.` by `_` when fetching the request parameter
|
||||
$currentFilteringContextKey = array_key_first($filteringContext['filterCondition']);
|
||||
$currentFilteringContext = [
|
||||
str_replace('.', '_', $currentFilteringContextKey) => $filteringContext['filterCondition'][$currentFilteringContextKey]
|
||||
];
|
||||
$currentFilteringContext = [];
|
||||
foreach ($filteringContext['filterCondition'] as $currentFilteringContextKey => $value) {
|
||||
$currentFilteringContextKey = str_replace('.', '_', $currentFilteringContextKey);
|
||||
$currentFilteringContextKey = str_replace(' ', '_', $currentFilteringContextKey);
|
||||
$currentFilteringContext[$currentFilteringContextKey] = $value;
|
||||
}
|
||||
} else {
|
||||
$currentFilteringContext = $filteringContext['filterCondition'];
|
||||
}
|
||||
$contextArray[] = [
|
||||
'active' => $currentQuery == $currentFilteringContext,
|
||||
'active' => (
|
||||
(
|
||||
$currentQuery == $currentFilteringContext && // query conditions match
|
||||
!isset($filteringContext['filterConditionFunction']) && // not a custom filtering
|
||||
empty($filteringLabel) // do not check `All` by default
|
||||
) ||
|
||||
$filteringContext['label'] == $filteringLabel // labels should not be duplicated
|
||||
),
|
||||
'isFilter' => true,
|
||||
'onClick' => 'changeIndexContext',
|
||||
'onClickParams' => [
|
||||
'this',
|
||||
$this->Url->build($urlParams),
|
||||
$this->Url->build($urlParams, [
|
||||
'escape' => false, // URL builder escape `&` when multiple ? arguments
|
||||
]),
|
||||
"#table-container-${tableRandomValue}",
|
||||
"#table-container-${tableRandomValue} table.table",
|
||||
],
|
||||
|
|
|
@ -12,12 +12,32 @@
|
|||
* - id: element ID for the input field - defaults to quickFilterField
|
||||
*/
|
||||
if (!isset($data['requirement']) || $data['requirement']) {
|
||||
$filteringButton = '';
|
||||
if (!empty($data['allowFilering'])) {
|
||||
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
|
||||
$buttonConfig = [
|
||||
'icon' => 'filter',
|
||||
'params' => [
|
||||
'title' => __('Filter index'),
|
||||
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
||||
]
|
||||
];
|
||||
if (count($activeFilters) > 0) {
|
||||
$buttonConfig['badge'] = [
|
||||
'variant' => 'light',
|
||||
'text' => count($activeFilters),
|
||||
'title' => __n('There is {0} active filter', 'There are {0} active filters', count($activeFilters), count($activeFilters))
|
||||
];
|
||||
}
|
||||
$filteringButton = $this->Bootstrap->button($buttonConfig);
|
||||
}
|
||||
$button = empty($data['button']) && empty($data['fa-icon']) ? '' : sprintf(
|
||||
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s">%s%s</button></div>',
|
||||
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s">%s%s</button>%s</div>',
|
||||
empty($data['data']) ? '' : h($data['data']),
|
||||
h($tableRandomValue),
|
||||
empty($data['fa-icon']) ? '' : sprintf('<i class="fa fa-%s"></i>', h($data['fa-icon'])),
|
||||
empty($data['button']) ? '' : h($data['button'])
|
||||
empty($data['button']) ? '' : h($data['button']),
|
||||
$filteringButton
|
||||
);
|
||||
if (!empty($data['cancel'])) {
|
||||
$button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel']));
|
||||
|
@ -45,6 +65,7 @@
|
|||
var action = '<?= $this->request->getParam('action') ?>';
|
||||
var additionalUrlParams = '';
|
||||
var quickFilter = <?= json_encode(!empty($quickFilter) ? $quickFilter : []) ?>;
|
||||
var activeFilters = <?= json_encode(!empty($activeFilters) ? $activeFilters : []) ?>;
|
||||
<?php
|
||||
if (!empty($data['additionalUrlParams'])) {
|
||||
echo sprintf(
|
||||
|
@ -75,6 +96,14 @@
|
|||
$(`#quickFilterField-${randomValue}`).popover('hide')
|
||||
});
|
||||
|
||||
$(`#toggleFilterButton-${randomValue}`)
|
||||
.data('activeFilters', activeFilters)
|
||||
.click(function() {
|
||||
const url = `/${controller}/filtering`
|
||||
const reloadUrl = `/${controller}/index${additionalUrlParams}`
|
||||
openFilteringModal(this, url, reloadUrl, $(`#table-container-${randomValue}`));
|
||||
})
|
||||
|
||||
function doFilter($button) {
|
||||
$(`#quickFilterField-${randomValue}`).popover('hide')
|
||||
const encodedFilters = encodeURIComponent($(`#quickFilterField-${randomValue}`).val())
|
||||
|
@ -114,5 +143,12 @@
|
|||
return $table[0].outerHTML
|
||||
}
|
||||
|
||||
function openFilteringModal(clicked, url, reloadUrl, tableId) {
|
||||
const loadingOverlay = new OverlayFactory(clicked);
|
||||
loadingOverlay.show()
|
||||
UI.openModalFromURL(url, reloadUrl, tableId).finally(() => {
|
||||
loadingOverlay.hide()
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
use Cake\Utility\Inflector;
|
||||
|
||||
$filteringForm = $this->Bootstrap->table(
|
||||
[
|
||||
'small' => true,
|
||||
'striped' => false,
|
||||
'hover' => false,
|
||||
'tableClass' => ['indexFilteringTable'],
|
||||
],
|
||||
[
|
||||
'fields' => [
|
||||
__('Field'),
|
||||
__('Operator'),
|
||||
[
|
||||
'labelHtml' => sprintf('%s %s',
|
||||
__('Value'),
|
||||
sprintf('<span class="fa fa-info ml-1" title="%s"><span>', __('Supports strict match and LIKE match with the `%` character. Example: `%.com`'))
|
||||
)
|
||||
],
|
||||
__('Action')
|
||||
],
|
||||
'items' => []
|
||||
]);
|
||||
|
||||
|
||||
echo $this->Bootstrap->modal([
|
||||
'title' => __('Filtering options for {0}', Inflector::singularize($this->request->getParam('controller'))),
|
||||
'size' => 'lg',
|
||||
'type' => 'confirm',
|
||||
'bodyHtml' => $filteringForm,
|
||||
'confirmText' => __('Filter'),
|
||||
'confirmFunction' => 'filterIndex(this)'
|
||||
]);
|
||||
?>
|
||||
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
const $filteringTable = $('table.indexFilteringTable')
|
||||
initFilteringTable($filteringTable)
|
||||
})
|
||||
|
||||
function filterIndex(clicked) {
|
||||
const controller = '<?= $this->request->getParam('controller') ?>';
|
||||
const action = 'index';
|
||||
const $clicked = $(clicked)
|
||||
const $tbody = $clicked.closest('div.modal-content').find('table.indexFilteringTable tbody')
|
||||
const $rows = $tbody.find('tr:not(#controlRow)')
|
||||
const activeFilters = {}
|
||||
$rows.each(function() {
|
||||
const rowData = getDataFromRow($(this))
|
||||
let fullFilter = rowData['name']
|
||||
if (rowData['operator'] == '!=') {
|
||||
fullFilter += ' !='
|
||||
}
|
||||
activeFilters[fullFilter] = rowData['value']
|
||||
})
|
||||
const searchParam = (new URLSearchParams(activeFilters)).toString();
|
||||
const url = `/${controller}/${action}?${searchParam}`
|
||||
|
||||
const randomValue = getRandomValue()
|
||||
UI.reload(url, $(`#table-container-${randomValue}`), $(`#table-container-${randomValue} table.table`), [{
|
||||
node: $(`#toggleFilterButton-${randomValue}`),
|
||||
config: {}
|
||||
}])
|
||||
}
|
||||
|
||||
function initFilteringTable($filteringTable) {
|
||||
const $controlRow = $filteringTable.find('#controlRow')
|
||||
$filteringTable.find('tbody').empty()
|
||||
addControlRow($filteringTable)
|
||||
const randomValue = getRandomValue()
|
||||
const activeFilters = $(`#toggleFilterButton-${randomValue}`).data('activeFilters')
|
||||
for (let [field, value] of Object.entries(activeFilters)) {
|
||||
const fieldParts = field.split(' ')
|
||||
let operator = '='
|
||||
if (fieldParts.length == 2 && fieldParts[1] == '!=') {
|
||||
operator = '!='
|
||||
field = fieldParts[0]
|
||||
} else if (fieldParts.length > 2) {
|
||||
console.error('Field contains multiple spaces. ' + field)
|
||||
}
|
||||
addFilteringRow($filteringTable, field, value, operator)
|
||||
}
|
||||
}
|
||||
|
||||
function addControlRow($filteringTable) {
|
||||
const availableFilters = <?= json_encode($filters) ?>;
|
||||
const $selectField = $('<select/>').addClass('fieldSelect custom-select custom-select-sm')
|
||||
availableFilters.forEach(filter => {
|
||||
$selectField.append($('<option/>').text(filter))
|
||||
});
|
||||
const $selectOperator = $('<select/>').addClass('fieldOperator custom-select custom-select-sm')
|
||||
.append([
|
||||
$('<option/>').text('=').val('='),
|
||||
$('<option/>').text('!=').val('!='),
|
||||
])
|
||||
const $row = $('<tr/>').attr('id', 'controlRow')
|
||||
.append(
|
||||
$('<td/>').append($selectField),
|
||||
$('<td/>').append($selectOperator),
|
||||
$('<td/>').append(
|
||||
$('<input>').attr('type', 'text').addClass('fieldValue form-control form-control-sm')
|
||||
),
|
||||
$('<td/>').append(
|
||||
$('<button/>').attr('type', 'button').addClass('btn btn-sm btn-primary')
|
||||
.append($('<span/>').addClass('fa fa-plus'))
|
||||
.click(addFiltering)
|
||||
)
|
||||
)
|
||||
$filteringTable.append($row)
|
||||
}
|
||||
|
||||
function addFilteringRow($filteringTable, field, value, operator) {
|
||||
const $selectOperator = $('<select/>').addClass('fieldOperator custom-select custom-select-sm')
|
||||
.append([
|
||||
$('<option/>').text('=').val('='),
|
||||
$('<option/>').text('!=').val('!='),
|
||||
]).val(operator)
|
||||
const $row = $('<tr/>')
|
||||
.append(
|
||||
$('<td/>').text(field).addClass('fieldName').data('fieldName', field),
|
||||
$('<td/>').append($selectOperator),
|
||||
$('<td/>').append(
|
||||
$('<input>').attr('type', 'text').addClass('fieldValue form-control form-control-sm').val(value)
|
||||
),
|
||||
$('<td/>').append(
|
||||
$('<button/>').attr('type', 'button').addClass('btn btn-sm btn-danger')
|
||||
.append($('<span/>').addClass('fa fa-trash'))
|
||||
.click(removeSelf)
|
||||
)
|
||||
)
|
||||
$filteringTable.append($row)
|
||||
const $controlRow = $filteringTable.find('#controlRow')
|
||||
disableOptionFromSelect($controlRow, field)
|
||||
}
|
||||
|
||||
function addFiltering() {
|
||||
const $table = $(this).closest('table.indexFilteringTable')
|
||||
const $controlRow = $table.find('#controlRow')
|
||||
const field = $controlRow.find('select.fieldSelect').val()
|
||||
const value = $controlRow.find('input.fieldValue').val()
|
||||
const operator = $controlRow.find('input.fieldOperator').val()
|
||||
addFilteringRow($table, field, value, operator)
|
||||
$controlRow.find('input.fieldValue').val('')
|
||||
$controlRow.find('select.fieldSelect').val('')
|
||||
}
|
||||
|
||||
function removeSelf() {
|
||||
const $row = $(this).closest('tr')
|
||||
const $controlRow = $row.closest('table.indexFilteringTable').find('#controlRow')
|
||||
const field = $row.data('fieldName')
|
||||
$row.remove()
|
||||
enableOptionFromSelect($controlRow, field)
|
||||
}
|
||||
|
||||
function disableOptionFromSelect($controlRow, optionName) {
|
||||
$controlRow.find('select.fieldSelect option').each(function() {
|
||||
const $option = $(this)
|
||||
if ($option.text() == optionName) {
|
||||
$option.prop('disabled', true)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enableOptionFromSelect($controlRow, optionName) {
|
||||
$controlRow.find('select.fieldSelect option').each(function() {
|
||||
const $option = $(this)
|
||||
if ($option.text() == optionName) {
|
||||
$option.prop('disabled', false)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getDataFromRow($row) {
|
||||
const rowData = {};
|
||||
rowData['name'] = $row.find('td.fieldName').data('fieldName')
|
||||
rowData['operator'] = $row.find('select.fieldOperator').val()
|
||||
rowData['value'] = $row.find('input.fieldValue').val()
|
||||
return rowData
|
||||
}
|
||||
|
||||
function getRandomValue() {
|
||||
const $container = $('div[id^="table-container-"]')
|
||||
const randomValue = $container.attr('id').split('-')[2]
|
||||
return randomValue
|
||||
}
|
||||
</script>
|
|
@ -48,6 +48,7 @@ class UIFactory {
|
|||
/**
|
||||
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the table after a successful operation and handles displayOnSuccess option
|
||||
* @param {string} url - The URL from which the modal's content should be fetched
|
||||
* @param {string} reloadUrl - The URL from which the data should be fetched after confirming
|
||||
* @param {string} tableId - The table ID which should be reloaded on success
|
||||
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
|
||||
*/
|
||||
|
@ -588,6 +589,7 @@ class ModalFactory {
|
|||
/** Attach the submission click listener for modals that have been generated by raw HTML */
|
||||
findSubmitButtonAndAddListener(clearOnclick=true) {
|
||||
const $submitButton = this.$modal.find('.modal-footer #submitButton')
|
||||
if ($submitButton[0]) {
|
||||
const formID = $submitButton.data('form-id')
|
||||
let $form
|
||||
if (formID) {
|
||||
|
@ -617,6 +619,7 @@ class ModalFactory {
|
|||
$submitButton.click(this.getConfirmationHandlerFunction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Class representing an loading overlay */
|
||||
class OverlayFactory {
|
||||
|
|
Loading…
Reference in New Issue