libreqda 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +481 -0
- libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
- libreqda-1.2.0.dist-info/METADATA +131 -0
- libreqda-1.2.0.dist-info/RECORD +255 -0
- libreqda-1.2.0.dist-info/WHEEL +4 -0
- libreqda-1.2.0.dist-info/entry_points.txt +4 -0
- scripts/babelrc +8 -0
- scripts/dist/LICENSE.txt +33 -0
- scripts/dist/RELEASE.txt +23 -0
- scripts/dist/macos/libreqda.icns +0 -0
- scripts/dist/macos/macos.spec +43 -0
- scripts/dist/macos/plist.patch +10 -0
- scripts/dist/macos/taguette.icns +0 -0
- scripts/dist/macos/taguette_console_wrapper +4 -0
- scripts/dist/macos.sh +32 -0
- scripts/dist/pyinstaller_entrypoint.py +19 -0
- scripts/dist/windows/windows-installer.iss +51 -0
- scripts/dist/windows/windows.spec +39 -0
- scripts/edugain/worker_update_edugain_idps.py +89 -0
- scripts/edugain/worker_update_edugain_idps.sh +9 -0
- scripts/make_password_reset_link.py +56 -0
- scripts/make_theme.py +99 -0
- scripts/new_db_revision.sh +8 -0
- scripts/register_user.py +76 -0
- scripts/update_pot.sh +33 -0
- scripts/update_translations.sh +17 -0
- scripts/validate.py +140 -0
- scripts/wipe_and_disable_user.py +90 -0
- taguette/__init__.py +13 -0
- taguette/access_control.py +145 -0
- taguette/convert.py +623 -0
- taguette/database/__init__.py +177 -0
- taguette/database/base.py +9 -0
- taguette/database/copy.py +433 -0
- taguette/database/models.py +920 -0
- taguette/demodata.py +688 -0
- taguette/export.py +500 -0
- taguette/extract.py +287 -0
- taguette/import_codebook.py +110 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/loki_handler.py +107 -0
- taguette/main.py +649 -0
- taguette/migrations/env.py +72 -0
- taguette/migrations/script.py.mako +24 -0
- taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
- taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
- taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
- taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
- taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
- taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
- taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
- taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
- taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
- taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
- taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
- taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
- taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
- taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
- taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
- taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
- taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
- taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
- taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
- taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
- taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
- taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
- taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
- taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
- taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
- taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
- taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
- taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
- taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
- taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
- taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
- taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
- taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
- taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
- taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
- taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
- taguette/static/README.md +34 -0
- taguette/static/alt_templates/.gitkeep +0 -0
- taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
- taguette/static/css/custom.css +1696 -0
- taguette/static/css/fa-all.min.css +5 -0
- taguette/static/css/themed.css +3 -0
- taguette/static/css/themed.css.map +25 -0
- taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
- taguette/static/css/vendor/bootstrap.min.css +6 -0
- taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
- taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
- taguette/static/css/vendor/hierarchy-select.min.css +1 -0
- taguette/static/css/vendor/introjs.min.css +2 -0
- taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
- taguette/static/favicon.ico +0 -0
- taguette/static/js/codebook-viz.js +428 -0
- taguette/static/js/collection/context-menu.js +180 -0
- taguette/static/js/collection/hierarchy-select.js +71 -0
- taguette/static/js/collection/manager.js +217 -0
- taguette/static/js/document/context-menu.js +92 -0
- taguette/static/js/document/custom-select.js +82 -0
- taguette/static/js/document/manager.js +943 -0
- taguette/static/js/document/treeview.js +593 -0
- taguette/static/js/highlight/README.md +18 -0
- taguette/static/js/highlight/crud.js +119 -0
- taguette/static/js/highlight/extracts.js +497 -0
- taguette/static/js/highlight/loader.js +109 -0
- taguette/static/js/highlight/renderer.js +405 -0
- taguette/static/js/login.js +15 -0
- taguette/static/js/permissions.js +153 -0
- taguette/static/js/polling-event.js +145 -0
- taguette/static/js/project/intro.js +524 -0
- taguette/static/js/project/manager.js +0 -0
- taguette/static/js/project/search.js +124 -0
- taguette/static/js/tag/context-menu.js +133 -0
- taguette/static/js/tag/explore-codes.js +400 -0
- taguette/static/js/tag/hierarchy-select.js +69 -0
- taguette/static/js/tag/manager.js +545 -0
- taguette/static/js/tag/treeview.js +613 -0
- taguette/static/js/taguette.js +1790 -0
- taguette/static/js/utilities.js +714 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
- taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
- taguette/static/js/vendor/hierarchy-select.min.js +1 -0
- taguette/static/js/vendor/introjs.min.js +17 -0
- taguette/static/js/vendor/jquery-3.7.1.js +2 -0
- taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
- taguette/static/libreqda.ico +0 -0
- taguette/static/logo-h.png +0 -0
- taguette/static/logo.png +0 -0
- taguette/static/logo_blanc.png +0 -0
- taguette/static/missing.png +0 -0
- taguette/static/robots.txt +2 -0
- taguette/static/scss/components/_badge.scss +42 -0
- taguette/static/scss/components/_button.scss +169 -0
- taguette/static/scss/components/_checkbox.scss +40 -0
- taguette/static/scss/components/_context_menu.scss +28 -0
- taguette/static/scss/components/_custom_select.scss +150 -0
- taguette/static/scss/components/_dropdown.scss +43 -0
- taguette/static/scss/components/_form.scss +54 -0
- taguette/static/scss/components/_modal.scss +57 -0
- taguette/static/scss/components/_project_card.scss +163 -0
- taguette/static/scss/components/_tag.scss +308 -0
- taguette/static/scss/components/_treeview.scss +76 -0
- taguette/static/scss/custom.scss +809 -0
- taguette/static/scss/layout/_credits.scss +62 -0
- taguette/static/scss/layout/_footer.scss +6 -0
- taguette/static/scss/layout/_navigation.scss +165 -0
- taguette/static/scss/layout/_sidebar.scss +37 -0
- taguette/static/scss/themes/default/_variables.scss +103 -0
- taguette/static/scss/themes/default/theme.css +3 -0
- taguette/static/scss/themes/default/theme.css.map +26 -0
- taguette/static/scss/themes/default/theme.scss +4 -0
- taguette/static/webfonts/BadScript-OFL.txt +94 -0
- taguette/static/webfonts/BadScript.ttf +0 -0
- taguette/static/webfonts/Montserrat-Light.ttf +0 -0
- taguette/static/webfonts/Montserrat-OFL.txt +93 -0
- taguette/static/webfonts/fa-brands-400.eot +0 -0
- taguette/static/webfonts/fa-brands-400.svg +1184 -0
- taguette/static/webfonts/fa-brands-400.ttf +0 -0
- taguette/static/webfonts/fa-brands-400.woff +0 -0
- taguette/static/webfonts/fa-brands-400.woff2 +0 -0
- taguette/static/webfonts/fa-regular-400.eot +0 -0
- taguette/static/webfonts/fa-regular-400.svg +467 -0
- taguette/static/webfonts/fa-regular-400.ttf +0 -0
- taguette/static/webfonts/fa-regular-400.woff +0 -0
- taguette/static/webfonts/fa-regular-400.woff2 +0 -0
- taguette/static/webfonts/fa-solid-900.eot +0 -0
- taguette/static/webfonts/fa-solid-900.svg +2618 -0
- taguette/static/webfonts/fa-solid-900.ttf +0 -0
- taguette/static/webfonts/fa-solid-900.woff +0 -0
- taguette/static/webfonts/fa-solid-900.woff2 +0 -0
- taguette/templates/alt_templates/.gitkeep +0 -0
- taguette/templates/archived_projects.html +50 -0
- taguette/templates/auth/account.html +74 -0
- taguette/templates/auth/cilogon.login.html +89 -0
- taguette/templates/auth/email_reset_password.html +5 -0
- taguette/templates/auth/external_account.html +60 -0
- taguette/templates/auth/login.html +144 -0
- taguette/templates/auth/reset_password.html +27 -0
- taguette/templates/auth/token_needed.html +7 -0
- taguette/templates/base.html +209 -0
- taguette/templates/bug_report.html +158 -0
- taguette/templates/cookies_prompt.html +15 -0
- taguette/templates/downloads.html +127 -0
- taguette/templates/email_bug_report.txt +9 -0
- taguette/templates/email_reset_password.txt +5 -0
- taguette/templates/error.html +8 -0
- taguette/templates/export_codebook.html +20 -0
- taguette/templates/export_document.html +20 -0
- taguette/templates/export_highlights.html +47 -0
- taguette/templates/index.html +347 -0
- taguette/templates/maintenance.html +8 -0
- taguette/templates/messages.js +74 -0
- taguette/templates/modal/change_document.html +68 -0
- taguette/templates/modal/create_collection.html +100 -0
- taguette/templates/modal/create_document.html +84 -0
- taguette/templates/modal/create_highlight.html +38 -0
- taguette/templates/modal/create_tag.html +73 -0
- taguette/templates/modal/explore_codes.html +58 -0
- taguette/templates/modal/manage_collaborators.html +44 -0
- taguette/templates/modal/merge_tag.html +49 -0
- taguette/templates/new_password.html +32 -0
- taguette/templates/partials/alert.html +16 -0
- taguette/templates/partials/codebook_viz_modal.html +34 -0
- taguette/templates/partials/confirm.html +17 -0
- taguette/templates/partials/document_toolbar.html +37 -0
- taguette/templates/partials/hlinfo.html +13 -0
- taguette/templates/partials/project_card.html +44 -0
- taguette/templates/partials/project_card_archived.html +41 -0
- taguette/templates/partials/project_sidebar.html +99 -0
- taguette/templates/partials/spinner.html +11 -0
- taguette/templates/permissions.html +54 -0
- taguette/templates/project.html +288 -0
- taguette/templates/project_delete.html +17 -0
- taguette/templates/project_import.html +122 -0
- taguette/templates/project_import_codebook.html +34 -0
- taguette/templates/project_import_codebook_confirm.html +49 -0
- taguette/templates/project_new.html +31 -0
- taguette/templates/tos.html +8 -0
- taguette/templates/trans.js +7 -0
- taguette/templates/welcome.html +15 -0
- taguette/utils.py +177 -0
- taguette/validate.py +141 -0
- taguette/version_check.py +102 -0
- taguette/web/__init__.py +252 -0
- taguette/web/api.py +1835 -0
- taguette/web/base.py +864 -0
- taguette/web/export.py +338 -0
- taguette/web/views.py +1618 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
$(document).ready(function() {
|
|
2
|
+
const regEscape = v => v.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
3
|
+
const $formSearch = $("#form-search-documents");
|
|
4
|
+
const $inputSearch = $("#input-search-documents");
|
|
5
|
+
const $divSearchResults = $("#ul-search-results");
|
|
6
|
+
const minimumCharacters = 3;
|
|
7
|
+
let searchTimeout = null;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
$inputSearch.keyup(function(){
|
|
11
|
+
clearTimeout(searchTimeout);
|
|
12
|
+
searchTimeout = setTimeout(function(){
|
|
13
|
+
doSearch();
|
|
14
|
+
}, 500);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
$formSearch.submit(function(e){
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
clearTimeout(searchTimeout);
|
|
20
|
+
doSearch();
|
|
21
|
+
|
|
22
|
+
return false;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
$('button.page-selector').click(function(e) {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
|
|
29
|
+
doSearch($(this).data('page'));
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function showSearchSpinner() {
|
|
35
|
+
$formSearch.addClass('searching');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hideSearchSpinner() {
|
|
39
|
+
$formSearch.removeClass('searching');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearSearchContent() {
|
|
43
|
+
$divSearchResults.html('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function doSearch(page=1) {
|
|
47
|
+
const searchText = $inputSearch.val().toLowerCase(),
|
|
48
|
+
previousPage = page - 1,
|
|
49
|
+
nextPage = page + 1;
|
|
50
|
+
log(`Search in documents '${searchText}'`);
|
|
51
|
+
$divSearchResults.html('');
|
|
52
|
+
$('button.page-selector').hide();
|
|
53
|
+
if(searchText.length < minimumCharacters) {
|
|
54
|
+
log(`Not enough characters to search documents, ${minimumCharacters} or more characters are required`)
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if(searchText !== '') {
|
|
59
|
+
showSearchSpinner();
|
|
60
|
+
|
|
61
|
+
getJSON(
|
|
62
|
+
`/api/project/${project_id}/search/${encodeURIComponent(searchText)}?page=${page}`
|
|
63
|
+
)
|
|
64
|
+
.then(function (result) {
|
|
65
|
+
$("<li/>", {
|
|
66
|
+
'class': "dropdown-item disabled sticky first",
|
|
67
|
+
'html': gettext("Found in %(nb)s document(s)", {nb: result['nb_results']}) + `<span class='pagination'>${page} / ${result['pages']}</span>`
|
|
68
|
+
}).appendTo($divSearchResults);
|
|
69
|
+
if(result['nb_results'] > 0) {
|
|
70
|
+
$('#btn-search-previous-page').data('page', previousPage);
|
|
71
|
+
$('#btn-search-next-page').data('page', nextPage);
|
|
72
|
+
if(previousPage <= 0) {
|
|
73
|
+
$('#btn-search-previous-page').prop('disabled', true);
|
|
74
|
+
} else {
|
|
75
|
+
$('#btn-search-previous-page').prop('disabled', false);
|
|
76
|
+
}
|
|
77
|
+
log(`nextpage: ${nextPage} >= ${result['pages']}`)
|
|
78
|
+
if(nextPage > result['pages']) {
|
|
79
|
+
$('#btn-search-next-page').prop('disabled', true);
|
|
80
|
+
} else {
|
|
81
|
+
$('#btn-search-next-page').prop('disabled', false);
|
|
82
|
+
}
|
|
83
|
+
$('button.page-selector').show();
|
|
84
|
+
} else {
|
|
85
|
+
$('button.page-selector').hide();
|
|
86
|
+
}
|
|
87
|
+
for(let k in result['documents']) {
|
|
88
|
+
let d = result['documents'][k];
|
|
89
|
+
// Add document title
|
|
90
|
+
$("<li/>", {
|
|
91
|
+
'class': "dropdown-item disabled sticky",
|
|
92
|
+
'text': `${d['name']} (${d['occurences']} occurrences)`
|
|
93
|
+
}).appendTo($divSearchResults);
|
|
94
|
+
|
|
95
|
+
// Find results and context
|
|
96
|
+
let results = d['contents'].split(new RegExp(regEscape(result['search_text']), "ig"));
|
|
97
|
+
for(let i=0; i<results.length-1; i++) {
|
|
98
|
+
let start = results[i].slice(-40),
|
|
99
|
+
end = results[i+1].slice(0, 40),
|
|
100
|
+
a = document.createElement('a');
|
|
101
|
+
a.className = "dropdown-item";
|
|
102
|
+
a.innerHTML = `...${start}<span class="search-item">${result['search_text']}</span>${end}...`
|
|
103
|
+
linkDocument(a, d['id'], null, `${result['search_text']}/${i}`);
|
|
104
|
+
$("<li/>").append(a).appendTo($divSearchResults);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Force show
|
|
109
|
+
if(!$("#dropdown-trigger-search").hasClass('show')) {
|
|
110
|
+
$("#dropdown-trigger-search").dropdown('toggle');
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.catch(function (error) {
|
|
114
|
+
log("Failed to search in documents", 'error', {error});
|
|
115
|
+
showAlert(gettext("Error searching documents!"), 'error', {error})
|
|
116
|
+
})
|
|
117
|
+
.then(function (result) {
|
|
118
|
+
hideSearchSpinner();
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
clearSearchContent()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
|
|
2
|
+
function addContextMenu() {
|
|
3
|
+
$.contextMenu({
|
|
4
|
+
selector: '.tag-context-menu',
|
|
5
|
+
build: function ($trigger) {
|
|
6
|
+
const nodeId = $trigger.attr('id');
|
|
7
|
+
const node = getNode(treeview_tags, nodeId);
|
|
8
|
+
|
|
9
|
+
let items = generateContextMenuItems(node);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
callback: (key, options) => contextMenuAction(key, options.$trigger.attr("id")),
|
|
13
|
+
items
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generateContextMenuItems(node) {
|
|
20
|
+
|
|
21
|
+
const tagId = getTagIdFromTreeviewNode(treeview_tags, node.nodeId);
|
|
22
|
+
const tag = findTag(tagId);
|
|
23
|
+
|
|
24
|
+
const hasChildren = tag.children.length > 0;
|
|
25
|
+
const hasParent = tag.parent > 0;
|
|
26
|
+
const isExpanded = node.state.expanded;
|
|
27
|
+
const isSelected = node.state.selected;
|
|
28
|
+
|
|
29
|
+
const selectedNodes = getAllSelectedNodes(treeview_tags).length > 0;
|
|
30
|
+
const hasSelectedChildren = checkNodeHasSelectedChildren(node);
|
|
31
|
+
const hasUnselectedChildren = checkNodeHasUnselectedChildren(node);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"toggleSelectAll": {
|
|
35
|
+
name: gettext('selectAll'),
|
|
36
|
+
icon: 'fas fa-check-square',
|
|
37
|
+
visible: hasUnselectedChildren && hasChildren,
|
|
38
|
+
callback: (key, options) => contextMenuAction('selectAll', node.nodeId)
|
|
39
|
+
},
|
|
40
|
+
"toggleDeSelectAll": {
|
|
41
|
+
name: gettext('unselectAll'),
|
|
42
|
+
icon: 'fas fa-square',
|
|
43
|
+
visible: hasChildren && hasSelectedChildren,
|
|
44
|
+
callback: (key, options) => contextMenuAction('unselectAll', node.nodeId)
|
|
45
|
+
},
|
|
46
|
+
"exploreCode": {
|
|
47
|
+
name: gettext('Explore codes'),
|
|
48
|
+
icon: 'fas fa-search',
|
|
49
|
+
callback: (key, options) => contextMenuAction('exploreCode', tagId)
|
|
50
|
+
},
|
|
51
|
+
"toggleSelect": {
|
|
52
|
+
name: gettext('Select With Condition'),
|
|
53
|
+
icon: 'fas fa-filter',
|
|
54
|
+
visible: false,
|
|
55
|
+
items: {
|
|
56
|
+
"andSelect": {
|
|
57
|
+
name: gettext('AND'),
|
|
58
|
+
icon: 'fas fa-plus',
|
|
59
|
+
callback: (key, options) => contextMenuAction('selectAnd', node.nodeId)
|
|
60
|
+
},
|
|
61
|
+
"orSelect": {
|
|
62
|
+
name: gettext('OR'),
|
|
63
|
+
icon: 'fas fa-exchange-alt',
|
|
64
|
+
callback: (key, options) => contextMenuAction('selectOr', node.nodeId)
|
|
65
|
+
},
|
|
66
|
+
"notSelect": {
|
|
67
|
+
name: gettext('NOT'),
|
|
68
|
+
icon: 'fas fa-ban',
|
|
69
|
+
callback: (key, options) => contextMenuAction('selectNot', node.nodeId)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"toggleExpand": {
|
|
74
|
+
name: isExpanded ? gettext('collapse') : gettext('expand'),
|
|
75
|
+
icon: isExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down',
|
|
76
|
+
visible: hasChildren,
|
|
77
|
+
callback: (key, options) => contextMenuAction(isExpanded ? 'collapse' : 'expand', node.nodeId)
|
|
78
|
+
},
|
|
79
|
+
"addChild": {
|
|
80
|
+
name: gettext('Add child'),
|
|
81
|
+
icon: "fas fa-plus-circle",
|
|
82
|
+
callback: (key, options) => contextMenuAction('addChild', tagId)
|
|
83
|
+
},
|
|
84
|
+
"edit": {
|
|
85
|
+
name: gettext('edit'),
|
|
86
|
+
icon: "fas fa-pen",
|
|
87
|
+
callback: (key, options) => contextMenuAction('edit', tagId)
|
|
88
|
+
},
|
|
89
|
+
"merge": {
|
|
90
|
+
name: gettext('merge'),
|
|
91
|
+
icon: "fas fa-copy",
|
|
92
|
+
callback: (key, options) => contextMenuAction('merge', tagId)
|
|
93
|
+
},
|
|
94
|
+
"deleteParent": {
|
|
95
|
+
name: gettext('deleteParent'),
|
|
96
|
+
icon: "fas fa-times-circle",
|
|
97
|
+
visible: hasParent,
|
|
98
|
+
callback: (key, options) => contextMenuAction('deleteParent', tagId)
|
|
99
|
+
},
|
|
100
|
+
"delete": {
|
|
101
|
+
name: gettext('delete'),
|
|
102
|
+
icon: "fas fa-trash",
|
|
103
|
+
callback: (key, options) => contextMenuAction('delete', tagId)
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function contextMenuAction(action, param) {
|
|
109
|
+
const actionFunction = actions[action];
|
|
110
|
+
|
|
111
|
+
if (actionFunction) {
|
|
112
|
+
actionFunction(param);
|
|
113
|
+
} else {
|
|
114
|
+
log('No action found for:', 'warn', {action});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const actions = {
|
|
119
|
+
selectAll: nodeId => selectNodeAndChildren(treeview_tags, nodeId),
|
|
120
|
+
unselectAll: nodeId => unselectNodeAndChildren(treeview_tags, nodeId),
|
|
121
|
+
selectAnd: nodeId => selectWithCondition(treeview_tags, nodeId, 'and'),
|
|
122
|
+
selectOr: nodeId => selectWithCondition(treeview_tags, nodeId, 'or'),
|
|
123
|
+
selectNot: nodeId => selectWithCondition(treeview_tags, nodeId, 'not'),
|
|
124
|
+
exploreCode: tagId => openExploreCodeFromContextMenu(tagId),
|
|
125
|
+
expand: nodeId => expandNode(treeview_tags, nodeId),
|
|
126
|
+
collapse: nodeId => collapseNode(treeview_tags, nodeId),
|
|
127
|
+
unselect: nodeId => unselectNode(treeview_tags, nodeId),
|
|
128
|
+
edit: tagId => editTag(tagId),
|
|
129
|
+
addChild: tagId => createTag(false, current_selection, tagId),
|
|
130
|
+
merge: tagId => mergeTag(tagId),
|
|
131
|
+
delete: tagId => deleteTag(tagId),
|
|
132
|
+
deleteParent: tagId => removeTagParent(tagId)
|
|
133
|
+
};
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
const treeview_tags_explore = $('#treeview-tags-explore');
|
|
2
|
+
|
|
3
|
+
let exploreSelectedTags = []; // [{id, path, zone}] where zone is 'must' | 'can' | 'exclude'
|
|
4
|
+
let exploreActiveZone = null;
|
|
5
|
+
let appliedExploreTags = []; // last query actually applied, kept after the modal closes so it can be re-edited
|
|
6
|
+
|
|
7
|
+
function initTagsExploreTreeview(filterTags = null) {
|
|
8
|
+
|
|
9
|
+
if (filterTags == null) {
|
|
10
|
+
filterTags = tags;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const treeData = convertToTreeNode(filterTags, true);
|
|
14
|
+
|
|
15
|
+
const customNodeOptions = customNodeRow(false, true, false, 'tag-node-explore-');
|
|
16
|
+
|
|
17
|
+
const options = {
|
|
18
|
+
data: treeData,
|
|
19
|
+
...customNodeOptions,
|
|
20
|
+
showBorder: true,
|
|
21
|
+
levels: CUSTOM_LEVEL_OPTION,
|
|
22
|
+
multiSelect: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
treeview_tags_explore.treeview(options);
|
|
26
|
+
|
|
27
|
+
treeview_tags_explore.off('nodeSelected').on('nodeSelected', function (event, node) {
|
|
28
|
+
if (!exploreSelectedTags.some(item => item.id === node.id)) {
|
|
29
|
+
exploreSelectedTags.push({
|
|
30
|
+
id: node.id,
|
|
31
|
+
path: node.text,
|
|
32
|
+
zone: exploreActiveZone
|
|
33
|
+
});
|
|
34
|
+
renderExploreZones();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only clear on a genuine user click, not the programmatic re-selection
|
|
38
|
+
// that happens when the tree is rebuilt (e.g. restoring selections).
|
|
39
|
+
if (!isProgrammaticSelection) {
|
|
40
|
+
clearExploreSearchFilter();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
treeview_tags_explore.off('nodeUnselected').on('nodeUnselected', function (event, node) {
|
|
45
|
+
// Clicking a code that's already picked but for a different zone
|
|
46
|
+
// means "move it here", not "drop it": update its zone and put the
|
|
47
|
+
// node back in the tree so it stays checked.
|
|
48
|
+
const existing = exploreSelectedTags.find(item => item.id === node.id);
|
|
49
|
+
if (
|
|
50
|
+
!isProgrammaticSelection
|
|
51
|
+
&& existing
|
|
52
|
+
&& exploreActiveZone
|
|
53
|
+
&& existing.zone !== exploreActiveZone
|
|
54
|
+
) {
|
|
55
|
+
existing.zone = exploreActiveZone;
|
|
56
|
+
renderExploreZones();
|
|
57
|
+
const nodeId = getNodeIdFromTreeviewByTagId(
|
|
58
|
+
treeview_tags_explore, node.id,
|
|
59
|
+
);
|
|
60
|
+
if (nodeId !== null) {
|
|
61
|
+
selectNode(treeview_tags_explore, nodeId);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
exploreSelectedTags = exploreSelectedTags.filter(item => item.id !== node.id);
|
|
67
|
+
renderExploreZones();
|
|
68
|
+
|
|
69
|
+
// Same as nodeSelected: only clear on a genuine user click, not a
|
|
70
|
+
// programmatic unselection.
|
|
71
|
+
if (!isProgrammaticSelection) {
|
|
72
|
+
clearExploreSearchFilter();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
restoreExploreSelectedNodes();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clearExploreSearchFilter() {
|
|
80
|
+
const searchInput = $('#input-search-tags-explore');
|
|
81
|
+
if (searchInput.val() === '') return;
|
|
82
|
+
searchInput.val('');
|
|
83
|
+
$('#alert-tag-explore-treeview').addClass('d-none');
|
|
84
|
+
// Deferred so it runs after the treeview's own pending render (see treeview.js's similar setTimeout uses).
|
|
85
|
+
setTimeout(() => initTagsExploreTreeview(), 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function restoreExploreSelectedNodes() {
|
|
89
|
+
exploreSelectedTags.forEach(item => {
|
|
90
|
+
const nodeId = getNodeIdFromTreeviewByTagId(treeview_tags_explore, item.id);
|
|
91
|
+
if (nodeId !== null) {
|
|
92
|
+
selectNode(treeview_tags_explore, nodeId);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function generateExploreChipHtml(tag) {
|
|
98
|
+
const abbreviation = abbreviateTag(tag.path);
|
|
99
|
+
return `<span class="tag-badge me-2 my-1" title="${tag.path}" style="background-color: ${tag.color};">
|
|
100
|
+
${abbreviation}
|
|
101
|
+
<button type="button" onclick="event.stopPropagation(); removeExploreChip(${tag.id})" class="tag-badge-button">x</button>
|
|
102
|
+
</span>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderExploreZones() {
|
|
106
|
+
const zones = {
|
|
107
|
+
must: $('#explore-zone-must-chips'),
|
|
108
|
+
can: $('#explore-zone-can-chips'),
|
|
109
|
+
exclude: $('#explore-zone-exclude-chips')
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
Object.keys(zones).forEach(zoneKey => {
|
|
113
|
+
const zoneTags = exploreSelectedTags.filter(item => item.zone === zoneKey);
|
|
114
|
+
if (zoneTags.length === 0) {
|
|
115
|
+
const noCodesYetLabel = gettext('No codes yet');
|
|
116
|
+
zones[zoneKey].html(`<span class="text-muted small fst-italic">${noCodesYetLabel}</span>`);
|
|
117
|
+
} else {
|
|
118
|
+
let content = '';
|
|
119
|
+
zoneTags.forEach(item => {
|
|
120
|
+
const tag = findTag(item.id);
|
|
121
|
+
if (tag) {
|
|
122
|
+
content += generateExploreChipHtml(tag);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
zones[zoneKey].html(content);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const hasQuery = exploreSelectedTags.some(item => item.zone === 'must' || item.zone === 'can');
|
|
130
|
+
$('#explore-codes-apply').prop('disabled', !hasQuery);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeExploreChip(tagId) {
|
|
134
|
+
const nodeId = getNodeIdFromTreeviewByTagId(treeview_tags_explore, tagId);
|
|
135
|
+
if (nodeId !== null) {
|
|
136
|
+
unselectNode(treeview_tags_explore, nodeId);
|
|
137
|
+
} else {
|
|
138
|
+
exploreSelectedTags = exploreSelectedTags.filter(item => item.id !== tagId);
|
|
139
|
+
renderExploreZones();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Toggle which zone newly-clicked codes are added to. Reveals the code picker
|
|
144
|
+
// (search box + codebook tree) in place; clicking the same zone's + again
|
|
145
|
+
// closes it, clicking a different zone's + retargets it and flashes the
|
|
146
|
+
// picker so the change in destination is noticeable.
|
|
147
|
+
function toggleExploreZone(zone) {
|
|
148
|
+
const picker = $('#explore-code-picker');
|
|
149
|
+
const isOpen = !picker.hasClass('d-none');
|
|
150
|
+
|
|
151
|
+
if (isOpen && exploreActiveZone === zone) {
|
|
152
|
+
picker.addClass('d-none');
|
|
153
|
+
exploreActiveZone = null;
|
|
154
|
+
updateActiveZoneStyling();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const zoneChanged = isOpen && exploreActiveZone !== zone;
|
|
159
|
+
exploreActiveZone = zone;
|
|
160
|
+
updateActiveZoneStyling();
|
|
161
|
+
picker.removeClass('d-none');
|
|
162
|
+
|
|
163
|
+
if (zoneChanged) {
|
|
164
|
+
flashExplorePicker();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleZoneBoxKeydown(event, zone) {
|
|
169
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
toggleExploreZone(zone);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function updateActiveZoneStyling() {
|
|
176
|
+
$('.explore-zone-box').removeClass('zone-active');
|
|
177
|
+
if (exploreActiveZone) {
|
|
178
|
+
$(`#explore-zone-${exploreActiveZone}-box`).addClass('zone-active');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function flashExplorePicker() {
|
|
183
|
+
const picker = document.getElementById('explore-code-picker');
|
|
184
|
+
picker.classList.remove('explore-picker-flash');
|
|
185
|
+
void picker.offsetWidth; // force reflow so the animation restarts on repeated clicks
|
|
186
|
+
picker.classList.add('explore-picker-flash');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildExploreQueryString() {
|
|
190
|
+
const must = exploreSelectedTags.filter(item => item.zone === 'must').map(item => item.path);
|
|
191
|
+
const can = exploreSelectedTags.filter(item => item.zone === 'can').map(item => item.path);
|
|
192
|
+
const exclude = exploreSelectedTags.filter(item => item.zone === 'exclude').map(item => item.path);
|
|
193
|
+
|
|
194
|
+
const mustGroup = must.join('∩');
|
|
195
|
+
|
|
196
|
+
let groups;
|
|
197
|
+
if (can.length > 0) {
|
|
198
|
+
groups = can.map(path => mustGroup ? `${mustGroup}∩${path}` : path);
|
|
199
|
+
} else {
|
|
200
|
+
groups = mustGroup ? [mustGroup] : [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let query = groups.join('∪');
|
|
204
|
+
exclude.forEach(path => {
|
|
205
|
+
query += `¬${path}`;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return query;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function applyExploreQuery() {
|
|
212
|
+
const queryString = buildExploreQueryString();
|
|
213
|
+
if (!queryString) return;
|
|
214
|
+
|
|
215
|
+
const tagsSnapshot = exploreSelectedTags.slice();
|
|
216
|
+
appliedExploreTags = tagsSnapshot.map(item => ({...item}));
|
|
217
|
+
|
|
218
|
+
$('#explore-codes-modal').modal('hide');
|
|
219
|
+
|
|
220
|
+
const url = `${base_path}/project/${project_id}/highlights/${encodeURIComponent(queryString)}`;
|
|
221
|
+
window.history.pushState({tag_path: queryString}, `Tag ${queryString}`, url);
|
|
222
|
+
// Shows the progress bar animating over 600ms (vs. the brief spinner flash
|
|
223
|
+
// on a normal tag click), so applying a query reads as "computed", not a glitch.
|
|
224
|
+
loadTag(queryString, undefined, 600);
|
|
225
|
+
|
|
226
|
+
const tags_return = $('#tag-selection-return'),
|
|
227
|
+
tags_return_link = $('a', tags_return);
|
|
228
|
+
tags_return_link.unbind('click').hide();
|
|
229
|
+
tags_return_link.attr('href', url);
|
|
230
|
+
tags_return_link.bind('click', function (e) {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
if (window.history.state.tag_path === '') {
|
|
233
|
+
loadTag('');
|
|
234
|
+
selectAllTag();
|
|
235
|
+
} else {
|
|
236
|
+
window.history.back();
|
|
237
|
+
}
|
|
238
|
+
tags_return_link.hide();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
showExploreQuerySummary(tagsSnapshot);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Mirrors showTagSelection()'s sidebar query box, but grouped by zone since the
|
|
245
|
+
// explore query's AND/OR/NOT structure can't always be expressed as the linear
|
|
246
|
+
// chain that selectedTags/showTagSelection() understands.
|
|
247
|
+
function showExploreQuerySummary(tagsSnapshot) {
|
|
248
|
+
const criteriaArea = $('#tag-selection-criteria-area');
|
|
249
|
+
|
|
250
|
+
const zoneLabels = {
|
|
251
|
+
can: gettext('Can use'),
|
|
252
|
+
must: gettext('Must use'),
|
|
253
|
+
exclude: gettext('Exclude')
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const editSelectionLabel = gettext('Edit selection');
|
|
257
|
+
const clearSelectionLabel = gettext('Clear selection');
|
|
258
|
+
let content = '<div class="d-flex justify-content-center gap-3 explore-query-actions">';
|
|
259
|
+
content += `<span id="edit-selection" onclick="editExploreQuery()" style="cursor:pointer;">` +
|
|
260
|
+
`<i class="fas fa-pen" aria-hidden="true" title="${editSelectionLabel}"></i></span>`;
|
|
261
|
+
content += `<span id="clear-selection" onclick="clearExploreQuerySelection()" style="cursor:pointer;">` +
|
|
262
|
+
`<i class="fa fa-times" aria-hidden="true" title="${clearSelectionLabel}"></i></span>`;
|
|
263
|
+
content += '</div>';
|
|
264
|
+
|
|
265
|
+
['can', 'must', 'exclude'].forEach(zoneKey => {
|
|
266
|
+
const zoneTags = tagsSnapshot.filter(item => item.zone === zoneKey);
|
|
267
|
+
if (zoneTags.length === 0) return;
|
|
268
|
+
|
|
269
|
+
content += `<div class="mt-1"><strong style="font-size: 0.8rem;">${zoneLabels[zoneKey]}:</strong> `;
|
|
270
|
+
zoneTags.forEach(item => {
|
|
271
|
+
const tag = findTag(item.id);
|
|
272
|
+
if (tag) {
|
|
273
|
+
content += `<span title="${tag.path}" class="tag-badge me-1" style="background-color: ${tag.color};">${abbreviateTag(tag.path)}</span>`;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
content += '</div>';
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
criteriaArea.html(content).show();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function clearExploreQuerySelection() {
|
|
283
|
+
appliedExploreTags = [];
|
|
284
|
+
const url = `${base_path}/project/${project_id}/highlights/`;
|
|
285
|
+
window.history.pushState({tag_path: ''}, 'Tag ', url);
|
|
286
|
+
$('#tag-selection-criteria-area').hide().html('');
|
|
287
|
+
hideTagReturnLink();
|
|
288
|
+
loadDocument(current_document);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function seedExploreModalAndOpen(tagsToSeed) {
|
|
292
|
+
exploreSelectedTags = tagsToSeed.map(item => ({...item}));
|
|
293
|
+
initTagsExploreTreeview();
|
|
294
|
+
renderExploreZones();
|
|
295
|
+
openExploreCodes();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Reopen the modal with the currently applied query's zones populated, so it
|
|
299
|
+
// can be tweaked and re-applied instead of having to rebuild it from scratch.
|
|
300
|
+
function editExploreQuery() {
|
|
301
|
+
seedExploreModalAndOpen(appliedExploreTags);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Right-click a code in the codebook tree -> "Explore codes": opens the modal
|
|
305
|
+
// with that code already added to "Can use", for people who reach conditional
|
|
306
|
+
// selection via the context menu rather than the toolbar button.
|
|
307
|
+
function openExploreCodeFromContextMenu(tagId) {
|
|
308
|
+
const tag = findTag(tagId);
|
|
309
|
+
if (!tag) return;
|
|
310
|
+
seedExploreModalAndOpen([{id: tag.id, path: tag.path, zone: 'can'}]);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resetExploreModal() {
|
|
314
|
+
exploreSelectedTags = [];
|
|
315
|
+
deselectAllNodes(treeview_tags_explore);
|
|
316
|
+
exploreActiveZone = null;
|
|
317
|
+
updateActiveZoneStyling();
|
|
318
|
+
$('#explore-code-picker').addClass('d-none').removeClass('explore-picker-flash');
|
|
319
|
+
$('#input-search-tags-explore').val('');
|
|
320
|
+
$('#alert-tag-explore-treeview').addClass('d-none');
|
|
321
|
+
initTagsExploreTreeview();
|
|
322
|
+
renderExploreZones();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function openExploreCodes() {
|
|
326
|
+
const modal = document.getElementById('explore-codes-modal');
|
|
327
|
+
$(modal).modal('show').drags({handle: '.modal-header'});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseExploreQueryString(queryString) {
|
|
331
|
+
if (!queryString) return [];
|
|
332
|
+
|
|
333
|
+
const parts = queryString.split('¬');
|
|
334
|
+
const head = parts[0];
|
|
335
|
+
const excludePaths = parts.slice(1).filter(Boolean);
|
|
336
|
+
const groups = head
|
|
337
|
+
? head.split('∪').map(g => g.split('∩').filter(Boolean))
|
|
338
|
+
: [];
|
|
339
|
+
|
|
340
|
+
let mustPaths = [];
|
|
341
|
+
let canPaths = [];
|
|
342
|
+
if (groups.length === 1) {
|
|
343
|
+
mustPaths = groups[0];
|
|
344
|
+
} else if (groups.length > 1) {
|
|
345
|
+
mustPaths = groups[0].filter(p => groups.every(g => g.includes(p)));
|
|
346
|
+
const seen = new Set();
|
|
347
|
+
groups.forEach(g => g.forEach(p => {
|
|
348
|
+
if (!mustPaths.includes(p) && !seen.has(p)) {
|
|
349
|
+
seen.add(p);
|
|
350
|
+
canPaths.push(p);
|
|
351
|
+
}
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const toTag = (path, zone) => {
|
|
356
|
+
const tag = findTag(path);
|
|
357
|
+
return tag ? {id: tag.id, path: tag.path, zone} : null;
|
|
358
|
+
};
|
|
359
|
+
return [
|
|
360
|
+
...mustPaths.map(p => toTag(p, 'must')),
|
|
361
|
+
...canPaths.map(p => toTag(p, 'can')),
|
|
362
|
+
...excludePaths.map(p => toTag(p, 'exclude')),
|
|
363
|
+
].filter(Boolean);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function restoreExploreQueryFromUrl() {
|
|
367
|
+
const re = new RegExp(`^${escapeRegExp(base_path)}/project/[0-9]+/highlights/([^/]+)`);
|
|
368
|
+
const m = window.location.pathname.match(re);
|
|
369
|
+
if (!m) return;
|
|
370
|
+
const queryString = decodeURIComponent(m[1]);
|
|
371
|
+
const tags = parseExploreQueryString(queryString);
|
|
372
|
+
if (!tags.length) return;
|
|
373
|
+
appliedExploreTags = tags;
|
|
374
|
+
showExploreQuerySummary(tags);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
$(document).ready(function () {
|
|
378
|
+
initTagsExploreTreeview();
|
|
379
|
+
renderExploreZones();
|
|
380
|
+
restoreExploreQueryFromUrl();
|
|
381
|
+
|
|
382
|
+
document.getElementById('input-search-tags-explore').addEventListener('input', function () {
|
|
383
|
+
const filterText = $(this).val().toLowerCase();
|
|
384
|
+
const filteredTags = handleSearch(filterText);
|
|
385
|
+
initTagsExploreTreeview(filteredTags);
|
|
386
|
+
|
|
387
|
+
const alert_explore_treeview = $('#alert-tag-explore-treeview');
|
|
388
|
+
if (filteredTags.length === 0) {
|
|
389
|
+
alert_explore_treeview.removeClass('d-none').html(`<strong>${TAG_NOT_FOUND_MESSAGE}</strong>`);
|
|
390
|
+
} else {
|
|
391
|
+
alert_explore_treeview.addClass('d-none');
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
document.getElementById('explore-codes-apply').addEventListener('click', applyExploreQuery);
|
|
396
|
+
|
|
397
|
+
$('#explore-codes-modal').on('hidden.bs.modal', resetExploreModal);
|
|
398
|
+
|
|
399
|
+
enableTypeToFocusSearch('#explore-codes-modal', '#input-search-tags-explore');
|
|
400
|
+
});
|