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,1790 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Table of contents
|
|
3
|
+
*
|
|
4
|
+
* - Utilities
|
|
5
|
+
* - Selection stuff
|
|
6
|
+
* - Project metadata
|
|
7
|
+
* - Documents list
|
|
8
|
+
* - Add document
|
|
9
|
+
* - Tags list
|
|
10
|
+
* - Highlights
|
|
11
|
+
* - Add highlight
|
|
12
|
+
* - Members
|
|
13
|
+
* - Load contents
|
|
14
|
+
* - Long polling
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* Project metadata
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const project_name_input = document.getElementById('project-name');
|
|
23
|
+
let project_name = project_name_input.value;
|
|
24
|
+
|
|
25
|
+
const project_description_input = document.getElementById('project-description');
|
|
26
|
+
let project_description = project_description_input.value;
|
|
27
|
+
let upload_canceled = false;
|
|
28
|
+
|
|
29
|
+
function openSidebar() {
|
|
30
|
+
|
|
31
|
+
// Desktop behavior
|
|
32
|
+
if (window.innerWidth > 768) {
|
|
33
|
+
document.getElementById("project-sidebar").style.width = "600px";
|
|
34
|
+
} else {
|
|
35
|
+
document.getElementById("project-sidebar").style.width = "100%";
|
|
36
|
+
}
|
|
37
|
+
showMembers();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function closeSideBar() {
|
|
41
|
+
document.getElementById("project-sidebar").style.width = "0";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
document.getElementById('project-sidebar-open').addEventListener('click', openSidebar);
|
|
45
|
+
|
|
46
|
+
document.addEventListener('click', function(event) {
|
|
47
|
+
const sidebar = document.getElementById('project-sidebar');
|
|
48
|
+
const openButton = document.getElementById('project-sidebar-open');
|
|
49
|
+
|
|
50
|
+
if (sidebar && sidebar.style.width && sidebar.style.width !== "0" && sidebar.style.width !== "0px") {
|
|
51
|
+
// Skip targets detached by their own click handler — `contains` would falsely report "outside".
|
|
52
|
+
if (!document.body.contains(event.target)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!sidebar.contains(event.target) && !openButton.contains(event.target)) {
|
|
56
|
+
closeSideBar();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function setProjectMetadata(metadata, form = true) {
|
|
62
|
+
if (project_name === metadata.project_name && project_description === metadata.description) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Update globals
|
|
66
|
+
project_name = metadata.project_name;
|
|
67
|
+
project_description = metadata.description;
|
|
68
|
+
// Update form
|
|
69
|
+
if (form) {
|
|
70
|
+
project_name_input.value = project_name;
|
|
71
|
+
project_description_input.value = project_description;
|
|
72
|
+
}
|
|
73
|
+
// Update elements
|
|
74
|
+
const elems = document.getElementsByClassName('project-name');
|
|
75
|
+
for (let i = 0; i < elems.length; ++i) {
|
|
76
|
+
elems[i].textContent = project_name;
|
|
77
|
+
}
|
|
78
|
+
log("Project metadata updated");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function projectMetadataChanged() {
|
|
82
|
+
if (project_name_input.value !== project_name || project_description_input.value !== project_description) {
|
|
83
|
+
log("Posting project metadata update");
|
|
84
|
+
const meta = {
|
|
85
|
+
name: project_name_input.value, description: project_description_input.value
|
|
86
|
+
};
|
|
87
|
+
postJSON(`/api/project/${project_id}`, meta)
|
|
88
|
+
.then(function () {
|
|
89
|
+
setProjectMetadata({
|
|
90
|
+
project_name: meta.name,
|
|
91
|
+
description: meta.description,
|
|
92
|
+
}, false);
|
|
93
|
+
})
|
|
94
|
+
.catch(function (error) {
|
|
95
|
+
log("Failed to update project metadata:", 'error', {error});
|
|
96
|
+
const updateProjectMetadataErrorMessage = gettext("Couldn't update project metadata!");
|
|
97
|
+
showAlert(`${updateProjectMetadataErrorMessage} \n \n ${error}`, 'error');
|
|
98
|
+
project_name_input.value = project_name;
|
|
99
|
+
project_description_input.value = project_description;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
document.getElementById('project-metadata-form').addEventListener('submit', function (e) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
projectMetadataChanged();
|
|
107
|
+
});
|
|
108
|
+
project_name_input.addEventListener('blur', projectMetadataChanged);
|
|
109
|
+
project_description_input.addEventListener('blur', projectMetadataChanged);
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Add document
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
const document_add_modal = document.getElementById('document-add-modal');
|
|
118
|
+
|
|
119
|
+
function createDocument() {
|
|
120
|
+
document.getElementById('document-add-form').reset();
|
|
121
|
+
|
|
122
|
+
cleanupDocumentDispatchInterface();
|
|
123
|
+
|
|
124
|
+
initDocumentCollectionHierarchySelect();
|
|
125
|
+
|
|
126
|
+
$(document_add_modal).modal('show').drags({handle: '.modal-header'});
|
|
127
|
+
customSelect.classList.remove("active");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function basename(filename) {
|
|
131
|
+
if (filename) {
|
|
132
|
+
const idx = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
|
|
133
|
+
if (idx > -1) {
|
|
134
|
+
filename = filename.substring(idx + 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return filename;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
141
|
+
|
|
142
|
+
function doUpload(form_data, percent=0) {
|
|
143
|
+
return new Promise(function(resolve, reject) {
|
|
144
|
+
let xhr = new XMLHttpRequest(),
|
|
145
|
+
name = form_data.get('name');
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
xhr.responseType = 'json';
|
|
149
|
+
xhr.open('POST', base_path + '/api/project/' + project_id + '/document/new');
|
|
150
|
+
xhr.onload = function () {
|
|
151
|
+
if (xhr.status === 200) {
|
|
152
|
+
log("Document upload complete");
|
|
153
|
+
resolve({
|
|
154
|
+
'doc_to_load': xhr.response.created,
|
|
155
|
+
'percent': percent
|
|
156
|
+
})
|
|
157
|
+
} else {
|
|
158
|
+
const context = xhr.status;
|
|
159
|
+
log("Document upload failed: status", 'error', {context})
|
|
160
|
+
let error = null;
|
|
161
|
+
try {
|
|
162
|
+
error = `<p>(${name}) ${xhr.response.error}</p>`;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
}
|
|
165
|
+
if (!error) {
|
|
166
|
+
error = `<p>(${name}) Status ${xhr.status}</p>`;
|
|
167
|
+
}
|
|
168
|
+
reject({
|
|
169
|
+
'error_msg': error,
|
|
170
|
+
'percent': percent
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
xhr.onerror = function (e) {
|
|
175
|
+
log("Document upload failed:", 'error', {e});
|
|
176
|
+
reject({
|
|
177
|
+
'error_msg': `<p>${e}</p>`,
|
|
178
|
+
'percent': percent
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
xhr.send(form_data);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function uploadFiles(files) {
|
|
187
|
+
|
|
188
|
+
showSpinner(function(){ upload_canceled = true; }, true);
|
|
189
|
+
await delay(100); // Trick to refresh dom
|
|
190
|
+
let error_msg = '',
|
|
191
|
+
doc_to_load = null,
|
|
192
|
+
num_files = files.length,
|
|
193
|
+
file_number = 1;
|
|
194
|
+
upload_canceled = false;
|
|
195
|
+
for(let f in files) {
|
|
196
|
+
if(isNaN(f) || upload_canceled) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
log(files[f]);
|
|
200
|
+
let form_data = new FormData(),
|
|
201
|
+
name = files[f].name,
|
|
202
|
+
percent = Math.ceil(100*file_number/num_files);
|
|
203
|
+
name = name.substring(0, 50);
|
|
204
|
+
|
|
205
|
+
form_data.append('name', name);
|
|
206
|
+
form_data.append('description', document.getElementById('document-add-description').value);
|
|
207
|
+
form_data.append('file', files[f]);
|
|
208
|
+
form_data.append('_xsrf', getCookie('_xsrf'));
|
|
209
|
+
|
|
210
|
+
const fileIndex = parseInt(f);
|
|
211
|
+
const fileSelector = document.querySelector(`#document-files-list select[data-file-index="${fileIndex}"]:not(.text-direction-selector)`);
|
|
212
|
+
const textDirectionSelector = document.querySelector(`#document-files-list .text-direction-selector[data-file-index="${fileIndex}"]`);
|
|
213
|
+
|
|
214
|
+
// Use individual text direction if available, otherwise fall back to global
|
|
215
|
+
const textDirection = textDirectionSelector?.value || document.getElementById('document-add-form').elements['document-add-direction'].value;
|
|
216
|
+
form_data.append('text_direction', textDirection);
|
|
217
|
+
|
|
218
|
+
if (fileSelector) {
|
|
219
|
+
if (fileSelector.dataset.createNew === 'true') {
|
|
220
|
+
const newCollectionName = fileSelector.dataset.newCollectionName || `Collection for ${name}`;
|
|
221
|
+
form_data.append('create_collection_name', newCollectionName);
|
|
222
|
+
} else {
|
|
223
|
+
const selectedCollectionId = fileSelector.value;
|
|
224
|
+
if (selectedCollectionId) {
|
|
225
|
+
form_data.append('collection_ids', selectedCollectionId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
file_number++;
|
|
231
|
+
|
|
232
|
+
await doUpload(form_data, percent).then(
|
|
233
|
+
function(success) {
|
|
234
|
+
updateProgress(success.percent);
|
|
235
|
+
if(doc_to_load === null) {
|
|
236
|
+
doc_to_load = success.doc_to_load;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
function(error) {
|
|
240
|
+
updateProgress(error.percent);
|
|
241
|
+
error_msg += error.error_msg;
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if(error_msg !== '') {
|
|
247
|
+
const uploadFileErrorMessage = gettext("Error uploading file!");
|
|
248
|
+
showAlert(`${uploadFileErrorMessage} \n \n${error_msg}`, 'error')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Load first uploaded document
|
|
252
|
+
if(doc_to_load !== null) {
|
|
253
|
+
$(document_add_modal).modal('hide');
|
|
254
|
+
document.getElementById('document-add-form').reset();
|
|
255
|
+
|
|
256
|
+
cleanupDocumentDispatchInterface();
|
|
257
|
+
|
|
258
|
+
const url = `${base_path}/project/${project_id}/document/${doc_to_load}`;
|
|
259
|
+
window.history.pushState({document_id: doc_to_load}, "Document " + doc_to_load, url);
|
|
260
|
+
updateDocumentsList();
|
|
261
|
+
loadDocument(doc_to_load);
|
|
262
|
+
}
|
|
263
|
+
hideSpinner();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
document.getElementById('document-add-form').addEventListener('submit', function (e) {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
log("Uploading documents...");
|
|
269
|
+
uploadFiles(document.getElementById('document-add-file').files)
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
/*
|
|
274
|
+
* Change document
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
const document_change_modal = document.getElementById('document-change-modal');
|
|
278
|
+
|
|
279
|
+
function editDocument(doc_id) {
|
|
280
|
+
document.getElementById('document-change-form').reset();
|
|
281
|
+
document.getElementById('document-change-id').value = '' + doc_id;
|
|
282
|
+
document.getElementById('document-change-name').value = '' + documents['' + doc_id].name;
|
|
283
|
+
document.getElementById('document-change-description').value = '' + documents['' + doc_id].description;
|
|
284
|
+
document.getElementById('document-change-form').elements['document-change-direction'].value = documents['' + doc_id].text_direction;
|
|
285
|
+
|
|
286
|
+
initDocumentChangeCollectionHierarchySelect(true);
|
|
287
|
+
|
|
288
|
+
const docCollections = documents['' + doc_id].collections || [];
|
|
289
|
+
if (docCollections.length > 0) {
|
|
290
|
+
const firstCollectionId = docCollections[0];
|
|
291
|
+
document.getElementById('document-change-collection-id').value = firstCollectionId;
|
|
292
|
+
const collection = findCollection(firstCollectionId);
|
|
293
|
+
if (collection) {
|
|
294
|
+
document.getElementById('dropdownMenuButtonDocumentChangeCollection').innerText = collection.name;
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
document.getElementById('document-change-collection-id').value = '';
|
|
298
|
+
document.getElementById('dropdownMenuButtonDocumentChangeCollection').innerText = gettext('None');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
$(document_change_modal).modal('show').drags({handle: '.modal-header'});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function doDeleteDocument(project_id, doc_id, successCallback=null, errorCallback=null) {
|
|
305
|
+
deleteURL(`/api/project/${project_id}/document/${doc_id}`)
|
|
306
|
+
.then(successCallback)
|
|
307
|
+
.catch(function (error) {
|
|
308
|
+
if(errorCallback === null) {
|
|
309
|
+
log("Failed to delete document", 'error', {error});
|
|
310
|
+
const deleteDocumentErrorMessage = gettext("Couldn't delete document!!");
|
|
311
|
+
showAlert(`${deleteDocumentErrorMessage} \n \n`, 'error', {error})
|
|
312
|
+
} else {
|
|
313
|
+
errorCallback(error)
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function deleteDocument(doc_id) {
|
|
319
|
+
showConfirm(gettext("Are you sure you want to delete the document '%(doc)s'?", {doc: documents[doc_id].name}), function () {
|
|
320
|
+
log(`document ${doc_id} ...`);
|
|
321
|
+
doDeleteDocument(project_id, doc_id, function () {
|
|
322
|
+
log("Document deletion posted");
|
|
323
|
+
$(document_change_modal).modal('hide');
|
|
324
|
+
document.getElementById('document-change-form').reset();
|
|
325
|
+
|
|
326
|
+
if (document && document.collections) {
|
|
327
|
+
for (let collectionId of document.collections) {
|
|
328
|
+
const collection = findCollection(parseInt(collectionId));
|
|
329
|
+
if (collection && collection.document_ids) {
|
|
330
|
+
const index = collection.document_ids.indexOf(parseInt(doc_id));
|
|
331
|
+
if (index !== -1) {
|
|
332
|
+
collection.document_ids.splice(index, 1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
delete documents['' + doc_id];
|
|
339
|
+
updateDocumentsList();
|
|
340
|
+
initDocumentsTreeview();
|
|
341
|
+
|
|
342
|
+
if (current_document === doc_id) {
|
|
343
|
+
window.history.pushState({}, "Project", base_path + '/project/' + project_id);
|
|
344
|
+
loadDocument(null);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}, true);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
document.getElementById('document-change-form').addEventListener('submit', function (e) {
|
|
351
|
+
e.preventDefault();
|
|
352
|
+
log("Changing document...");
|
|
353
|
+
|
|
354
|
+
const doc_id = document.getElementById('document-change-id').value
|
|
355
|
+
|
|
356
|
+
const updatedDocument = {
|
|
357
|
+
id: doc_id,
|
|
358
|
+
name: document.getElementById('document-change-name').value,
|
|
359
|
+
description: document.getElementById('document-change-description').value,
|
|
360
|
+
text_direction: document.getElementById('document-change-form').elements['document-change-direction'].value
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
if (!updatedDocument.name || updatedDocument.name.length === 0) {
|
|
364
|
+
showAlert(gettext("Document name cannot be empty"));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const selectedCollectionId = document.getElementById('document-change-collection-id').value;
|
|
369
|
+
updatedDocument.collections = selectedCollectionId ? [selectedCollectionId] : [];
|
|
370
|
+
|
|
371
|
+
const callbackSuccess = (response) => {
|
|
372
|
+
log("Document update posted");
|
|
373
|
+
$(document_change_modal).modal('hide');
|
|
374
|
+
document.getElementById('document-change-form').reset();
|
|
375
|
+
loadDocument(current_document);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const callbackFail = (error) => {
|
|
379
|
+
log("Document upload failed:", 'error', {error});
|
|
380
|
+
const uploadFileErrorMessage = gettext("Error uploading file!");
|
|
381
|
+
showAlert(`${uploadFileErrorMessage} \n \n`, 'error', {error})
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
submitDocumentUpdate(updatedDocument, callbackSuccess, callbackFail)
|
|
385
|
+
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
document.getElementById('document-change-delete').addEventListener('click', function (e) {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
const doc_id = parseInt(document.getElementById('document-change-id').value);
|
|
391
|
+
deleteDocument(doc_id);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
/*
|
|
396
|
+
* Tags list
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
let sortTags = ['path', 'asc'];
|
|
400
|
+
let compactLayout = typeof projectCompactLayout !== 'undefined' && projectCompactLayout;
|
|
401
|
+
|
|
402
|
+
function applyCompactLayout(enabled) {
|
|
403
|
+
document.querySelector('.bordered-container').classList.toggle('compact-layout', enabled);
|
|
404
|
+
updateTagsList();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function toggleCompactLayout() {
|
|
408
|
+
compactLayout = document.getElementById('compact-layout-toggle').checked;
|
|
409
|
+
applyCompactLayout(compactLayout);
|
|
410
|
+
// Fire and forget — a failed save just means the toggle won't stick past
|
|
411
|
+
// the next reload, which is harmless enough to skip a spinner over.
|
|
412
|
+
postJSON(
|
|
413
|
+
`/api/project/${project_id}/display`,
|
|
414
|
+
{compact_layout: compactLayout},
|
|
415
|
+
).catch(function (error) {
|
|
416
|
+
console.warn('Failed to persist compact layout:', error);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
$(document).ready(function () {
|
|
421
|
+
if (compactLayout) {
|
|
422
|
+
applyCompactLayout(true);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
function sortTagsBy(field, dir) {
|
|
427
|
+
if (hideCodeCounts && field === 'count') {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
sortTags = [field, dir];
|
|
431
|
+
let options = document.querySelectorAll('#tag-menu-more-actions a.sort-option');
|
|
432
|
+
options.forEach(function (option) {
|
|
433
|
+
if (option.dataset.sortField === field && option.dataset.sortDir === dir) {
|
|
434
|
+
option.classList.add('active');
|
|
435
|
+
} else {
|
|
436
|
+
option.classList.remove('active');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
updateTagsList();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function linkTag(elem, tag_path, selectAll = true) {
|
|
443
|
+
const url = `${base_path}/project/${project_id}/highlights/${encodeURIComponent(tag_path)}`;
|
|
444
|
+
elem.setAttribute('href', url);
|
|
445
|
+
elem.addEventListener('click', function (e) {
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
window.history.pushState({tag_path: tag_path}, "Tag " + tag_path, url);
|
|
448
|
+
loadTag(tag_path);
|
|
449
|
+
selectAllTag();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
linkTag(document.getElementById('load-all-tags'), '');
|
|
454
|
+
|
|
455
|
+
function highlightModalReset() {
|
|
456
|
+
document.getElementById('highlight-add-form').reset();
|
|
457
|
+
highlightSelectedTags.length = 0;
|
|
458
|
+
showHighlightTagSelection();
|
|
459
|
+
hideSearchErrorMessage();
|
|
460
|
+
savedSelections = [];
|
|
461
|
+
initTagsHighlightTreeview();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
const tag_add_modal = document.getElementById('tag-add-modal');
|
|
466
|
+
|
|
467
|
+
$(tag_add_modal).on('shown.bs.modal', function () {
|
|
468
|
+
document.getElementById('tag-add-path').focus();
|
|
469
|
+
});
|
|
470
|
+
let createTagFromHighlight;
|
|
471
|
+
function createTag(fromHighlight, selection, parent) {
|
|
472
|
+
document.getElementById('hlinfo').style.display = 'none';
|
|
473
|
+
createTagFromHighlight = fromHighlight;
|
|
474
|
+
document.getElementById('tag-add-form').reset();
|
|
475
|
+
document.getElementById('tag-add-id').value = '';
|
|
476
|
+
document.getElementById('tag-add-path').value = '';
|
|
477
|
+
document.getElementById('tag-add-color').value = '';
|
|
478
|
+
document.getElementById('tag-highlight-add-start').value = '';
|
|
479
|
+
document.getElementById('tag-highlight-add-end').value = '';
|
|
480
|
+
document.getElementById('tag-add-label-new').style.display = '';
|
|
481
|
+
document.getElementById('tag-add-label-change').style.display = 'none';
|
|
482
|
+
document.getElementById('tag-add-cancel').style.display = '';
|
|
483
|
+
document.getElementById('tag-add-delete').style.display = 'none';
|
|
484
|
+
document.getElementById('tag-add-merge').style.display = 'none';
|
|
485
|
+
if (fromHighlight) {
|
|
486
|
+
document.getElementById('tag-add-path').value = document.getElementById('input-search-tags-highlight').value;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const tagPreview = document.getElementById('tag-selection-preview');
|
|
490
|
+
if (tagPreview) {
|
|
491
|
+
const showPreview = !!(selection || (fromHighlight && current_selection_text));
|
|
492
|
+
tagPreview.textContent = formatSelectionPreview(current_selection_text);
|
|
493
|
+
tagPreview.style.display = showPreview && current_selection_text ? '' : 'none';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if(selection) {
|
|
497
|
+
document.getElementById('tag-highlight-add-start').value = selection[0];
|
|
498
|
+
document.getElementById('tag-highlight-add-end').value = selection[1];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if(parent) {
|
|
502
|
+
document.getElementById('tag-add-parent-id').value = parent;
|
|
503
|
+
document.getElementById('dropdownMenuButtonTag').innerText = findTag(parent).path
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
$(tag_add_modal).modal('show').drags({handle: '.modal-header'});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function editTag(tag_id) {
|
|
510
|
+
const tag = findTag(tag_id);
|
|
511
|
+
document.getElementById('tag-add-form').reset();
|
|
512
|
+
document.getElementById('tag-add-id').value = '' + tag_id;
|
|
513
|
+
if (tag.parent) {
|
|
514
|
+
document.getElementById('tag-add-parent-id').value = tag.parent;
|
|
515
|
+
document.getElementById('dropdownMenuButtonTag').innerText = findTag(tag.parent).path;
|
|
516
|
+
}
|
|
517
|
+
document.getElementById('tag-add-path').value = tag.path;
|
|
518
|
+
document.getElementById('tag-add-color').value = tag.color;
|
|
519
|
+
document.getElementById('tag-add-description').value = tag.description;
|
|
520
|
+
document.getElementById('tag-add-label-new').style.display = 'none';
|
|
521
|
+
document.getElementById('tag-add-label-change').style.display = '';
|
|
522
|
+
document.getElementById('tag-add-cancel').style.display = 'none';
|
|
523
|
+
document.getElementById('tag-add-delete').style.display = '';
|
|
524
|
+
document.getElementById('tag-add-merge').style.display = '';
|
|
525
|
+
$(tag_add_modal).modal('show').drags({handle: '.modal-header'});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Save tag button
|
|
529
|
+
document.getElementById('tag-add-form').addEventListener('submit', function (e) {
|
|
530
|
+
e.preventDefault();
|
|
531
|
+
|
|
532
|
+
let tag_id = document.getElementById('tag-add-id').value
|
|
533
|
+
? parseInt(document.getElementById('tag-add-id').value) : null;
|
|
534
|
+
let tag_path = document.getElementById('tag-add-path').value;
|
|
535
|
+
let tag_parent_id = document.getElementById('tag-add-parent-id').value
|
|
536
|
+
? parseInt(document.getElementById('tag-add-parent-id').value) : null;
|
|
537
|
+
|
|
538
|
+
if (!tag_path) {
|
|
539
|
+
showAlert(gettext("Invalid tag name"));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (tag_id && tag_parent_id && tag_id === tag_parent_id) {
|
|
544
|
+
showAlert(gettext("Impossible to define the same tag as parent"), 'warning');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const new_tag = {
|
|
549
|
+
id: tag_id,
|
|
550
|
+
path: tag_path,
|
|
551
|
+
description: document.getElementById('tag-add-description').value,
|
|
552
|
+
color: document.getElementById('tag-add-color').value,
|
|
553
|
+
parent_id: tag_parent_id
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const callbackSuccess = (reply) => {
|
|
557
|
+
|
|
558
|
+
log("Tag posted");
|
|
559
|
+
|
|
560
|
+
last_added_tag = reply.id;
|
|
561
|
+
|
|
562
|
+
const selection = [
|
|
563
|
+
parseInt(document.getElementById('tag-highlight-add-start').value),
|
|
564
|
+
parseInt(document.getElementById('tag-highlight-add-end').value)
|
|
565
|
+
];
|
|
566
|
+
|
|
567
|
+
// Check if either element of the selection array is NaN
|
|
568
|
+
const containsNaN = selection.some(isNaN);
|
|
569
|
+
|
|
570
|
+
// add highlight if it created from selection
|
|
571
|
+
if(!containsNaN && !tag_id) {
|
|
572
|
+
const highlight = {
|
|
573
|
+
id: undefined, selection, tags: [last_added_tag]
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
setTimeout(function() {
|
|
577
|
+
//call submit
|
|
578
|
+
submitHighLightCreation(highlight);
|
|
579
|
+
}, 60);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
$(tag_add_modal).modal('hide');
|
|
584
|
+
document.getElementById('input-search-tags-highlight').value = '';
|
|
585
|
+
document.getElementById('dropdownMenuButtonTag').innerText = gettext('None');
|
|
586
|
+
|
|
587
|
+
};
|
|
588
|
+
const callbackFail = (error) => {
|
|
589
|
+
log("Failed to create tag", 'error', {error});
|
|
590
|
+
const createTagErrorMessage = gettext("Couldn't create tag!");
|
|
591
|
+
showAlert(`${createTagErrorMessage} \n \n`, 'error', {error})
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// Call submitTagCreation with the new_tag object and callbacks
|
|
595
|
+
submitTagCreation(new_tag, callbackSuccess, callbackFail);
|
|
596
|
+
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Delete tag button
|
|
600
|
+
document.getElementById('tag-add-delete').addEventListener('click', function (e) {
|
|
601
|
+
let tag_id = document.getElementById('tag-add-id').value;
|
|
602
|
+
deleteTag(tag_id);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
function deleteTag(tag_id) {
|
|
606
|
+
if (tag_id) {
|
|
607
|
+
const tag = findTag(tag_id)
|
|
608
|
+
showConfirm(gettext("Are you sure you want to delete the tag '%(tag)s'?", {tag: tag.path}), function () {
|
|
609
|
+
tag_id = parseInt(tag_id);
|
|
610
|
+
log(`Posting tag ${tag_id} deletion`);
|
|
611
|
+
const callbackSuccess = (result) => {
|
|
612
|
+
$(tag_add_modal).modal('hide');
|
|
613
|
+
document.getElementById('tag-add-form').reset();
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const callbackFail = (error) => {
|
|
617
|
+
log("Failed to delete tag", 'error', {error});
|
|
618
|
+
const deleteTagErrorMessage = gettext("Couldn't delete tag!");
|
|
619
|
+
showAlert(`${deleteTagErrorMessage} \n \n`, 'error', {error})
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
submitTagDelete(tag_id, callbackSuccess, callbackFail);
|
|
623
|
+
}, true);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
// Merge tags button in tag edit modal: shows merge modal
|
|
630
|
+
document.getElementById('tag-add-merge').addEventListener('click', function (e) {
|
|
631
|
+
e.preventDefault();
|
|
632
|
+
|
|
633
|
+
let tag_id = document.getElementById('tag-add-id').value;
|
|
634
|
+
if (!tag_id) return;
|
|
635
|
+
mergeTag(tag_id);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
function mergeTag(tag_id) {
|
|
639
|
+
|
|
640
|
+
tag_id = parseInt(tag_id);
|
|
641
|
+
const tag = findTag(tag_id);
|
|
642
|
+
|
|
643
|
+
document.getElementById('tag-merge-form').reset();
|
|
644
|
+
document.getElementById('tag-merge-src-id').value = '' + tag_id;
|
|
645
|
+
document.getElementById('tag-merge-src-name').value = tag.path;
|
|
646
|
+
|
|
647
|
+
document.getElementById('tag-merge-warning').classList.add('d-none');
|
|
648
|
+
document.getElementById('tag-merge-warning-text').textContent = '';
|
|
649
|
+
|
|
650
|
+
$(document.getElementById('tag-merge-modal')).modal('show');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function updateMergeWarning() {
|
|
654
|
+
const warningDiv = document.getElementById('tag-merge-warning');
|
|
655
|
+
const warningText = document.getElementById('tag-merge-warning-text');
|
|
656
|
+
|
|
657
|
+
const srcId = document.getElementById('tag-merge-src-id').value;
|
|
658
|
+
const destId = document.getElementById('tag-merge-dest').value;
|
|
659
|
+
|
|
660
|
+
if (!srcId || !destId) {
|
|
661
|
+
warningDiv.classList.add('d-none');
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const srcTag = findTag(parseInt(srcId));
|
|
666
|
+
const destTag = findTag(parseInt(destId));
|
|
667
|
+
|
|
668
|
+
if (srcTag && srcTag.children && srcTag.children.length > 0 && destTag) {
|
|
669
|
+
const childCount = srcTag.children.length;
|
|
670
|
+
warningText.textContent = gettext("This tag has %(count)s child tag(s) that will be moved to \"%(dest)s\".", {count: childCount, dest: destTag.path});
|
|
671
|
+
warningDiv.classList.remove('d-none');
|
|
672
|
+
} else {
|
|
673
|
+
warningDiv.classList.add('d-none');
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const mergeSelectDropdown = document.getElementById('tag-hierarchy-merge-select');
|
|
678
|
+
if (mergeSelectDropdown) {
|
|
679
|
+
mergeSelectDropdown.addEventListener('click', function(e) {
|
|
680
|
+
if (e.target.classList.contains('dropdown-item')) {
|
|
681
|
+
setTimeout(updateMergeWarning, 50);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Merge modal submit button
|
|
687
|
+
document.getElementById('tag-merge-form').addEventListener('submit', function (e) {
|
|
688
|
+
e.preventDefault();
|
|
689
|
+
|
|
690
|
+
let tag_src = document.getElementById('tag-merge-src-id').value;
|
|
691
|
+
if (!tag_src) return;
|
|
692
|
+
tag_src = parseInt(tag_src);
|
|
693
|
+
|
|
694
|
+
let tag_dest = document.getElementById('tag-merge-dest').value;
|
|
695
|
+
if (!tag_dest) return;
|
|
696
|
+
tag_dest = parseInt(tag_dest);
|
|
697
|
+
|
|
698
|
+
if (tag_src === tag_dest) {
|
|
699
|
+
showAlert(gettext("Couldn't merge tags!"))
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const callbackSuccess = (result) => {
|
|
704
|
+
log("Tag merge posted");
|
|
705
|
+
$(document.getElementById('tag-merge-modal')).modal('hide');
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const callbackFail = (error) => {
|
|
709
|
+
log("Failed to merge tags", 'error', {error});
|
|
710
|
+
showAlert(`${gettext("Couldn't merge tags!")} \n \n`, 'error', {error})
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
submitTagMerge(tag_src, tag_dest, callbackSuccess, callbackFail);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
/*
|
|
718
|
+
* Add highlight
|
|
719
|
+
*/
|
|
720
|
+
|
|
721
|
+
const highlight_add_modal = document.getElementById('highlight-add-modal');
|
|
722
|
+
|
|
723
|
+
// Updates current_selection and visibility of the controls
|
|
724
|
+
function selectionChanged() {
|
|
725
|
+
current_selection = describeSelection();
|
|
726
|
+
if (current_selection) {
|
|
727
|
+
current_selection_text = window.getSelection().toString();
|
|
728
|
+
}
|
|
729
|
+
log('current selection offset : ' + current_selection);
|
|
730
|
+
let hlinfo = document.getElementById('hlinfo');
|
|
731
|
+
if (current_selection !== null) {
|
|
732
|
+
const current_range = window.getSelection().getRangeAt(0);
|
|
733
|
+
if (current_range.toString().match(onlyWhitespace)) {
|
|
734
|
+
hlinfo.style.display = 'none';
|
|
735
|
+
} else if (current_range.endOffset > 0) {
|
|
736
|
+
let last_char_range = document.createRange();
|
|
737
|
+
last_char_range.setStart(current_range.endContainer, current_range.endOffset - 1);
|
|
738
|
+
last_char_range.setEnd(current_range.endContainer, current_range.endOffset);
|
|
739
|
+
let rect = last_char_range.getClientRects().item(0);
|
|
740
|
+
let scrollPos = getScrollPos();
|
|
741
|
+
hlinfo.style.left = ((rect.x || rect.left) + rect.width) + 'px';
|
|
742
|
+
hlinfo.style.top = ((rect.y || rect.top) + rect.height + scrollPos.y + 20) + 'px';
|
|
743
|
+
hlinfo.style.display = 'block';
|
|
744
|
+
} else {
|
|
745
|
+
// We are in a weird situation where the end of the selection is in
|
|
746
|
+
// an empty node. We just don't move the popup, seems to work in practice
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
hlinfo.style.display = 'none';
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
document.addEventListener('selectionchange', selectionChanged);
|
|
754
|
+
|
|
755
|
+
function formatSelectionPreview(text, maxLen = 150) {
|
|
756
|
+
if (!text) return '';
|
|
757
|
+
const trimmed = text.trim();
|
|
758
|
+
const truncated = trimmed.length > maxLen ? trimmed.slice(0, maxLen) + ' [...]' : trimmed;
|
|
759
|
+
return '\u201C' + truncated + '\u201D';
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function createHighlight(selection) {
|
|
763
|
+
document.getElementById('hlinfo').style.display = 'none';
|
|
764
|
+
document.getElementById('highlight-add-id').value = '';
|
|
765
|
+
document.getElementById('highlight-add-start').value = selection[0];
|
|
766
|
+
document.getElementById('highlight-add-end').value = selection[1];
|
|
767
|
+
const hlPreview = document.getElementById('highlight-selection-preview');
|
|
768
|
+
if (hlPreview) {
|
|
769
|
+
hlPreview.textContent = formatSelectionPreview(current_selection_text);
|
|
770
|
+
hlPreview.style.display = current_selection_text ? '' : 'none';
|
|
771
|
+
}
|
|
772
|
+
highlightModalReset();
|
|
773
|
+
$(highlight_add_modal).modal('show').drags({handle: '.modal-header'});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function editHighlight() {
|
|
777
|
+
const selection = describeSelection();
|
|
778
|
+
if(selection) {
|
|
779
|
+
createHighlight(selection);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
highlightModalReset();
|
|
783
|
+
const id = this.getAttribute('data-highlight-id');
|
|
784
|
+
document.getElementById('highlight-add-id').value = id;
|
|
785
|
+
document.getElementById('highlight-add-start').value = highlights[id].start_offset;
|
|
786
|
+
document.getElementById('highlight-add-end').value = highlights[id].end_offset;
|
|
787
|
+
const hlPreview = document.getElementById('highlight-selection-preview');
|
|
788
|
+
if (hlPreview) {
|
|
789
|
+
const _tmp = document.createElement('div');
|
|
790
|
+
_tmp.innerHTML = (highlights[id].snippet || '')
|
|
791
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
792
|
+
.replace(/<\/p>|<\/div>|<\/h[1-6]>|<\/li>/gi, '\n');
|
|
793
|
+
const snippet = _tmp.textContent.trim().replace(/\n{3,}/g, '\n\n') || null;
|
|
794
|
+
hlPreview.textContent = formatSelectionPreview(snippet);
|
|
795
|
+
hlPreview.style.display = snippet ? '' : 'none';
|
|
796
|
+
}
|
|
797
|
+
const hl_tags = highlights['' + id].tags;
|
|
798
|
+
const allNodes = treeview_tags_highlight.treeview('getEnabled');
|
|
799
|
+
for (let i = 0; i < hl_tags.length; ++i) {
|
|
800
|
+
highlightSelectedTags.push(hl_tags[i]);
|
|
801
|
+
const node = allNodes.find((node) => node.id === hl_tags[i]);
|
|
802
|
+
treeview_tags_highlight.treeview('selectNode', node.nodeId);
|
|
803
|
+
}
|
|
804
|
+
showHighlightTagSelection();
|
|
805
|
+
$(highlight_add_modal).modal('show').drags({handle: '.modal-header'});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
$('#highlight-add-form').off().on('submit', (e) => {
|
|
809
|
+
|
|
810
|
+
e.preventDefault();
|
|
811
|
+
|
|
812
|
+
const highlight_id = document.getElementById('highlight-add-id').value;
|
|
813
|
+
const selection = [parseInt(document.getElementById('highlight-add-start').value), parseInt(document.getElementById('highlight-add-end').value)];
|
|
814
|
+
|
|
815
|
+
const selectedTags = highlightSelectedTags;
|
|
816
|
+
|
|
817
|
+
const hl_tags = selectedTags;
|
|
818
|
+
|
|
819
|
+
if (hl_tags.length === 0) return;
|
|
820
|
+
|
|
821
|
+
const highlight = {
|
|
822
|
+
id: highlight_id, selection, tags: hl_tags
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const callbackSuccess = () => {
|
|
826
|
+
log("Highlight posted");
|
|
827
|
+
$(highlight_add_modal).modal('hide');
|
|
828
|
+
highlightModalReset();
|
|
829
|
+
};
|
|
830
|
+
const callbackFail = (error) => {
|
|
831
|
+
log("Failed to create highlight", 'error', {error})
|
|
832
|
+
const createHighlightErrorMessage = gettext("Couldn't create highlight!");
|
|
833
|
+
showAlert(`${createHighlightErrorMessage} \n\n`,'error');
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
//call submit
|
|
837
|
+
submitHighLightCreation(highlight, callbackSuccess, callbackFail);
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
// Delete highlight button
|
|
841
|
+
document.getElementById('highlight-delete').addEventListener('click', function () {
|
|
842
|
+
let highlight_id = document.getElementById('highlight-add-id').value;
|
|
843
|
+
if (highlight_id) {
|
|
844
|
+
highlight_id = parseInt(highlight_id);
|
|
845
|
+
log(`Posting highlight ${highlight_id} deletion`);
|
|
846
|
+
deleteURL(`/api/project/${project_id}/document/${current_document}/highlight/${highlight_id}`)
|
|
847
|
+
.then(function () {
|
|
848
|
+
$(highlight_add_modal).modal('hide');
|
|
849
|
+
highlightModalReset();
|
|
850
|
+
})
|
|
851
|
+
.catch(function (error) {
|
|
852
|
+
log("Failed to delete highlight", 'error', {error});
|
|
853
|
+
const deleteHighlightErrorMessage = gettext("Couldn't delete highlight!");
|
|
854
|
+
showAlert(`${deleteHighlightErrorMessage} \n \n`, 'error', {error})
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
/*
|
|
861
|
+
* Members
|
|
862
|
+
*/
|
|
863
|
+
|
|
864
|
+
function addMember(login, privileges) {
|
|
865
|
+
members[login] = {privileges: privileges};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function removeMember(login) {
|
|
869
|
+
delete members[login];
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const members_modal = document.getElementById('members-modal');
|
|
873
|
+
|
|
874
|
+
function patchMember(login, payload) {
|
|
875
|
+
const body = {};
|
|
876
|
+
body[login] = payload;
|
|
877
|
+
showSpinner();
|
|
878
|
+
return patchJSON('/api/project/' + project_id + '/members', body)
|
|
879
|
+
.finally(hideSpinner);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function _memberRow(login, user, can_edit, is_self) {
|
|
883
|
+
// Check if this user is the only admin
|
|
884
|
+
const adminCount = Object.values(members).filter(m => m.privileges === 'ADMIN').length;
|
|
885
|
+
const is_only_admin = is_self && user.privileges === 'ADMIN' && adminCount === 1;
|
|
886
|
+
|
|
887
|
+
let leaveBtn = '';
|
|
888
|
+
if (is_self && !is_only_admin) {
|
|
889
|
+
leaveBtn = '<button type="button" style="height: 35px" class="btn btn-danger col-md-4 form-group">Leave project</button>';
|
|
890
|
+
} else if (!is_self) {
|
|
891
|
+
leaveBtn = '<button type="button" style="height: 35px" class="btn btn-danger col-md-4 form-group"' + (can_edit ? '' : ' disabled') + '>Remove collaborator</button>';
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let elem = document.createElement('div');
|
|
895
|
+
elem.className = 'row members-item';
|
|
896
|
+
elem.innerHTML = '<div class="col-md-4 text-center">' +
|
|
897
|
+
' <i class="fas fa-user fa-2x"></i>' +
|
|
898
|
+
' <p class="members-item-login">' + login + '</p>' +
|
|
899
|
+
'</div>' +
|
|
900
|
+
'<div class="col-md-4 form-group">' +
|
|
901
|
+
' <select class="form-control"' + (can_edit ? '' : ' disabled') + '>' +
|
|
902
|
+
' <option value="ADMIN">' + gettext("Full permissions") + '</option>' +
|
|
903
|
+
' <option value="MANAGE_DOCS">' + gettext("Can't change collaborators / delete project") + '</option>' +
|
|
904
|
+
' <option value="TAG">' + gettext("View & make changes") + '</option>' +
|
|
905
|
+
' <option value="READ">' + gettext("View only") + '</option>' +
|
|
906
|
+
' </select>' +
|
|
907
|
+
'</div>' +
|
|
908
|
+
leaveBtn +
|
|
909
|
+
'<hr class="custom-hr" />';
|
|
910
|
+
|
|
911
|
+
[].forEach.call(elem.querySelectorAll('option'), function (e) {
|
|
912
|
+
if (e.value === user.privileges) {
|
|
913
|
+
e.selected = true;
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
const select = elem.querySelector('select');
|
|
918
|
+
if (select && can_edit) {
|
|
919
|
+
let previousValue = user.privileges;
|
|
920
|
+
select.addEventListener('change', function () {
|
|
921
|
+
const newPriv = select.value;
|
|
922
|
+
patchMember(login, {privileges: newPriv})
|
|
923
|
+
.then(function () {
|
|
924
|
+
if (members[login]) {
|
|
925
|
+
members[login].privileges = newPriv;
|
|
926
|
+
}
|
|
927
|
+
previousValue = newPriv;
|
|
928
|
+
})
|
|
929
|
+
.catch(function (error) {
|
|
930
|
+
log("Failed to update collaborator privileges", 'error', error);
|
|
931
|
+
showAlert(gettext("Couldn't update collaborator!"), 'error');
|
|
932
|
+
select.value = previousValue;
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const btn = elem.querySelector('button');
|
|
938
|
+
if (btn) {
|
|
939
|
+
btn.addEventListener('click', function () {
|
|
940
|
+
patchMember(login, null)
|
|
941
|
+
.then(function () {
|
|
942
|
+
elem.parentNode.removeChild(elem);
|
|
943
|
+
delete members[login];
|
|
944
|
+
if (is_self) {
|
|
945
|
+
$(members_modal).modal('hide');
|
|
946
|
+
window.location.href = '/';
|
|
947
|
+
}
|
|
948
|
+
})
|
|
949
|
+
.catch(function (error) {
|
|
950
|
+
log("Failed to remove collaborator", 'error', error);
|
|
951
|
+
showAlert(gettext("Couldn't remove collaborator!"), 'error');
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return elem;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function showMembers() {
|
|
960
|
+
document.getElementById('members-add').reset();
|
|
961
|
+
|
|
962
|
+
const can_edit = members[user_login].privileges === 'ADMIN';
|
|
963
|
+
|
|
964
|
+
if (can_edit) {
|
|
965
|
+
document.getElementById('members-add-fields').removeAttribute('disabled');
|
|
966
|
+
} else {
|
|
967
|
+
document.getElementById('members-add-fields').setAttribute('disabled', 1);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
let entries = Object.entries(members);
|
|
971
|
+
sortByKey(entries, function (e) {
|
|
972
|
+
return e[0];
|
|
973
|
+
});
|
|
974
|
+
log("Members:", entries.map(function (e) {
|
|
975
|
+
return e[0] + " (" + e[1].privileges + ")";
|
|
976
|
+
})
|
|
977
|
+
.join(", "));
|
|
978
|
+
|
|
979
|
+
// Empty the list
|
|
980
|
+
let current_members = document.getElementById('members-current');
|
|
981
|
+
current_members.innerHTML = '';
|
|
982
|
+
|
|
983
|
+
// Fill it back up
|
|
984
|
+
for (let i = 0; i < entries.length; ++i) {
|
|
985
|
+
const login = entries[i][0];
|
|
986
|
+
const user = entries[i][1];
|
|
987
|
+
const elem = _memberRow(login, user, can_edit, login === user_login);
|
|
988
|
+
|
|
989
|
+
current_members.appendChild(elem);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
document.getElementById('members-add').addEventListener('submit', function (e) {
|
|
994
|
+
e.preventDefault();
|
|
995
|
+
|
|
996
|
+
const login = document.getElementById('member-add-name').value.toLowerCase();
|
|
997
|
+
if (!login) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const privileges = document.getElementById('member-add-privileges').value;
|
|
1001
|
+
|
|
1002
|
+
if (login in members) {
|
|
1003
|
+
showAlert(gettext("Already a member!"));
|
|
1004
|
+
document.getElementById('members-add').reset();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
postJSON('/api/check_user', {login: login})
|
|
1008
|
+
.then(function (result) {
|
|
1009
|
+
if (!result.exists) {
|
|
1010
|
+
showAlert(gettext("This user doesn't exist!"));
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
return patchMember(login, {privileges: privileges})
|
|
1014
|
+
.then(function () {
|
|
1015
|
+
members[login] = {privileges: privileges};
|
|
1016
|
+
const can_edit = members[user_login].privileges === 'ADMIN';
|
|
1017
|
+
const elem = _memberRow(login, {privileges: privileges}, can_edit, login === user_login);
|
|
1018
|
+
const current_members = document.getElementById('members-current');
|
|
1019
|
+
current_members.insertBefore(elem, current_members.firstChild);
|
|
1020
|
+
document.getElementById('members-add').reset();
|
|
1021
|
+
});
|
|
1022
|
+
})
|
|
1023
|
+
.catch(function (error) {
|
|
1024
|
+
log("Failed to add collaborator", 'error', error);
|
|
1025
|
+
showAlert(gettext("Couldn't add collaborator!"), 'error');
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
document.getElementById('members-current').addEventListener('submit', function (e) {
|
|
1030
|
+
e.preventDefault();
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
/*
|
|
1035
|
+
* Load contents
|
|
1036
|
+
*/
|
|
1037
|
+
|
|
1038
|
+
const document_contents = document.getElementById('document-contents');
|
|
1039
|
+
|
|
1040
|
+
function loadDocument(document_id, highlight_id = null, splitParagraphs = true, search_text = null) {
|
|
1041
|
+
loadDocumentHighlights(document_id, highlight_id, splitParagraphs, search_text);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function disableEditing(element) {
|
|
1045
|
+
element.addEventListener('keydown', function (event) {
|
|
1046
|
+
event.preventDefault();
|
|
1047
|
+
});
|
|
1048
|
+
element.addEventListener('paste', function (event) {
|
|
1049
|
+
event.preventDefault();
|
|
1050
|
+
});
|
|
1051
|
+
element.addEventListener('input', function (event) {
|
|
1052
|
+
event.preventDefault();
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function addDragFunctionality(element) {
|
|
1057
|
+
element.addEventListener('dragstart', function (event) {
|
|
1058
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function scrollToHighlight(highlight_id, search_text=null) {
|
|
1063
|
+
window.setTimeout(function () {
|
|
1064
|
+
let scroll_position = 0,
|
|
1065
|
+
toolbar_height = $("#document-view").prev(".toolbar-container").outerHeight();
|
|
1066
|
+
// Find highlight position and go to it
|
|
1067
|
+
if (highlight_id !== null && !isNaN(highlight_id)) {
|
|
1068
|
+
const hl = highlights['' + highlight_id];
|
|
1069
|
+
const container = document.getElementById('highlight-content');
|
|
1070
|
+
if (hl && container) {
|
|
1071
|
+
const textNodes = collectTextNodes(container);
|
|
1072
|
+
const { fullText, charMap } = buildCharMap(textNodes);
|
|
1073
|
+
const startChar = getCharIndexFromUTF8Index(fullText, hl.start_offset);
|
|
1074
|
+
const endChar = getCharIndexFromUTF8Index(fullText, hl.end_offset);
|
|
1075
|
+
const startEntry = charMap[Math.max(0, startChar)];
|
|
1076
|
+
const endEntry = charMap[Math.min(charMap.length - 1, endChar - 1)];
|
|
1077
|
+
|
|
1078
|
+
if (startEntry && endEntry) {
|
|
1079
|
+
const range = document.createRange();
|
|
1080
|
+
range.setStart(startEntry.node, startEntry.offset);
|
|
1081
|
+
range.setEnd(endEntry.node, endEntry.offset + 1);
|
|
1082
|
+
|
|
1083
|
+
const rects = range.getClientRects();
|
|
1084
|
+
if (rects.length > 0) {
|
|
1085
|
+
scroll_position = rects[0].top + window.scrollY - 25;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Flash only the block elements that contain the highlighted text
|
|
1089
|
+
const startBlock = startEntry.node.parentElement.closest('p, .document-chunk');
|
|
1090
|
+
const endBlock = endEntry.node.parentElement.closest('p, .document-chunk');
|
|
1091
|
+
if (startBlock) startBlock.classList.add('flash-once');
|
|
1092
|
+
if (endBlock && endBlock !== startBlock) endBlock.classList.add('flash-once');
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if(search_text !== null) {
|
|
1098
|
+
search_text = decodeURI(search_text.toLowerCase());
|
|
1099
|
+
let lastIndex = search_text.lastIndexOf('/'),
|
|
1100
|
+
position = parseInt(search_text.substring(lastIndex+1));
|
|
1101
|
+
search_text = search_text.substring(0, lastIndex);
|
|
1102
|
+
log(`Go to ${position+1} occurence of ${search_text}`);
|
|
1103
|
+
|
|
1104
|
+
let elements = $(`*:contains_insensitive("${search_text}")`),
|
|
1105
|
+
count= 0,
|
|
1106
|
+
nb_occurences_in_container = 0,
|
|
1107
|
+
goto = null;
|
|
1108
|
+
elements.each(function() {
|
|
1109
|
+
if($(this)[0].children.length === 0){
|
|
1110
|
+
if($(this).parents('#document-contents').length) {
|
|
1111
|
+
nb_occurences_in_container = $(this).text().toLowerCase().split(search_text).length - 1;
|
|
1112
|
+
if((position >= count) && (position <= (count + nb_occurences_in_container))) {
|
|
1113
|
+
goto = $(this);
|
|
1114
|
+
}
|
|
1115
|
+
count += nb_occurences_in_container;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
if(goto) {
|
|
1120
|
+
scroll_position = goto.offset().top;
|
|
1121
|
+
goto.addClass('flash-once');
|
|
1122
|
+
} else {
|
|
1123
|
+
log('Text container not found..')
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
window.scrollTo({
|
|
1127
|
+
top: scroll_position - toolbar_height*3,
|
|
1128
|
+
behavior: 'instant'
|
|
1129
|
+
});
|
|
1130
|
+
}, 0);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function updateLinksTarget(document_contents) {
|
|
1134
|
+
$("A", document_contents).each(function(){
|
|
1135
|
+
$(this).attr('target', '_blank');
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function updateDocumentExportButton(project_id, document_id) {
|
|
1140
|
+
const items = document.querySelectorAll('#document-export-dropdown-menu a');
|
|
1141
|
+
for (let i = 0; i < items.length; ++i) {
|
|
1142
|
+
const ext = items[i].getAttribute('data-extension');
|
|
1143
|
+
if (items[i].getAttribute('data-document') !== 'false') {
|
|
1144
|
+
items[i].setAttribute('href', `${base_path}/project/${project_id}/export/document/${document_id}.${ext}`);
|
|
1145
|
+
items[i].style.display = '';
|
|
1146
|
+
} else {
|
|
1147
|
+
items[i].style.display = 'none';
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function updateDocumentLinkHighlight(current_document) {
|
|
1153
|
+
let document_links = document.getElementsByClassName('document-link-current');
|
|
1154
|
+
for (let i = document_links.length - 1; i >= 0; --i) {
|
|
1155
|
+
document_links[i].classList.remove('document-link-current');
|
|
1156
|
+
}
|
|
1157
|
+
let document_link = document.getElementById('document-link-' + current_document);
|
|
1158
|
+
if (document_link) {
|
|
1159
|
+
document_link.classList.add('document-link-current');
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Also update treeview highlighting if the function exists
|
|
1163
|
+
if (typeof updateDocumentTreeviewHighlight === 'function') {
|
|
1164
|
+
updateDocumentTreeviewHighlight(current_document);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Collect all text nodes under a DOM element in document order.
|
|
1170
|
+
*/
|
|
1171
|
+
function collectTextNodes(node) {
|
|
1172
|
+
const textNodes = [];
|
|
1173
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
|
|
1174
|
+
let n;
|
|
1175
|
+
while ((n = walker.nextNode())) {
|
|
1176
|
+
textNodes.push(n);
|
|
1177
|
+
}
|
|
1178
|
+
return textNodes;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// showTagUnderlineTooltip, hideTagUnderlineTooltip, applyTagUnderlines, loadTag
|
|
1182
|
+
// moved to highlight/extracts.js
|
|
1183
|
+
|
|
1184
|
+
// Load the document if the URL includes one
|
|
1185
|
+
setTimeout(function () {
|
|
1186
|
+
const _document_url_highlight = new RegExp(`^${escapeRegExp(base_path)}/project/([0-9]+)/document/([0-9]+)(/highlight/([0-9]+))?`);
|
|
1187
|
+
// const _document_url_highlight = new RegExp(`^${escapeRegExp(base_path)}/project/([0-9]+)/document/([0-9]+)(/highlight/([0-9]+))+`),
|
|
1188
|
+
const _document_url_search_text = new RegExp(`^${escapeRegExp(base_path)}/project/([0-9]+)/document/([0-9]+)(/search/([^\/]+/[0-9]+))+`);
|
|
1189
|
+
// Don't use RegExp literals https://github.com/python-babel/babel/issues/640
|
|
1190
|
+
let m = window.location.pathname.match(_document_url_highlight),
|
|
1191
|
+
m2 = window.location.pathname.match(_document_url_search_text);
|
|
1192
|
+
if (m) {
|
|
1193
|
+
let h_id = null;
|
|
1194
|
+
if(typeof m[4] !== 'undefined') {
|
|
1195
|
+
h_id = parseInt(m[4]);
|
|
1196
|
+
}
|
|
1197
|
+
loadDocument(parseInt(m[2]), h_id);
|
|
1198
|
+
// Or a search in document text
|
|
1199
|
+
} else if (m2) {
|
|
1200
|
+
let search_text = null;
|
|
1201
|
+
if(typeof m2[4] !== 'undefined') {
|
|
1202
|
+
search_text = m2[4];
|
|
1203
|
+
}
|
|
1204
|
+
loadDocument(parseInt(m2[2]), null, true, search_text);
|
|
1205
|
+
}
|
|
1206
|
+
// Or a tag
|
|
1207
|
+
const _tag_url = new RegExp(`^${escapeRegExp(base_path)}/project/([0-9]+)/highlights/([^/]*)`);
|
|
1208
|
+
m = window.location.pathname.match(_tag_url);
|
|
1209
|
+
if (m) {
|
|
1210
|
+
loadTag(decodeURIComponent(m[2]));
|
|
1211
|
+
}
|
|
1212
|
+
}, 0,);
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
// Load documents as we go through browser history
|
|
1216
|
+
window.onpopstate = function (e) {
|
|
1217
|
+
if (e.state) {
|
|
1218
|
+
if (e.state.document_id !== undefined) {
|
|
1219
|
+
let highlight_id = (e.state.highlight_id !== undefined ) ? e.state.highlight_id : null,
|
|
1220
|
+
search_text = (e.state.search_text !== undefined ) ? e.state.search_text : null;
|
|
1221
|
+
loadDocument(e.state.document_id, highlight_id, true, search_text);
|
|
1222
|
+
} else if (e.state.tag_path !== undefined) {
|
|
1223
|
+
loadTag(e.state.tag_path);
|
|
1224
|
+
} else {
|
|
1225
|
+
loadDocument(null);
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
loadDocument(null);
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// documents tab filter
|
|
1233
|
+
const DOCUMENT_NOT_FOUND_MESSAGE = gettext("No document or collection found");
|
|
1234
|
+
const alert_document_treeview = $('#alert-title-search');
|
|
1235
|
+
|
|
1236
|
+
function filterTitles() {
|
|
1237
|
+
const filterText = document.getElementById("input-search-titles").value.toLowerCase();
|
|
1238
|
+
alert_document_treeview.addClass('d-none');
|
|
1239
|
+
|
|
1240
|
+
if(filterText === '') {
|
|
1241
|
+
initDocumentsTreeview();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const filteredCollections = handleDocumentSearch(filterText);
|
|
1246
|
+
|
|
1247
|
+
// Check if there are uncategorized documents that match the search
|
|
1248
|
+
let hasUncategorizedMatches = false;
|
|
1249
|
+
for (let [docId, document] of Object.entries(documents)) {
|
|
1250
|
+
const docCollections = document.collections || [];
|
|
1251
|
+
if (docCollections.length === 0 && document.name.toLowerCase().includes(filterText)) {
|
|
1252
|
+
hasUncategorizedMatches = true;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
initDocumentsTreeview(filteredCollections);
|
|
1258
|
+
|
|
1259
|
+
// Only show "no results" message if there are no filtered collections AND no uncategorized matches
|
|
1260
|
+
if (filteredCollections.length === 0 && !hasUncategorizedMatches) {
|
|
1261
|
+
alert_document_treeview.removeClass('d-none');
|
|
1262
|
+
alert_document_treeview.html(`<strong>${DOCUMENT_NOT_FOUND_MESSAGE}</strong>`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
document.getElementById("input-search-titles").addEventListener('input', filterTitles);
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
/*
|
|
1270
|
+
* Attach input change for tag search event
|
|
1271
|
+
*/
|
|
1272
|
+
const TAG_NOT_FOUND_MESSAGE = gettext("No tag found");
|
|
1273
|
+
const alert_tag_treeview = $('#alert-tag-treeview');
|
|
1274
|
+
const alert_tag_highlight_treeview = $('#alert-tag-highlight-treeview');
|
|
1275
|
+
|
|
1276
|
+
document.getElementById('input-search-tags').addEventListener('input', function () {
|
|
1277
|
+
const filterText = $(this).val().toLowerCase();
|
|
1278
|
+
alert_tag_treeview.addClass('d-none')
|
|
1279
|
+
if(filterText === '') {
|
|
1280
|
+
initTagsTreeview();
|
|
1281
|
+
collapseAllNodes(treeview_tags);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const filteredTags = handleSearch(filterText);
|
|
1285
|
+
initTagsTreeview(filteredTags);
|
|
1286
|
+
expandAllNodes(treeview_tags);
|
|
1287
|
+
if (filteredTags.length === 0) {
|
|
1288
|
+
alert_tag_treeview.removeClass('d-none')
|
|
1289
|
+
alert_tag_treeview.html(`<strong>${TAG_NOT_FOUND_MESSAGE}</strong>`)
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
function saveSelectedNodes(treeview) {
|
|
1294
|
+
const selectedNodes = treeview.treeview('getSelected');
|
|
1295
|
+
return selectedNodes.map(node => node.id);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function restoreSelectedNodes(treeview, savedNodeIds) {
|
|
1299
|
+
savedNodeIds.forEach(id => {
|
|
1300
|
+
const nodeId = getNodeIdFromTreeviewByTagId(treeview, id)
|
|
1301
|
+
selectNode(treeview, nodeId);
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
let savedSelections = [];
|
|
1306
|
+
|
|
1307
|
+
document.getElementById('input-search-tags-highlight').addEventListener('input', function () {
|
|
1308
|
+
|
|
1309
|
+
const filterText = $(this).val().toLowerCase();
|
|
1310
|
+
hideSearchErrorMessage();
|
|
1311
|
+
|
|
1312
|
+
// save selected tags
|
|
1313
|
+
const tempSavedSelections = saveSelectedNodes(treeview_tags_highlight);
|
|
1314
|
+
|
|
1315
|
+
tempSavedSelections.forEach(id => {
|
|
1316
|
+
if (!savedSelections.includes(id)) {
|
|
1317
|
+
savedSelections.push(id);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
const filteredTags = handleSearch(filterText);
|
|
1322
|
+
initTagsHighlightTreeview(filteredTags);
|
|
1323
|
+
if (filteredTags.length === 0) {
|
|
1324
|
+
showSearchErrorMessage();
|
|
1325
|
+
alert_tag_highlight_treeview.html(`<strong>${TAG_NOT_FOUND_MESSAGE}</strong>`)
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Restore tags selections
|
|
1329
|
+
restoreSelectedNodes(treeview_tags_highlight, savedSelections);
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
enableTypeToFocusSearch('#left-panel', '#input-search-tags');
|
|
1333
|
+
enableTypeToFocusSearch('#highlight-add-modal', '#input-search-tags-highlight');
|
|
1334
|
+
|
|
1335
|
+
function showSearchErrorMessage() {
|
|
1336
|
+
alert_tag_highlight_treeview.removeClass('d-none');
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function hideSearchErrorMessage() {
|
|
1340
|
+
alert_tag_highlight_treeview.addClass('d-none');
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function updateToggleButtons() {
|
|
1344
|
+
const isDesktop = window.innerWidth > 768;
|
|
1345
|
+
|
|
1346
|
+
const leftButton = document.getElementById('toggle-left-panel');
|
|
1347
|
+
const rightButton = document.getElementById('toggle-right-panel');
|
|
1348
|
+
|
|
1349
|
+
if (isDesktop) {
|
|
1350
|
+
leftButton.querySelector('i').className = 'fas fa-chevron-left';
|
|
1351
|
+
rightButton.querySelector('i').className = 'fas fa-chevron-right';
|
|
1352
|
+
} else {
|
|
1353
|
+
leftButton.querySelector('i').className = 'fas fa-chevron-down';
|
|
1354
|
+
rightButton.querySelector('i').className = 'fas fa-chevron-down';
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
document.getElementById('toggle-left-panel').addEventListener('click', function() {
|
|
1359
|
+
const leftPanel = document.getElementById('left-panel');
|
|
1360
|
+
leftPanel.classList.toggle('reduced-col');
|
|
1361
|
+
if (window.innerWidth > 768) {
|
|
1362
|
+
adjustMiddlePanelWidth();
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
document.getElementById('toggle-right-panel').addEventListener('click', function() {
|
|
1367
|
+
const rightPanel = document.getElementById('right-panel');
|
|
1368
|
+
rightPanel.classList.toggle('reduced-col');
|
|
1369
|
+
if (window.innerWidth > 768) {
|
|
1370
|
+
adjustMiddlePanelWidth();
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
function adjustMiddlePanelWidth() {
|
|
1375
|
+
const leftPanel = document.getElementById('left-panel');
|
|
1376
|
+
const rightPanel = document.getElementById('right-panel');
|
|
1377
|
+
const middlePanel = document.getElementById('middle-panel');
|
|
1378
|
+
|
|
1379
|
+
// Desktop behavior
|
|
1380
|
+
if (window.innerWidth > 768) {
|
|
1381
|
+
let leftWidth = leftPanel.classList.contains('reduced-col') ? '5%' : '20.8333%';
|
|
1382
|
+
let rightWidth = rightPanel.classList.contains('reduced-col') ? '5%' : '20.8333%';
|
|
1383
|
+
let middleWidth = `calc(100% - ${leftWidth} - ${rightWidth})`;
|
|
1384
|
+
middlePanel.style.setProperty('width', middleWidth, 'important');
|
|
1385
|
+
} else {
|
|
1386
|
+
// Mobile behavior
|
|
1387
|
+
middlePanel.style.setProperty('width', '100%', 'important');
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
loadDocument(current_document);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1394
|
+
|
|
1395
|
+
updateToggleButtons();
|
|
1396
|
+
|
|
1397
|
+
const scrollBtn = document.getElementById('scroll-to-top-button');
|
|
1398
|
+
|
|
1399
|
+
window.addEventListener('scroll', function() {
|
|
1400
|
+
if ((window.pageYOffset || document.documentElement.scrollTop) > 300) {
|
|
1401
|
+
scrollBtn.classList.remove('d-none');
|
|
1402
|
+
} else {
|
|
1403
|
+
scrollBtn.classList.add('d-none');
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
scrollBtn.addEventListener('click', function() {
|
|
1408
|
+
window.scrollTo({
|
|
1409
|
+
top: 0,
|
|
1410
|
+
behavior: 'smooth'
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
function debounce(func, wait) {
|
|
1416
|
+
let timeout;
|
|
1417
|
+
return function executedFunction(...args) {
|
|
1418
|
+
const later = () => {
|
|
1419
|
+
clearTimeout(timeout);
|
|
1420
|
+
func(...args);
|
|
1421
|
+
};
|
|
1422
|
+
clearTimeout(timeout);
|
|
1423
|
+
timeout = setTimeout(later, wait);
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
window.addEventListener('resize', debounce(() => {
|
|
1428
|
+
// Issue in test
|
|
1429
|
+
// loadDocument(current_document);
|
|
1430
|
+
}, 250));
|
|
1431
|
+
|
|
1432
|
+
/* Scroll hide/show navbar handled by utilities.js */
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
function editCollection(collectionId) {
|
|
1437
|
+
const collection = findCollection(collectionId);
|
|
1438
|
+
if (!collection) {
|
|
1439
|
+
log('Collection not found:', 'error', {collectionId});
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
document.getElementById('collection-add-form').reset();
|
|
1444
|
+
document.getElementById('collection-add-id').value = '' + collectionId;
|
|
1445
|
+
document.getElementById('collection-add-name').value = collection.name;
|
|
1446
|
+
document.getElementById('collection-add-description').value = collection.description || '';
|
|
1447
|
+
|
|
1448
|
+
initCollectionHierarchySelect();
|
|
1449
|
+
|
|
1450
|
+
if (collection.parent) {
|
|
1451
|
+
document.getElementById('collection-add-parent-id').value = collection.parent;
|
|
1452
|
+
const parentCollection = findCollection(collection.parent);
|
|
1453
|
+
if (parentCollection) {
|
|
1454
|
+
document.getElementById('dropdownMenuButtonCollection').innerText = parentCollection.name;
|
|
1455
|
+
}
|
|
1456
|
+
} else {
|
|
1457
|
+
document.getElementById('collection-add-parent-id').value = '';
|
|
1458
|
+
document.getElementById('dropdownMenuButtonCollection').innerText = gettext('None');
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
populateDocumentsList(collectionId);
|
|
1462
|
+
|
|
1463
|
+
document.getElementById('collection-add-label-new').style.display = 'none';
|
|
1464
|
+
document.getElementById('collection-add-label-change').style.display = '';
|
|
1465
|
+
document.getElementById('collection-add-cancel').style.display = 'none';
|
|
1466
|
+
document.getElementById('collection-add-delete').style.display = '';
|
|
1467
|
+
|
|
1468
|
+
$('#collection-add-modal').modal('show').drags({handle: '.modal-header'});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function deleteCollection(collectionId) {
|
|
1472
|
+
const collection = findCollection(collectionId);
|
|
1473
|
+
if (!collection) {
|
|
1474
|
+
log('Collection not found:', 'error', {collectionId});
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (collection.children && collection.children.length > 0) {
|
|
1479
|
+
showCollectionDeleteOptions(collection);
|
|
1480
|
+
} else {
|
|
1481
|
+
showConfirm(gettext("Are you sure you want to delete the collection '%(name)s'? Its documents will be moved.", {name: collection.name}), function () {
|
|
1482
|
+
deleteURL(`/api/project/${project_id}/collection/${collectionId}`)
|
|
1483
|
+
.then(function() {
|
|
1484
|
+
})
|
|
1485
|
+
.catch(function(error) {
|
|
1486
|
+
log('Error deleting collection:', 'error', {error});
|
|
1487
|
+
showAlert(gettext('Error deleting collection: ') + (error.error || error), 'error');
|
|
1488
|
+
});
|
|
1489
|
+
}, true);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function showCollectionDeleteOptions(collection, successCallback = null) {
|
|
1494
|
+
const message = gettext("The collection '%(name)s' has subcollections. How do you want to proceed?", {name: collection.name});
|
|
1495
|
+
|
|
1496
|
+
const deleteAllLabel = gettext('Delete all');
|
|
1497
|
+
const deleteAllDescription = gettext('Deletes this collection and all its subcollections recursively.');
|
|
1498
|
+
const deleteOnlyLabel = gettext('Delete only');
|
|
1499
|
+
const deleteOnlyDescription = gettext('Deletes only this collection. Subcollections will become root collections (parent removed).');
|
|
1500
|
+
const cancelLabel = gettext('Cancel');
|
|
1501
|
+
|
|
1502
|
+
const legend = `
|
|
1503
|
+
<div class="alert alert-info mt-3 mb-0">
|
|
1504
|
+
<strong>${deleteAllLabel}:</strong> ${deleteAllDescription}<br>
|
|
1505
|
+
<strong>${deleteOnlyLabel}:</strong> ${deleteOnlyDescription}
|
|
1506
|
+
</div>
|
|
1507
|
+
`;
|
|
1508
|
+
|
|
1509
|
+
$('#confirmMessage').html(message + legend);
|
|
1510
|
+
$('#confirmModalLabel').text(gettext('Delete collection'));
|
|
1511
|
+
|
|
1512
|
+
const originalFooter = $('#confirmModal .modal-footer').html();
|
|
1513
|
+
|
|
1514
|
+
const customFooter = `
|
|
1515
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${cancelLabel}</button>
|
|
1516
|
+
<button type="button" class="btn btn-danger" id="deleteAllButton">${deleteAllLabel}</button>
|
|
1517
|
+
<button type="button" class="btn btn-warning" id="deleteOnlyButton">${deleteOnlyLabel}</button>
|
|
1518
|
+
`;
|
|
1519
|
+
|
|
1520
|
+
$('#confirmModal .modal-footer').html(customFooter);
|
|
1521
|
+
$('#confirmModal').modal('show');
|
|
1522
|
+
|
|
1523
|
+
$('#deleteAllButton, #deleteOnlyButton').off('click');
|
|
1524
|
+
|
|
1525
|
+
$('#deleteAllButton').on('click', function() {
|
|
1526
|
+
$('#confirmModal').modal('hide');
|
|
1527
|
+
deleteURL(`/api/project/${project_id}/collection/${collection.id}`, {delete_mode: 'cascade'})
|
|
1528
|
+
.then(function() {
|
|
1529
|
+
log('Collection and children deleted successfully');
|
|
1530
|
+
if (successCallback) {
|
|
1531
|
+
successCallback();
|
|
1532
|
+
}
|
|
1533
|
+
})
|
|
1534
|
+
.catch(function(error) {
|
|
1535
|
+
log('Error deleting collection:', 'error', {error});
|
|
1536
|
+
showAlert(gettext('Error deleting collection: ') + (error.error || error), 'error');
|
|
1537
|
+
});
|
|
1538
|
+
$('#confirmModal .modal-footer').html(originalFooter);
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
$('#deleteOnlyButton').on('click', function() {
|
|
1542
|
+
$('#confirmModal').modal('hide');
|
|
1543
|
+
deleteURL(`/api/project/${project_id}/collection/${collection.id}`, {delete_mode: 'orphan'})
|
|
1544
|
+
.then(function() {
|
|
1545
|
+
log('Collection deleted, children orphaned successfully');
|
|
1546
|
+
if (successCallback) {
|
|
1547
|
+
successCallback();
|
|
1548
|
+
}
|
|
1549
|
+
})
|
|
1550
|
+
.catch(function(error) {
|
|
1551
|
+
log('Error deleting collection:', 'error', {error});
|
|
1552
|
+
showAlert(gettext('Error deleting collection: ') + (error.error || error), 'error');
|
|
1553
|
+
});
|
|
1554
|
+
$('#confirmModal .modal-footer').html(originalFooter);
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
$('#confirmModal').on('hidden.bs.modal', function() {
|
|
1558
|
+
$('#confirmModal .modal-footer').html(originalFooter);
|
|
1559
|
+
$('#confirmModal').off('hidden.bs.modal');
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function removeCollectionParent(collectionId) {
|
|
1564
|
+
const collection = findCollection(collectionId);
|
|
1565
|
+
if (!collection) {
|
|
1566
|
+
log('Collection not found:', 'error', {collectionId});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (!collection.parent) {
|
|
1571
|
+
log('Collection has no parent to remove:', 'warn', {collectionId});
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const parentCollection = findCollection(collection.parent);
|
|
1576
|
+
const parentName = parentCollection ? parentCollection.name : '';
|
|
1577
|
+
|
|
1578
|
+
showConfirm(gettext("Are you sure you want to remove collection '%(name)s' from parent '%(parent)s'?", {name: collection.name, parent: parentName}), function () {
|
|
1579
|
+
const data = {
|
|
1580
|
+
name: collection.name,
|
|
1581
|
+
description: collection.description || '',
|
|
1582
|
+
document_ids: collection.document_ids || [],
|
|
1583
|
+
parent_id: null
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
const url = `/api/project/${project_id}/collection/${collectionId}`;
|
|
1587
|
+
|
|
1588
|
+
postJSON(url, data)
|
|
1589
|
+
.then(function(response) {
|
|
1590
|
+
log('Collection parent removed successfully');
|
|
1591
|
+
})
|
|
1592
|
+
.catch(function(error) {
|
|
1593
|
+
log('Error removing collection parent:', 'error', {error});
|
|
1594
|
+
showAlert(gettext('Error removing collection parent: ') + (error.error || error), 'error');
|
|
1595
|
+
});
|
|
1596
|
+
}, true);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function createChildCollection(parentId) {
|
|
1600
|
+
const parentCollection = findCollection(parentId);
|
|
1601
|
+
if (!parentCollection) {
|
|
1602
|
+
log('Parent collection not found:', 'error', {parentId});
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
initCollectionHierarchySelect();
|
|
1607
|
+
resetCollectionUI();
|
|
1608
|
+
|
|
1609
|
+
document.getElementById('collection-add-form').reset();
|
|
1610
|
+
document.getElementById('collection-add-id').value = '';
|
|
1611
|
+
|
|
1612
|
+
document.getElementById('collection-add-parent-id').value = parentId;
|
|
1613
|
+
document.getElementById('dropdownMenuButtonCollection').innerText = parentCollection.name;
|
|
1614
|
+
|
|
1615
|
+
populateDocumentsList();
|
|
1616
|
+
|
|
1617
|
+
document.getElementById('collection-add-label-new').style.display = '';
|
|
1618
|
+
document.getElementById('collection-add-label-change').style.display = 'none';
|
|
1619
|
+
document.getElementById('collection-add-cancel').style.display = '';
|
|
1620
|
+
document.getElementById('collection-add-delete').style.display = 'none';
|
|
1621
|
+
|
|
1622
|
+
$('#collection-add-modal').modal('show').drags({handle: '.modal-header'});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const collection_add_modal = document.getElementById('collection-add-modal');
|
|
1626
|
+
|
|
1627
|
+
function createCollection() {
|
|
1628
|
+
document.getElementById('collection-add-form').reset();
|
|
1629
|
+
document.getElementById('collection-add-id').value = '';
|
|
1630
|
+
|
|
1631
|
+
initCollectionHierarchySelect();
|
|
1632
|
+
|
|
1633
|
+
populateDocumentsList();
|
|
1634
|
+
|
|
1635
|
+
document.getElementById('collection-add-label-new').style.display = '';
|
|
1636
|
+
document.getElementById('collection-add-label-change').style.display = 'none';
|
|
1637
|
+
document.getElementById('collection-add-cancel').style.display = '';
|
|
1638
|
+
document.getElementById('collection-add-delete').style.display = 'none';
|
|
1639
|
+
|
|
1640
|
+
resetCollectionUI();
|
|
1641
|
+
|
|
1642
|
+
$('#collection-add-modal').modal('show').drags({handle: '.modal-header'});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
$('#collection-add-form').on('submit', function(e) {
|
|
1646
|
+
e.preventDefault();
|
|
1647
|
+
const collectionId = $('#collection-add-id').val();
|
|
1648
|
+
const name = $('#collection-add-name').val().trim();
|
|
1649
|
+
const description = $('#collection-add-description').val().trim();
|
|
1650
|
+
const parentId = $('#collection-add-parent-id').val();
|
|
1651
|
+
|
|
1652
|
+
if (!name) {
|
|
1653
|
+
showAlert(gettext('Collection name is required'), 'error');
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
if (collectionId && parentId && parseInt(collectionId, 10) === parseInt(parentId, 10)) {
|
|
1658
|
+
showAlert(gettext('A collection cannot be its own parent.'), 'error');
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const selectedDocs = Array.from(selectedDocuments);
|
|
1663
|
+
|
|
1664
|
+
const data = {
|
|
1665
|
+
name: name,
|
|
1666
|
+
description: description,
|
|
1667
|
+
document_ids: selectedDocs,
|
|
1668
|
+
parent_id: parentId || null
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
let url;
|
|
1672
|
+
if (collectionId) {
|
|
1673
|
+
url = `/api/project/${project_id}/collection/${collectionId}`;
|
|
1674
|
+
method = 'POST';
|
|
1675
|
+
} else {
|
|
1676
|
+
url = `/api/project/${project_id}/collection/new`;
|
|
1677
|
+
method = 'POST';
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
postJSON(url, data)
|
|
1681
|
+
.then(function(response) {
|
|
1682
|
+
$('#collection-add-modal').modal('hide');
|
|
1683
|
+
$('#collection-add-form')[0].reset();
|
|
1684
|
+
})
|
|
1685
|
+
.catch(function(error) {
|
|
1686
|
+
const errorMsg = collectionId ?
|
|
1687
|
+
gettext('Error updating collection: ') :
|
|
1688
|
+
gettext('Error creating collection: ');
|
|
1689
|
+
showAlert(errorMsg + (error.error || error), 'error');
|
|
1690
|
+
});
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
$('#collection-add-delete').on('click', function(e) {
|
|
1695
|
+
e.preventDefault();
|
|
1696
|
+
const collectionId = $('#collection-add-id').val();
|
|
1697
|
+
if (!collectionId) return;
|
|
1698
|
+
|
|
1699
|
+
const collection = findCollection(parseInt(collectionId));
|
|
1700
|
+
if (!collection) return;
|
|
1701
|
+
|
|
1702
|
+
if (collection.children && collection.children.length > 0) {
|
|
1703
|
+
showCollectionDeleteOptions(collection, function() {
|
|
1704
|
+
$('#collection-add-modal').modal('hide');
|
|
1705
|
+
$('#collection-add-form')[0].reset();
|
|
1706
|
+
showAlert(gettext('Collection deleted!'), 'success');
|
|
1707
|
+
});
|
|
1708
|
+
} else {
|
|
1709
|
+
showConfirm(gettext("Are you sure you want to delete the collection '%(name)s'? Its documents will be moved.", {name: collection.name}), function() {
|
|
1710
|
+
deleteURL(`/api/project/${project_id}/collection/${collectionId}`)
|
|
1711
|
+
.then(function() {
|
|
1712
|
+
$('#collection-add-modal').modal('hide');
|
|
1713
|
+
$('#collection-add-form')[0].reset();
|
|
1714
|
+
showAlert(gettext('Collection deleted!'), 'success');
|
|
1715
|
+
})
|
|
1716
|
+
.catch(function(error) {
|
|
1717
|
+
showAlert(gettext('Error deleting collection: ') + (error.error || error), 'error');
|
|
1718
|
+
});
|
|
1719
|
+
}, true);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
$('#collection-reset-selection').on('click', function(e) {
|
|
1724
|
+
e.preventDefault();
|
|
1725
|
+
if (typeof resetDocumentSelection === 'function') {
|
|
1726
|
+
resetDocumentSelection();
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
function handleDocumentSearch(filterText) {
|
|
1731
|
+
function searchInChildren(item) {
|
|
1732
|
+
const filteredChildren = [];
|
|
1733
|
+
|
|
1734
|
+
item.children.forEach(child => {
|
|
1735
|
+
const childMatches = child.name.toLowerCase().includes(filterText);
|
|
1736
|
+
const childDocumentMatches = searchInDocuments(child);
|
|
1737
|
+
const matchingChildren = searchInChildren(child);
|
|
1738
|
+
|
|
1739
|
+
if (childMatches || childDocumentMatches.length > 0 || matchingChildren.length > 0) {
|
|
1740
|
+
filteredChildren.push({
|
|
1741
|
+
...child,
|
|
1742
|
+
document_ids: childMatches ? child.document_ids : childDocumentMatches.map(doc => doc.document_id),
|
|
1743
|
+
children: matchingChildren
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
return filteredChildren;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function searchInDocuments(item) {
|
|
1752
|
+
const matchingDocuments = [];
|
|
1753
|
+
|
|
1754
|
+
if (item.document_ids && item.document_ids.length > 0) {
|
|
1755
|
+
for (let docId of item.document_ids) {
|
|
1756
|
+
const document = documents[docId.toString()];
|
|
1757
|
+
if (document && document.name.toLowerCase().includes(filterText)) {
|
|
1758
|
+
matchingDocuments.push({
|
|
1759
|
+
id: 'document_' + docId,
|
|
1760
|
+
text: document.name,
|
|
1761
|
+
icon: 'fas fa-file-alt',
|
|
1762
|
+
selectable: false,
|
|
1763
|
+
document_id: parseInt(docId)
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return matchingDocuments;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
return collections
|
|
1773
|
+
.map(item => {
|
|
1774
|
+
const collectionMatches = item.name.toLowerCase().includes(filterText);
|
|
1775
|
+
const documentMatches = searchInDocuments(item);
|
|
1776
|
+
const filteredChildren = searchInChildren(item);
|
|
1777
|
+
|
|
1778
|
+
// Keep the collection if it matches OR if it has matching children OR if it has matching documents
|
|
1779
|
+
if (collectionMatches || documentMatches.length > 0 || filteredChildren.length > 0) {
|
|
1780
|
+
return {
|
|
1781
|
+
...item,
|
|
1782
|
+
document_ids: collectionMatches ? item.document_ids : documentMatches.map(doc => doc.document_id),
|
|
1783
|
+
children: filteredChildren
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
return null;
|
|
1788
|
+
})
|
|
1789
|
+
.filter(item => item !== null);
|
|
1790
|
+
}
|