diff --git a/Phraseanet-production-client/src/components/ui/workzone/index.js b/Phraseanet-production-client/src/components/ui/workzone/index.js index 006cc6afcd..8fc803b765 100644 --- a/Phraseanet-production-client/src/components/ui/workzone/index.js +++ b/Phraseanet-production-client/src/components/ui/workzone/index.js @@ -540,7 +540,12 @@ const workzone = (services) => { $('.bloc', cache).droppable({ accept: function (elem) { -return false; +// return false; + let currentTab = $('#idFrameC .tabs').data('hash'); + if(currentTab !== '#baskets_wrapper' && currentTab !== '#baskets') { + return false; // can't drop on baskets if the baskets tab is not front + } + if ($(elem).hasClass('grouping') && !$(elem).hasClass('SSTT')) { return true; } @@ -565,7 +570,12 @@ return false; hoverClass: 'baskDrop', tolerance: 'pointer', accept: function (elem) { -//return false; +// return false; + let currentTab = $('#idFrameC .tabs').data('hash'); + if(currentTab !== '#baskets_wrapper' && currentTab !== '#baskets') { + return false; // can't drop on baskets if the baskets tab is not front + } + if ($(elem).hasClass('CHIM')) { if ($(elem).closest('.content').prev()[0] === $(this)[0]) { return false; @@ -615,7 +625,12 @@ return false; hoverClass: 'baskDrop', tolerance: 'pointer', accept: function (elem) { -return false; +// return false; + let currentTab = $('#idFrameC .tabs').data('hash'); + if(currentTab !== '#baskets_wrapper') { + return false; // can't drop on baskets if the baskets tab is not front + } + if ($(elem).hasClass('CHIM')) { if ($(elem).closest('.content').prev()[0] === $(this)[0]) { return false; @@ -864,7 +879,12 @@ return false; dest.droppable({ accept: function (elem) { -return false; +// return false; + let currentTab = $('#idFrameC .tabs').data('hash'); + if(currentTab !== '#baskets_wrapper' && currentTab !== '#baskets') { + return false; // can't drop on baskets if the baskets tab is not front + } + if ($(elem).hasClass('CHIM')) { if ($(elem).closest('.content')[0] === $(this)[0]) { return false; diff --git a/lib/Alchemy/Phrasea/Controller/Prod/EditController.php b/lib/Alchemy/Phrasea/Controller/Prod/EditController.php index 84356a7b38..0a86e785b0 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/EditController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/EditController.php @@ -26,6 +26,8 @@ use Alchemy\Phrasea\Twig\PhraseanetExtension; use Alchemy\Phrasea\Vocabulary\ControlProvider\ControlProviderInterface; use Alchemy\Phrasea\WorkerManager\Event\RecordEditInWorkerEvent; use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents; +use stdClass; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -342,6 +344,29 @@ class EditController extends Controller return $this->app->json(['success' => true]); } + + /** + * performs an editing using a similar json-body as api_v3:record:patch (except here we can work on a list of records) + * + * @param Request $request + * @return JsonResponse + */ + public function applyJSAction(Request $request): JsonResponse + { + // todo : by worker + + // for now call record_adapter. no check, no acl, ... + /** @var stdClass $arg */ + $arg = json_decode($request->getContent()); + + foreach($arg->records as $rec) { + $r = $this->getApplicationBox()->get_databox($rec->sbas_id)->get_record($rec->record_id); + $r->setMetadatasByActions($arg->actions); + } + + return $this->app->json(['success' => true]); + } + /** * @param int $preset_id * @return Preset @@ -359,7 +384,7 @@ class EditController extends Controller * route GET "../prod/records/edit/presets/{preset_id}" * * @param int $preset_id - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return JsonResponse */ public function presetsLoadAction($preset_id) { @@ -381,7 +406,7 @@ class EditController extends Controller * route GET "../prod/records/edit/presets" * * @param Request $request - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return JsonResponse */ public function presetsListAction(Request $request) { @@ -399,7 +424,7 @@ class EditController extends Controller * route DELETE "../prod/records/edit/presets/{preset_id}" * * @param int $preset_id - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return JsonResponse */ public function presetsDeleteAction($preset_id) { @@ -421,7 +446,7 @@ class EditController extends Controller * route POST "../prod/records/edit/presets" * * @param Request $request - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return JsonResponse */ public function presetsSaveAction(Request $request) { diff --git a/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php b/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php index 7db30f4e72..4e38ad4e10 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php @@ -30,15 +30,24 @@ class ThesaurusController extends Controller { $sbas_id = $request->get('sbas_id'); $tx_term_id = $request->get('tx_term_id'); + $records = RecordsRequest::fromRequest($this->app, $request, RecordsRequest::FLATTEN_YES_PRESERVE_STORIES, [ACL::CANMODIFRECORD]); + // array of sbid/rid. too bad we cannot array_map on arraycollection + $recRefs = []; + foreach($records as $r) { + $recRefs[] = [ + 'sbas_id'=>$r->getDataboxId(), + 'record_id'=>$r->getRecordId() + ]; + } + // twig parameters $twp = [ 'error' => null, 'dlg_level' => $request->get('dlg_level'), - // 'fields' => [], // fields the can receive the value - // 'fvalue' => 'Europe', - 'lst' => $records->serializedList(), + // 'lst' => $records->serializedList(), + 'records' => $recRefs, 'received_cnt' => $records->received()->count(), 'rejected_cnt' => $records->rejected()->count(), 'up_paths' => [], diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Edit.php b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Edit.php index 2051dfa93e..8c2ff64a6a 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Edit.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Edit.php @@ -12,9 +12,9 @@ namespace Alchemy\Phrasea\ControllerProvider\Prod; use Alchemy\Phrasea\Application as PhraseaApplication; -use Alchemy\Phrasea\Core\LazyLocator; use Alchemy\Phrasea\Controller\Prod\EditController; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; +use Alchemy\Phrasea\Core\LazyLocator; use Silex\Application; use Silex\ControllerProviderInterface; use Silex\ServiceProviderInterface; @@ -54,6 +54,11 @@ class Edit implements ControllerProviderInterface, ServiceProviderInterface $controllers->get('/vocabulary/{vocabulary}/', 'controller.prod.edit:searchVocabularyAction'); + /** @uses \Alchemy\Phrasea\Controller\Prod\EditController::applyJSAction */ + $controllers + ->post('/applyjs/', 'controller.prod.edit:applyJSAction') + ->bind('prod_edit_applyJSAction'); + $controllers->post('/apply/', 'controller.prod.edit:applyAction'); $controllers->get('/presets/{preset_id}', 'controller.prod.edit:presetsLoadAction'); diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php index c30b5ab68c..3ce9ee26f9 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php @@ -42,11 +42,6 @@ class Thesaurus implements ControllerProviderInterface, ServiceProviderInterface public function connect(Application $app) { $controllers = $this->createAuthenticatedCollection($app); -// $firewall = $this->getFirewall($app); - -// $controllers->before(function () use ($firewall) { -// $firewall->requireRight(\ACL::CANMODIFRECORD); -// }); /** @uses ThesaurusController::dropRecordsAction() */ $controllers->get('/droprecords', 'controller.prod.thesaurus:dropRecordsAction'); diff --git a/lib/classes/record/adapter.php b/lib/classes/record/adapter.php index f208368c9d..1242e210ef 100644 --- a/lib/classes/record/adapter.php +++ b/lib/classes/record/adapter.php @@ -32,11 +32,8 @@ use Alchemy\Phrasea\Model\Entities\User; use Alchemy\Phrasea\Model\RecordInterface; use Alchemy\Phrasea\Model\Serializer\CaptionSerializer; use Alchemy\Phrasea\Record\RecordReference; -use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator\GpsPosition; -use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; -use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; -use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents; use Alchemy\Phrasea\WorkerManager\Event\RecordsWriteMetaEvent; +use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; use Doctrine\ORM\EntityManager; @@ -1128,6 +1125,351 @@ class record_adapter implements RecordInterface, cache_cacheableInterface return $this; } + public function setMetadatasByActions(stdClass $actions) + { + // WIP crashes when trying to access an undefined stdClass property ? should return null ? + // $this->apply_body($actions); + return $this; + } + + + /* + * ============================================================================= + * the following methods allows editing by the json api-v3:record:post format + * + */ + + /** + * @param stdClass $b + * @param record_adapter $record + * @throws Exception + */ + private function apply_body(stdClass $b) + { + // do metadatas ops + if (is_array($b->metadatas)) { + $this->do_metadatas($b->metadatas); + } + // do sb ops + if (is_array($b->status)) { + $this->do_status($b->status); + } + if(!is_null($b->base_id)) { + $this->do_collection($b->base_id); + } + } + + /** + * @param $base_id + */ + private function do_collection($base_id) + { + $this->move_to_collection(collection::getByCollectionId($this->app, $this->getDatabox(), $base_id)); + } + + + ////////////////////////////////// + /// TODO : keep multi-values uniques ! + /// it should be done in record_adapter + ////////////////////////////////// + + /** + * @param $metadatas + * @throws Exception + */ + private function do_metadatas($metadatas) + { + /** @var databox_field[] $struct */ + $struct = $this->getDatabox()->get_meta_structure(); + + + $structByKey = []; + $allStructFields = []; + foreach ($struct as $f) { + $allStructFields[$f->get_id()] = $f; + $structByKey[$f->get_id()] = &$allStructFields[$f->get_id()]; + $structByKey[$f->get_name()] = &$allStructFields[$f->get_id()]; + } + + $metadatas_ops = []; + foreach ($metadatas as $_m) { + // sanity + if($_m->meta_struct_id && $_m->field_name) { // WIP crashes if meta_struct_id is undefined + throw new Exception("define meta_struct_id OR field_name, not both."); + } + // select fields that match meta_struct_id or field_name (can be arrays) + $fields_list = null; // to filter caption_fields from record, default all + $struct_fields = []; // struct fields that match meta_struct_id or field_name + $field_keys = $_m->meta_struct_id ? $_m->meta_struct_id : $_m->field_name; // can be null if none defined (=match all) + if($field_keys !== null) { + if (!is_array($field_keys)) { + $field_keys = [$field_keys]; + } + $fields_list = []; + foreach ($field_keys as $k) { + if(array_key_exists($k, $structByKey)) { + $fields_list[] = $structByKey[$k]->get_name(); + $struct_fields[$structByKey[$k]->get_id()] = $structByKey[$k]; + } + else { + throw new Exception(sprintf("unknown field (%s).", $k)); + } + } + } + else { + // no meta_struct_id, no field_name --> match all struct fields ! + $struct_fields = $allStructFields; + } + $caption_fields = $this->get_caption()->get_fields($fields_list, true); + + $meta_id = is_null($_m->meta_id) ? null : (int)($_m->meta_id); + + if(!($match_method = (string)($_m->match_method))) { + $match_method = 'ignore_case'; + } + if(!in_array($match_method, ['strict', 'ignore_case', 'regexp'])) { + throw new Exception(sprintf("bad match_method (%s).", $match_method)); + } + + $values = []; + if(is_array($_m->value)) { + foreach ($_m->value as $v) { + if(($v = trim((string)$v)) !== '') { + $values[] = $v; + } + } + } + else { + if(($v = trim((string)($_m->value))) !== '') { + $values[] = $v; + } + } + + if(!($action = (string)($_m->action))) { + $action = 'set'; + } + + switch ($_m->action) { + case 'set': + $ops = $this->metadata_set($struct_fields, $caption_fields, $meta_id, $values); + break; + case 'add': + $ops = $this->metadata_add($struct_fields, $values); + break; + case 'delete': + $ops = $this->metadata_replace($caption_fields, $meta_id, $match_method, $values, null); + break; + case 'replace': + if (!is_string($_m->replace_with) && !is_null($_m->replace_with)) { + throw new Exception("bad \"replace_with\" for action \"replace\"."); + } + $ops = $this->metadata_replace($caption_fields, $meta_id, $match_method, $values, $_m->replace_with); + break; + default: + throw new Exception(sprintf("bad action (%s).", $action)); + } + + $metadatas_ops = array_merge($metadatas_ops, $ops); + } + + $this->set_metadatas($metadatas_ops, true); + + // order to write meta in file + $this->app['dispatcher']->dispatch(WorkerEvents::RECORDS_WRITE_META, + new RecordsWriteMetaEvent([$this->getRecordId()], $this->getDataboxId())); + + } + + /** + * @param $statuses + * @return array + * @throws Exception + */ + private function do_status($statuses) + { + $datas = strrev($this->getStatus()); + + foreach ($statuses as $status) { + $n = (int)($status->bit); + $value = (int)($status->state); + if ($n > 31 || $n < 4) { + throw new Exception(sprintf("Invalid status bit number (%s).", $n)); + } + if ($value < 0 || $value > 1) { + throw new Exception(sprintf("Invalid status bit state (%s) for bit (%s).", $value, $n)); + } + + $datas = substr($datas, 0, ($n)) . $value . substr($datas, ($n + 1)); + } + + $this->setStatus(strrev($datas)); + } + + private function match($pattern, $method, $value) + { + switch ($method) { + case 'strict': + return $value === $pattern; + case 'ignore_case': + return strtolower($value) === strtolower($pattern); + case 'regexp': + return preg_match($pattern, $value) == 1; + } + return false; + } + + /** + * @param databox_field[] $struct_fields struct-fields (from struct) matching meta_struct_id or field_name + * @param caption_field[] $caption_fields caption-fields (from record) matching meta_struct_id or field_name (or all if not set) + * @param int|null $meta_id + * @param string[] $values + * + * @return array ops to execute + * @throws Exception + */ + private function metadata_set(array $struct_fields, $caption_fields, $meta_id, $values): array + { + $ops = []; + + // if one field was multi-valued and no meta_id was set, we must delete all values + foreach ($caption_fields as $cf) { + foreach ($cf->get_values() as $field_value) { + if (is_null($meta_id) || $field_value->getId() === (int)$meta_id) { + $ops[] = [ + 'meta_struct_id' => $cf->get_meta_struct_id(), + 'meta_id' => $field_value->getId(), + 'value' => '' + ]; + } + } + } + // now set values to matching struct_fields + foreach ($struct_fields as $sf) { + if($sf->is_multi()) { + // add the non-null value(s) + foreach ($values as $value) { + if ($value) { + $ops[] = [ + 'meta_struct_id' => $sf->get_id(), + 'meta_id' => $meta_id, // can be null + 'value' => $value + ]; + } + } + } + else { + // mono-valued + if(count($values) > 1) { + throw new Exception(sprintf("setting mono-valued (%s) requires only one value.", $sf->get_name())); + } + if( ($value = $values[0]) ) { + $ops[] = [ + 'meta_struct_id' => $sf->get_id(), + 'meta_id' => $meta_id, // probably null, + 'value' => $value + ]; + } + } + } + + return $ops; + } + + /** + * @param databox_field[] $struct_fields struct-fields (from struct) matching meta_struct_id or field_name + * @param string[] $values + * + * @return array ops to execute + * @throws Exception + */ + private function metadata_add($struct_fields, $values) + { + $ops = []; + + // now set values to matching struct_fields + foreach ($struct_fields as $sf) { + if(!$sf->is_multi()) { + throw new Exception(sprintf("can't \"add\" to mono-valued (%s).", $sf->get_name())); + } + foreach ($values as $value) { + $ops[] = [ + 'meta_struct_id' => $sf->get_id(), + 'meta_id' => null, + 'value' => $value + ]; + } + } + + return $ops; + } + + /** + * @param caption_field[] $caption_fields caption-fields (from record) matching meta_struct_id or field_name (or all if not set) + * @param int|null $meta_id + * @param string $match_method "strict" | "ignore_case" | "regexp" + * @param string[] $values + * @param string|null $replace_with + * + * @return array ops to execute + */ + private function metadata_replace($caption_fields, $meta_id, $match_method, $values, $replace_with) + { + $ops = []; + + $replace_with = trim((string)$replace_with); + + foreach ($caption_fields as $cf) { + // match all ? + if(is_null($meta_id) && count($values) == 0) { + foreach ($cf->get_values() as $field_value) { + $ops[] = [ + 'meta_struct_id' => $cf->get_meta_struct_id(), + 'meta_id' => $field_value->getId(), + 'value' => $replace_with + ]; + } + } + // match by meta-id ? + if (!is_null($meta_id)) { + foreach ($cf->get_values() as $field_value) { + if ($field_value->getId() === $meta_id) { + $ops[] = [ + 'meta_struct_id' => $cf->get_meta_struct_id(), + 'meta_id' => $field_value->getId(), + 'value' => $replace_with + ]; + } + } + } + // match by value(s) ? + foreach ($values as $value) { + foreach ($cf->get_values() as $field_value) { + $rw = $replace_with; + if($match_method=='regexp' && $rw != '') { + $rw = preg_replace($value, $rw, $field_value->getValue()); + } + if ($this->match($value, $match_method, $field_value->getValue())) { + $ops[] = [ + 'meta_struct_id' => $cf->get_meta_struct_id(), + 'meta_id' => $field_value->getId(), + 'value' => $rw + ]; + } + } + } + } + + return $ops; + } + + /* + * + * END editing by the json api-v3:record:post format + * ============================================================================= + */ + + + /** * @return record_adapter */ diff --git a/templates/web/prod/Thesaurus/droppedrecords.html.twig b/templates/web/prod/Thesaurus/droppedrecords.html.twig index ed4b992b7f..dd5f0a4155 100644 --- a/templates/web/prod/Thesaurus/droppedrecords.html.twig +++ b/templates/web/prod/Thesaurus/droppedrecords.html.twig @@ -1,3 +1,6 @@ + +

POC/Experimental. The OK button will NOT edit records.

+ {% if error %} {{ error }} @@ -11,7 +14,7 @@
- + @@ -43,7 +46,9 @@ - + {# do NOT mix the following 2 options as one #} + +
svaluevalue to field
@@ -88,14 +93,30 @@ $(' .fieldSelect').filter(function () { return $(this).prop('selectedIndex')>0;}).each(function () { let n = $(this).data('n'); data.push({ - 'field': $(this).val(), - 'op' : $(' .actionSelect._'+n, $container).val(), - 'value': $(' .synonym._'+n, $container).val() + 'field_name': $(this).val(), + 'action' : $(' .actionSelect._'+n, $container).val(), + 'value' : $(' .synonym._'+n, $container).val() }); }); + data = { + 'records': {{ records|json_encode|raw }}, + 'actions': { + 'metadatas': data + } + }; + console.log(data); - // $( this ).dialog( "close" ); + $.ajax({ + url: "{{ path('prod_edit_applyJSAction') }}", + type: "POST", + contentType: "application/json", + data: JSON.stringify(data), + success: function (data, textStatus) { + console.log(data); + } + }, + ); } }, { @@ -115,18 +136,13 @@ let select = $(this); let n = select.data('n'); - if( $('option:eq(' + select.prop('selectedIndex') + ')', select).data('multi') ) { - // this select is on a multi-valued field - $(' .actionSelect._'+n+' OPTION.mono', $container).hide(); - $(' .actionSelect._'+n+' OPTION.multi', $container).show(); - $(' .actionSelect._'+n).prop('selectedIndex', 1); // add - } - else { - // this select is on a mono-valued field - $(' .actionSelect._'+n+' OPTION.multi', $container).hide(); - $(' .actionSelect._'+n+' OPTION.mono', $container).show(); - $(' .actionSelect._'+n).prop('selectedIndex', 0); // set - } + // show only possible actions, depending on field is mono/multi + let actionSelect = $(' .actionSelect._'+n, $container); + let multi = !!$('option:eq(' + select.prop('selectedIndex') + ')', select).data('multi'); + $(' OPTION.mono', actionSelect).toggle(!multi); + $(' OPTION.multi', actionSelect).toggle(multi); + // set default action + actionSelect.prop('selectedIndex', multi ? 1 : 0); // 1=add : 0=set // if at least one destination field is set, show some elements let oneFieldSet = false; diff --git a/templates/web/prod/WorkZone/Basket.html.twig b/templates/web/prod/WorkZone/Basket.html.twig index 679531a24a..c179750136 100644 --- a/templates/web/prod/WorkZone/Basket.html.twig +++ b/templates/web/prod/WorkZone/Basket.html.twig @@ -145,6 +145,7 @@ {% endif %} +{% if basket.getValidation() %} +{% endif %} diff --git a/templates/web/prod/index.html.twig b/templates/web/prod/index.html.twig index 2db39ee0e2..4f95ffe1a2 100644 --- a/templates/web/prod/index.html.twig +++ b/templates/web/prod/index.html.twig @@ -183,32 +183,33 @@
{% include 'prod/tab_headers.html.twig' %} - {% import 'prod/WorkZone/Macros.html.twig' as WorkZoneMacros %} - {% set expose_activate = app['conf'].get(['phraseanet-service', 'expose-service', 'activated']) %} - {% if expose_activate == 'true' %} - {% block tab_headers_wrapper %} -
-