diff --git a/Phraseanet-production-client/src/components/thesaurus/index.js b/Phraseanet-production-client/src/components/thesaurus/index.js index 8778e4350e..7638a52e34 100644 --- a/Phraseanet-production-client/src/components/thesaurus/index.js +++ b/Phraseanet-production-client/src/components/thesaurus/index.js @@ -16,6 +16,12 @@ const thesaurusService = services => { let dragging = false; // true when an object is dragged over the th zone let dragTarget = null; // the target where the mouse is over + let dragUniqueSbid = null; // will end-up as : null (nothing dragged) ; false (many sbids) ; sbid (same sbid for all) + let dragLstRecords = '' // list or records, format as expected for RecordsRequest::fromRequest + const url = configService.get('baseUrl'); + + let searchSelection = {asArray: [], serialized: ''}; + const initialize = params => { let { $container } = params; @@ -105,6 +111,18 @@ const thesaurusService = services => { $('#THPD_T_tree') .droppable({ accept: function(elem) { + let lstbr = searchSelection.asArray; + console.log("lstbr", lstbr); + + dragUniqueSbid = null; + lstbr.forEach(sbid_rid => { + sbid_rid = sbid_rid.split('_'); + let sbid = sbid_rid[0]; + let rid = sbid_rid[1]; + dragUniqueSbid = (dragUniqueSbid===null) ? sbid : (sbid===dragUniqueSbid ? sbid : false); + }); + dragLstRecords = lstbr.join(';'); // a list as expected for RecordsRequest::fromRequest + $(this).removeClass('draggingOver'); console.log("accept", elem); // if ($(elem).hasClass('grouping') && !$(elem).hasClass('SSTT')) { @@ -115,15 +133,37 @@ const thesaurusService = services => { // the th zone can accet drags only when in front (activated tab) // 'hash' is set by the 'workzone' js code. - return $('#idFrameC .tabs').data('hash') === '#thesaurus_tab'; + // return $('#idFrameC .tabs').data('hash') === '#thesaurus_tab'; + + if($('#idFrameC .tabs').data('hash') !== '#thesaurus_tab') { + return false; // can't drop on th if the th tab is not front + } + + // by using classes on both main container AND the (unique) acceptable thesaurus zone + // we can have custom drag/drop css for both ok / reject + $('#THPD_T_tree', $container).removeClass('draggingOver'); // the container + $('#THPD_T_tree>LI', $container).removeClass('draggingOver'); // all thesaurus + + if(dragUniqueSbid === null || dragUniqueSbid === false) { + // many sbids + // return false; // don't return false, as it will prevent "over" and will not apply css (no "not-allowed" cursor) + } + + return true; }, scope: 'objects', hoverClass: 'groupDrop', tolerance: 'pointer', over: function(event, ui) { - $(this).addClass('draggingOver'); console.log("over", event, ui, event.toElement); + $('#THPD_T_tree', $container).addClass('draggingOver'); + if(dragUniqueSbid !== null && dragUniqueSbid !== false) { + $('#TX_P\\.'+dragUniqueSbid+'\\.T', $container).addClass('draggingOver'); + } + /* + $(this).addClass('draggingOver'); + if(dragTarget) { // something was already hilighted (should no happen) dragTarget.removeClass('dragOver'); @@ -139,28 +179,50 @@ const thesaurusService = services => { dragTarget.addClass('dragOver'); console.log("IN : " + dragTarget.attr('id')); } + */ }, out: function(event, ui) { - $(this).removeClass('draggingOver'); console.log("out", event, ui, event.toElement); + $('#THPD_T_tree', $container).removeClass('draggingOver'); + $('#THPD_T_tree>LI', $container).removeClass('draggingOver'); + /* + $(this).removeClass('draggingOver'); if(dragTarget) { // something was hilighted dragTarget.removeClass('dragOver'); } dragging = false; // == no more dragging something over th dragTarget = null; + + */ }, drop: (event, ui) => { - $(this).removeClass('draggingOver'); console.log("drop", event, ui); - if(dragTarget) { - // const tid = $(event.toElement).data('tx_term_id'); - console.log("DROP ON id=" + dragTarget.attr('id')); - dragTarget.removeClass('dragOver'); -// appEvents.emit('searchAdvancedForm.activateDatabase', { databases: [sbid] }); + $('#THPD_T_tree', $container).removeClass('draggingOver'); + $('#THPD_T_tree>LI', $container).removeClass('draggingOver'); + + const target = $(event.toElement); + const sbas_id = target.data('sbas_id').toString(); // set on html by ThesaurusXmlHttpController.php + const tx_term_id = target.data('tx_term_id').toString(); // set on html by ThesaurusXmlHttpController.php + + if(sbas_id === dragUniqueSbid) { + dropRecordsOnTerm(sbas_id, tx_term_id, dragLstRecords); } - dragging = false; // == no more dragging something over th - dragTarget = null; + + + + /* + $(this).removeClass('draggingOver'); + if(dragTarget) { + // const tid = $(event.toElement).data('tx_term_id'); + console.log("DROP ON id=" + dragTarget.attr('id')); + dragTarget.removeClass('dragOver'); + // appEvents.emit('searchAdvancedForm.activateDatabase', { databases: [sbid] }); + } + dragging = false; // == no more dragging something over th + dragTarget = null; + + */ } }) // track the mouse @@ -194,6 +256,57 @@ const thesaurusService = services => { searchValue = _.debounce(searchValue, 300); }; + + + function dropRecordsOnTerm(sbas_id, tx_term_id, lstRecords) { + let dlg = dialog.create( + services, + { + size: 'Custom', + customWidth: 770, + customHeight: 400, + title: localeService.t('add data'), + loading: true + }, + 0 + ); + // dlg.setContent(""); + + // let parms = { + // url: '/thesaurus/drop_records.php', + // data: { + // 'sbas_id': sbas_id, + // 'tx_term_id': tx_term_id, + // 'lst': lstRecords + // }, + // async: true, + // cache: false, + // dataType: 'json', + // // timeout: 10 * 60 * 1000, // 10 minutes ! + // success: function (result, textStatus) { + // dlg.setContent(result); + // } + // }; + + $.get( + `${url}prod/thesaurus/droprecords`, + { + 'dlg_level': 0, + 'sbas_id': sbas_id, + 'tx_term_id': tx_term_id, + 'lst': lstRecords + }, + function (data, textStatus) { + dlg.setContent(data); + } + ); + } + + + + + + function show() { // first show of thesaurus if (options.currentWizard === '???') { @@ -1462,6 +1575,13 @@ const thesaurusService = services => { }); } + appEvents.listenAll({ + 'broadcast.searchResultSelection': (selection) => { + searchSelection = selection; + } + }); + + return { initialize, show }; }; diff --git a/Phraseanet-production-client/src/skins/style/ui-components/_workzone-thesaurus.scss b/Phraseanet-production-client/src/skins/style/ui-components/_workzone-thesaurus.scss index bf70671e29..0a12244db4 100644 --- a/Phraseanet-production-client/src/skins/style/ui-components/_workzone-thesaurus.scss +++ b/Phraseanet-production-client/src/skins/style/ui-components/_workzone-thesaurus.scss @@ -189,10 +189,25 @@ dans l'onglet thesaurus : arbres, menus contextuels } #THPD_T_tree.draggingOver { + //background-color: #FF0000; /* something is currently dragged over, we change some hover */ + &:hover { + cursor: not-allowed; + } span.tx_term:hover { - background-color: $highlightBackgroundColor; - color: $highlightTextColor; + color: $mediumTextColor; // no more highlight + cursor: not-allowed; + } + LI.draggingOver { + //background-color: #00FF00; + &:hover { + cursor: default; + } + span.tx_term:hover { + background-color: $highlightBackgroundColor; + color: $highlightTextColor; + cursor: copy; + } } } diff --git a/lib/Alchemy/Phrasea/Application/RouteLoader.php b/lib/Alchemy/Phrasea/Application/RouteLoader.php index 426b4f0003..ed6f4a15af 100644 --- a/lib/Alchemy/Phrasea/Application/RouteLoader.php +++ b/lib/Alchemy/Phrasea/Application/RouteLoader.php @@ -65,6 +65,7 @@ class RouteLoader '/prod/share/' => Providers\Prod\Share::class, '/prod/story' => Providers\Prod\Story::class, '/prod/subdefs' => Providers\Prod\Subdefs::class, + '/prod/thesaurus/' => Providers\Prod\Thesaurus::class, '/prod/tools/' => Providers\Prod\Tools::class, '/prod/tooltip' => Providers\Prod\Tooltip::class, '/prod/TOU/' => Providers\Prod\TOU::class, diff --git a/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php b/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php new file mode 100644 index 0000000000..74538141c5 --- /dev/null +++ b/lib/Alchemy/Phrasea/Controller/Prod/ThesaurusController.php @@ -0,0 +1,138 @@ +get('sbas_id'); + $tx_term_id = $request->get('tx_term_id'); + $records = RecordsRequest::fromRequest($this->app, $request, false); + + // twig parameters + $twp = [ + 'error' => null, + 'dlg_level' => $request->get('dlg_level'), + // 'fields' => [], // fields the can receive the value + // 'fvalue' => 'Europe', + 'up_paths' => [], + ]; + + // find which field(s) can be updated, that is what tbranches are linked to a parent of the term + + try { + $dbox = $this->app->getApplicationBox()->get_databox($sbas_id); + + if (!($domth = $dbox->get_dom_thesaurus())) { + throw new Exception("error fetching th"); + } + + $upPaths = []; + $xpath = new DOMXPath($domth); + foreach ($dbox->get_meta_structure() as $field) { + if (!($q = $field->get_tbranch())) { + continue; + } + $fieldName = $field->get_name(); + $roots = $xpath->query($q); // linked nodes for this field + $q = '(' . $q . ')//sy[@id=\'' . $tx_term_id . '\']'; // can we find the term under the tbranch(es) ? + // normally we should find only one linked parent, since we search from a unique term + // ...but... a bad th could link a field onto 2 points of the same branch : + // root + // |---A <-- "keyword" is linked here... + // |---B <-- ...but also linked here (bad idea btw) + // |---terms + // going up, we decide to stop at the first link (B) (easier) + if (($droppedSy = $xpath->query($q))->length > 0) { + // yes (and since the query targets a unique id, there is only one result) + $droppedSy = $droppedSy->item(0); + /** @var DOMElement $droppedSy */ + $droppedlng = $droppedSy->getAttribute('lng'); // the lng of the dropped term is prefered + + // go from the sy upto a linked branch (possibly multiples if the field is linked to many branches) + + $ok = true; // == the term (level up-to top) can populate the current field + for ($te = $droppedSy->parentNode; $te->nodeType === XML_ELEMENT_NODE; $te = $te->parentNode) { + /** @var DOMElement $te */ + $teid = $te->getAttribute('id'); + for ($i = 0; $i < $roots->length; $i++) { + if ($te->isSameNode($roots->item($i))) { + $ok = false; // we met the link point, upmost terms are not "values" anymore + } + } + if ($ok) { // acceptable value for the current field + if (!array_key_exists($teid, $upPaths)) { + $upPaths[$teid] = [ + 'synonyms' => [], + 'fields' => [] + ]; + // get all the sy so the user can choose which is prefered + $preferedId = null; + foreach ($te->childNodes as $sy) { + if ($sy->nodeName != 'sy') { + continue; // skip 'te' children + } + $lng = $sy->getAttribute('lng'); + $id = $sy->getAttribute('id'); + $s = [ + 'value' => $sy->getAttribute('v'), + 'lng' => $lng, + 'selected' => false + ]; + // this sy is prefered if... + if ($sy->getAttribute('lng') === $droppedlng) { + $preferedId = $id; // ... it has the same lng as the dropped + } + if ($sy->isSameNode($droppedSy)) { + $preferedId = $id; // ... better : it was the dropped target + } + $upPaths[$teid]['synonyms'][$id] = $s; + } + if ($preferedId) { + $upPaths[$teid]['synonyms'][$preferedId]['selected'] = true; + } + } + $upPaths[$teid]['fields'][$field->get_id()] = $field; + } + } + $twp['up_paths'] = array_reverse($upPaths); + $twp['fields'][] = $field; + // $field-> + } + } + + if (empty($upPaths)) { + // no fields (could happen if one drops on a top-level branch, or if the th is not linked, or...) + throw new Exception("this branch is not linked"); + } + + } + catch (Exception $e) { + $twp['error'] = $e->getMessage(); + } + + return $this->render('prod/Thesaurus/droppedrecords.html.twig', $twp); + } +} diff --git a/lib/Alchemy/Phrasea/ControllerProvider/ControllerProviderServiceProvider.php b/lib/Alchemy/Phrasea/ControllerProvider/ControllerProviderServiceProvider.php index 6eac03e8d5..f91484c5a5 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/ControllerProviderServiceProvider.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/ControllerProviderServiceProvider.php @@ -87,6 +87,7 @@ class ControllerProviderServiceProvider implements ServiceProviderInterface Prod\Share::class => [], Prod\Story::class => [], Prod\Subdefs::class => [], + Prod\Thesaurus::class => [], Prod\Tools::class => [], Prod\Tooltip::class => [], Prod\TOU::class => [], diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php new file mode 100644 index 0000000000..e98bf994ce --- /dev/null +++ b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Thesaurus.php @@ -0,0 +1,55 @@ +share(function (PhraseaApplication $app) { + return (new ThesaurusController($app)) + ->setDataboxLoggerLocator($app['phraseanet.logger']) + ->setDispatcher($app['dispatcher']) + ->setFileSystemLocator(new LazyLocator($app, 'filesystem')) + ; + }); + } + + public function boot(Application $app) + { + // no-op + } + + public function connect(Application $app) + { + $controllers = $this->createAuthenticatedCollection($app); +// $firewall = $this->getFirewall($app); + +// $controllers->before(function () use ($firewall) { +// $firewall->requireRight(\ACL::CANMODIFRECORD); +// }); + + $controllers->get('/droprecords', 'controller.prod.thesaurus:dropRecordsAction'); + + return $controllers; + } +} diff --git a/templates/web/prod/Thesaurus/droppedrecords.html.twig b/templates/web/prod/Thesaurus/droppedrecords.html.twig new file mode 100644 index 0000000000..0e1006725b --- /dev/null +++ b/templates/web/prod/Thesaurus/droppedrecords.html.twig @@ -0,0 +1,122 @@ +{% if error %} + {{ error }} +{% else %} +
+ + + + + + + {% if up_paths|length > 1 %} + + + + {% endif %} + {% set n=1 %} + {% for tid,up_path in up_paths %} + {% set cl = (n == up_paths|length) ? "" : "class='other' style='display:none'"%} + + + + + + + {% set n = n+1 %} + {% endfor %} + +
set value + to field
+  Other value(s)... +
+ {% if 0 and up_path.synonyms|length == 1 %} + {% set synonym = up_path.synonyms|first %} + + {% else %} + + {% endif %} + + → + + +
+
+{% endif %} + + \ No newline at end of file diff --git a/templates/web/thesaurus/thesaurus.html.twig b/templates/web/thesaurus/thesaurus.html.twig index b384e7c8bb..1ce8830e10 100644 --- a/templates/web/thesaurus/thesaurus.html.twig +++ b/templates/web/thesaurus/thesaurus.html.twig @@ -1082,8 +1082,10 @@ url += "&typ=" + typ; url += "&sylng=" + encodeURIComponent(lng); + var title = typ=="TS" ? "{{ 'thesaurus:: Nouveau terme specifique' | trans }}" : "{{ 'thesaurus:: Nouveau synonyme' | trans }}"; + // w = window.open(url, "NEWTERM", "directories=no, height=290, width=490, location=no, menubar=no, resizable=yes, scrollbars=yes, status=no, toolbar=no"); - w = loadDataAjax(url,490, 350,"#NEWSY_DLG_CONFIRM",'{% if typ == "TS" %}{{ 'thesaurus:: Nouveau terme specifique' | trans }}{% else %}{{ 'thesaurus:: Nouveau synonyme' | trans }}{% endif %}'); + w = loadDataAjax(url, 490, 350, "#NEWSY_DLG_CONFIRM", title); } } ],