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.
Files changed (255) hide show
  1. CHANGELOG.md +481 -0
  2. libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
  3. libreqda-1.2.0.dist-info/METADATA +131 -0
  4. libreqda-1.2.0.dist-info/RECORD +255 -0
  5. libreqda-1.2.0.dist-info/WHEEL +4 -0
  6. libreqda-1.2.0.dist-info/entry_points.txt +4 -0
  7. scripts/babelrc +8 -0
  8. scripts/dist/LICENSE.txt +33 -0
  9. scripts/dist/RELEASE.txt +23 -0
  10. scripts/dist/macos/libreqda.icns +0 -0
  11. scripts/dist/macos/macos.spec +43 -0
  12. scripts/dist/macos/plist.patch +10 -0
  13. scripts/dist/macos/taguette.icns +0 -0
  14. scripts/dist/macos/taguette_console_wrapper +4 -0
  15. scripts/dist/macos.sh +32 -0
  16. scripts/dist/pyinstaller_entrypoint.py +19 -0
  17. scripts/dist/windows/windows-installer.iss +51 -0
  18. scripts/dist/windows/windows.spec +39 -0
  19. scripts/edugain/worker_update_edugain_idps.py +89 -0
  20. scripts/edugain/worker_update_edugain_idps.sh +9 -0
  21. scripts/make_password_reset_link.py +56 -0
  22. scripts/make_theme.py +99 -0
  23. scripts/new_db_revision.sh +8 -0
  24. scripts/register_user.py +76 -0
  25. scripts/update_pot.sh +33 -0
  26. scripts/update_translations.sh +17 -0
  27. scripts/validate.py +140 -0
  28. scripts/wipe_and_disable_user.py +90 -0
  29. taguette/__init__.py +13 -0
  30. taguette/access_control.py +145 -0
  31. taguette/convert.py +623 -0
  32. taguette/database/__init__.py +177 -0
  33. taguette/database/base.py +9 -0
  34. taguette/database/copy.py +433 -0
  35. taguette/database/models.py +920 -0
  36. taguette/demodata.py +688 -0
  37. taguette/export.py +500 -0
  38. taguette/extract.py +287 -0
  39. taguette/import_codebook.py +110 -0
  40. taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
  41. taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
  42. taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
  43. taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
  44. taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
  45. taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
  46. taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
  47. taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
  48. taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
  49. taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
  50. taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
  51. taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
  52. taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
  53. taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
  54. taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
  55. taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
  56. taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
  57. taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
  58. taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
  59. taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
  60. taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
  61. taguette/loki_handler.py +107 -0
  62. taguette/main.py +649 -0
  63. taguette/migrations/env.py +72 -0
  64. taguette/migrations/script.py.mako +24 -0
  65. taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
  66. taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
  67. taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
  68. taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
  69. taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
  70. taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
  71. taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
  72. taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
  73. taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
  74. taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
  75. taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
  76. taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
  77. taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
  78. taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
  79. taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
  80. taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
  81. taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
  82. taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
  83. taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
  84. taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
  85. taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
  86. taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
  87. taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
  88. taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
  89. taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
  90. taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
  91. taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
  92. taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
  93. taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
  94. taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
  95. taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
  96. taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
  97. taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
  98. taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
  99. taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
  100. taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
  101. taguette/static/README.md +34 -0
  102. taguette/static/alt_templates/.gitkeep +0 -0
  103. taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
  104. taguette/static/css/custom.css +1696 -0
  105. taguette/static/css/fa-all.min.css +5 -0
  106. taguette/static/css/themed.css +3 -0
  107. taguette/static/css/themed.css.map +25 -0
  108. taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
  109. taguette/static/css/vendor/bootstrap.min.css +6 -0
  110. taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
  111. taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
  112. taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
  113. taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
  114. taguette/static/css/vendor/hierarchy-select.min.css +1 -0
  115. taguette/static/css/vendor/introjs.min.css +2 -0
  116. taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
  117. taguette/static/favicon.ico +0 -0
  118. taguette/static/js/codebook-viz.js +428 -0
  119. taguette/static/js/collection/context-menu.js +180 -0
  120. taguette/static/js/collection/hierarchy-select.js +71 -0
  121. taguette/static/js/collection/manager.js +217 -0
  122. taguette/static/js/document/context-menu.js +92 -0
  123. taguette/static/js/document/custom-select.js +82 -0
  124. taguette/static/js/document/manager.js +943 -0
  125. taguette/static/js/document/treeview.js +593 -0
  126. taguette/static/js/highlight/README.md +18 -0
  127. taguette/static/js/highlight/crud.js +119 -0
  128. taguette/static/js/highlight/extracts.js +497 -0
  129. taguette/static/js/highlight/loader.js +109 -0
  130. taguette/static/js/highlight/renderer.js +405 -0
  131. taguette/static/js/login.js +15 -0
  132. taguette/static/js/permissions.js +153 -0
  133. taguette/static/js/polling-event.js +145 -0
  134. taguette/static/js/project/intro.js +524 -0
  135. taguette/static/js/project/manager.js +0 -0
  136. taguette/static/js/project/search.js +124 -0
  137. taguette/static/js/tag/context-menu.js +133 -0
  138. taguette/static/js/tag/explore-codes.js +400 -0
  139. taguette/static/js/tag/hierarchy-select.js +69 -0
  140. taguette/static/js/tag/manager.js +545 -0
  141. taguette/static/js/tag/treeview.js +613 -0
  142. taguette/static/js/taguette.js +1790 -0
  143. taguette/static/js/utilities.js +714 -0
  144. taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
  145. taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
  146. taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
  147. taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
  148. taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
  149. taguette/static/js/vendor/hierarchy-select.min.js +1 -0
  150. taguette/static/js/vendor/introjs.min.js +17 -0
  151. taguette/static/js/vendor/jquery-3.7.1.js +2 -0
  152. taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
  153. taguette/static/libreqda.ico +0 -0
  154. taguette/static/logo-h.png +0 -0
  155. taguette/static/logo.png +0 -0
  156. taguette/static/logo_blanc.png +0 -0
  157. taguette/static/missing.png +0 -0
  158. taguette/static/robots.txt +2 -0
  159. taguette/static/scss/components/_badge.scss +42 -0
  160. taguette/static/scss/components/_button.scss +169 -0
  161. taguette/static/scss/components/_checkbox.scss +40 -0
  162. taguette/static/scss/components/_context_menu.scss +28 -0
  163. taguette/static/scss/components/_custom_select.scss +150 -0
  164. taguette/static/scss/components/_dropdown.scss +43 -0
  165. taguette/static/scss/components/_form.scss +54 -0
  166. taguette/static/scss/components/_modal.scss +57 -0
  167. taguette/static/scss/components/_project_card.scss +163 -0
  168. taguette/static/scss/components/_tag.scss +308 -0
  169. taguette/static/scss/components/_treeview.scss +76 -0
  170. taguette/static/scss/custom.scss +809 -0
  171. taguette/static/scss/layout/_credits.scss +62 -0
  172. taguette/static/scss/layout/_footer.scss +6 -0
  173. taguette/static/scss/layout/_navigation.scss +165 -0
  174. taguette/static/scss/layout/_sidebar.scss +37 -0
  175. taguette/static/scss/themes/default/_variables.scss +103 -0
  176. taguette/static/scss/themes/default/theme.css +3 -0
  177. taguette/static/scss/themes/default/theme.css.map +26 -0
  178. taguette/static/scss/themes/default/theme.scss +4 -0
  179. taguette/static/webfonts/BadScript-OFL.txt +94 -0
  180. taguette/static/webfonts/BadScript.ttf +0 -0
  181. taguette/static/webfonts/Montserrat-Light.ttf +0 -0
  182. taguette/static/webfonts/Montserrat-OFL.txt +93 -0
  183. taguette/static/webfonts/fa-brands-400.eot +0 -0
  184. taguette/static/webfonts/fa-brands-400.svg +1184 -0
  185. taguette/static/webfonts/fa-brands-400.ttf +0 -0
  186. taguette/static/webfonts/fa-brands-400.woff +0 -0
  187. taguette/static/webfonts/fa-brands-400.woff2 +0 -0
  188. taguette/static/webfonts/fa-regular-400.eot +0 -0
  189. taguette/static/webfonts/fa-regular-400.svg +467 -0
  190. taguette/static/webfonts/fa-regular-400.ttf +0 -0
  191. taguette/static/webfonts/fa-regular-400.woff +0 -0
  192. taguette/static/webfonts/fa-regular-400.woff2 +0 -0
  193. taguette/static/webfonts/fa-solid-900.eot +0 -0
  194. taguette/static/webfonts/fa-solid-900.svg +2618 -0
  195. taguette/static/webfonts/fa-solid-900.ttf +0 -0
  196. taguette/static/webfonts/fa-solid-900.woff +0 -0
  197. taguette/static/webfonts/fa-solid-900.woff2 +0 -0
  198. taguette/templates/alt_templates/.gitkeep +0 -0
  199. taguette/templates/archived_projects.html +50 -0
  200. taguette/templates/auth/account.html +74 -0
  201. taguette/templates/auth/cilogon.login.html +89 -0
  202. taguette/templates/auth/email_reset_password.html +5 -0
  203. taguette/templates/auth/external_account.html +60 -0
  204. taguette/templates/auth/login.html +144 -0
  205. taguette/templates/auth/reset_password.html +27 -0
  206. taguette/templates/auth/token_needed.html +7 -0
  207. taguette/templates/base.html +209 -0
  208. taguette/templates/bug_report.html +158 -0
  209. taguette/templates/cookies_prompt.html +15 -0
  210. taguette/templates/downloads.html +127 -0
  211. taguette/templates/email_bug_report.txt +9 -0
  212. taguette/templates/email_reset_password.txt +5 -0
  213. taguette/templates/error.html +8 -0
  214. taguette/templates/export_codebook.html +20 -0
  215. taguette/templates/export_document.html +20 -0
  216. taguette/templates/export_highlights.html +47 -0
  217. taguette/templates/index.html +347 -0
  218. taguette/templates/maintenance.html +8 -0
  219. taguette/templates/messages.js +74 -0
  220. taguette/templates/modal/change_document.html +68 -0
  221. taguette/templates/modal/create_collection.html +100 -0
  222. taguette/templates/modal/create_document.html +84 -0
  223. taguette/templates/modal/create_highlight.html +38 -0
  224. taguette/templates/modal/create_tag.html +73 -0
  225. taguette/templates/modal/explore_codes.html +58 -0
  226. taguette/templates/modal/manage_collaborators.html +44 -0
  227. taguette/templates/modal/merge_tag.html +49 -0
  228. taguette/templates/new_password.html +32 -0
  229. taguette/templates/partials/alert.html +16 -0
  230. taguette/templates/partials/codebook_viz_modal.html +34 -0
  231. taguette/templates/partials/confirm.html +17 -0
  232. taguette/templates/partials/document_toolbar.html +37 -0
  233. taguette/templates/partials/hlinfo.html +13 -0
  234. taguette/templates/partials/project_card.html +44 -0
  235. taguette/templates/partials/project_card_archived.html +41 -0
  236. taguette/templates/partials/project_sidebar.html +99 -0
  237. taguette/templates/partials/spinner.html +11 -0
  238. taguette/templates/permissions.html +54 -0
  239. taguette/templates/project.html +288 -0
  240. taguette/templates/project_delete.html +17 -0
  241. taguette/templates/project_import.html +122 -0
  242. taguette/templates/project_import_codebook.html +34 -0
  243. taguette/templates/project_import_codebook_confirm.html +49 -0
  244. taguette/templates/project_new.html +31 -0
  245. taguette/templates/tos.html +8 -0
  246. taguette/templates/trans.js +7 -0
  247. taguette/templates/welcome.html +15 -0
  248. taguette/utils.py +177 -0
  249. taguette/validate.py +141 -0
  250. taguette/version_check.py +102 -0
  251. taguette/web/__init__.py +252 -0
  252. taguette/web/api.py +1835 -0
  253. taguette/web/base.py +864 -0
  254. taguette/web/export.py +338 -0
  255. taguette/web/views.py +1618 -0
@@ -0,0 +1,943 @@
1
+
2
+ let current_document = null;
3
+ let current_tag = null;
4
+ let last_added_tag = null;
5
+ const documents_list = document.getElementById('documents-list');
6
+
7
+ function linkDocument(elem, doc_id, highlight_id=null, search_text=null) {
8
+ let url = `${base_path}/project/${project_id}/document/${doc_id}`;
9
+ if(highlight_id !== null && !isNaN(highlight_id)) {
10
+ url += `/highlight/${highlight_id}`;
11
+ }
12
+ if(search_text !== null) {
13
+ url += `/search/${search_text}`;
14
+ }
15
+ elem.setAttribute('href', url);
16
+ elem.addEventListener('click', function (e) {
17
+ if (e.ctrlKey || e.metaKey) {
18
+ return;
19
+ }
20
+ e.preventDefault();
21
+ e.stopPropagation();
22
+ deselectAllDocumentNodes();
23
+ window.history.pushState({document_id: doc_id, highlight_id: highlight_id, search_text: search_text}, "Document " + doc_id, url);
24
+ loadDocument(doc_id, highlight_id, true, search_text);
25
+ if(highlight_id !== null && !isNaN(highlight_id)) {
26
+ const tags_return = $('#tag-selection-return'),
27
+ tags_return_link = $('a', tags_return);
28
+ tags_return_link.show();
29
+ }
30
+ });
31
+ }
32
+
33
+ function updateDocumentsList() {
34
+
35
+ generateDocumentSelectPicker();
36
+
37
+ if(typeof addEventListenerToOption === "function") {
38
+ addEventListenerToOption()
39
+ }
40
+
41
+ // Update documents treeview if the function exists
42
+ if (typeof initDocumentsTreeview === 'function') {
43
+ initDocumentsTreeview();
44
+ }
45
+ }
46
+
47
+ function toggleFavorite(documentId) {
48
+ const favoriteIcon = document.querySelector(`#favorite-icon-${documentId}`);
49
+ const updatedDocument = {
50
+ id: parseInt(documentId),
51
+ favorite: favoriteIcon.classList.contains('far')
52
+ };
53
+
54
+ submitDocumentUpdate(updatedDocument, function(response) {
55
+ updateDocumentsList();
56
+ initDocumentsTreeview();
57
+ }, function(error) {
58
+ log('Error updating document favorite:', 'error', error);
59
+ });
60
+ }
61
+
62
+ function generateDocumentList() {
63
+ // Empty the list
64
+ while (documents_list.firstChild) {
65
+ const first = documents_list.firstChild;
66
+ if (first.classList && first.classList.contains('special-item-button')) {
67
+ break;
68
+ }
69
+ documents_list.removeChild(first);
70
+ }
71
+
72
+ // Fill up the list again
73
+ const before = documents_list.firstChild;
74
+ const entries = Object.entries(documents);
75
+ sortByKey(entries, function (e) {
76
+ return e[1].name;
77
+ });
78
+ for (let i = 0; i < entries.length; ++i) {
79
+ let doc = entries[i][1];
80
+ let elem = document.createElement('li');
81
+ elem.setAttribute('id', 'document-link-' + doc.id);
82
+ elem.className = 'list-group-item document-link';
83
+ if (doc.id === current_document) {
84
+ elem.classList.add('document-link-current');
85
+ }
86
+ let favoriteIconClass = doc.favorite === true ? 'fas fa-star' : 'far fa-star';
87
+ let favoriteTitle = doc.favorite === true ? gettext("Remove from Bookmarks") : gettext("Add to Bookmarks");
88
+ elem.innerHTML = `
89
+ <div class="d-flex justify-content-between align-items-center">
90
+ <a class="document-link-a text-truncate" style="flex: 1; margin-right: 10px;" title="${escapeHtml(doc.name)}">${escapeHtml(doc.name)}</a>
91
+ <div class="d-flex flex-shrink-0">
92
+ <a href="javascript:editDocument(${doc.id});" class="btn btn-primary btn-sm rounded-1 me-2">
93
+ <i class="fas fa-edit" aria-hidden="true"></i>
94
+ </a>
95
+ <a href="javascript:toggleFavorite(${doc.id});" class="favorite-icon" title="${favoriteTitle}">
96
+ <i id="favorite-icon-${doc.id}" class="${favoriteIconClass}" aria-hidden="true"></i>
97
+ </a>
98
+ </div>
99
+ </div>`;
100
+ documents_list.insertBefore(elem, before);
101
+ const links = elem.getElementsByTagName('a');
102
+ linkDocument(links[0], doc.id);
103
+ }
104
+ if (entries.length === 0) {
105
+ let elem = document.createElement('div');
106
+ elem.className = 'list-group-item disabled';
107
+ elem.textContent = gettext("There are no documents in this project yet.");
108
+ documents_list.insertBefore(elem, before);
109
+ }
110
+ log("Documents list updated");
111
+ }
112
+
113
+ function generateDocumentSelectPicker() {
114
+
115
+ const dropdown = document.getElementById('select-dropdown');
116
+
117
+ // Empty the list
118
+ while (dropdown.firstChild) {
119
+ dropdown.removeChild(dropdown.firstChild);
120
+ }
121
+
122
+ const entries = Object.entries(documents);
123
+ sortByKey(entries, function(e) {
124
+ return e[1].name;
125
+ });
126
+
127
+ // keep only favorites
128
+ const favorites = entries.filter(([key, doc]) => doc.favorite === true);
129
+
130
+ if (favorites.length === 0) {
131
+ let hintItem = document.createElement('li');
132
+ hintItem.classList.add('bookmark-hint');
133
+ hintItem.setAttribute('role', 'option');
134
+ const bookmarkHintLabel = gettext('Left-click a document to bookmark it');
135
+ hintItem.innerHTML = `<em>${bookmarkHintLabel}</em>`;
136
+ dropdown.appendChild(hintItem);
137
+ document.querySelector('.custom-select').classList.add('empty');
138
+ } else {
139
+ document.querySelector('.custom-select').classList.remove('empty');
140
+ favorites.forEach(([key, doc]) => {
141
+ let listItem = document.createElement('li');
142
+ listItem.setAttribute('role', 'option');
143
+
144
+ let input = document.createElement('input');
145
+ input.setAttribute('type', 'radio');
146
+ input.setAttribute('id', 'doc-' + doc.id);
147
+ input.setAttribute('name', 'document');
148
+
149
+ let label = document.createElement('label');
150
+ label.setAttribute('for', 'doc-' + doc.id);
151
+ label.innerHTML = `${escapeHtml(doc.name)}`;
152
+
153
+ listItem.appendChild(input);
154
+ listItem.appendChild(label);
155
+
156
+ dropdown.appendChild(listItem);
157
+ });
158
+ }
159
+ }
160
+
161
+ function submitDocumentUpdate(document, callbackSuccess, callbackFail) {
162
+
163
+ const { id } = document;
164
+
165
+ let request;
166
+
167
+ if (id) {
168
+ request = postJSON(`/api/project/${project_id}/document/${id}`, document);
169
+ }
170
+
171
+ showSpinner();
172
+ request.then(function (response) {
173
+ callbackSuccess(response);
174
+ }).catch(function (error) {
175
+ callbackFail(error);
176
+ }).finally(hideSpinner);
177
+ }
178
+
179
+ function addDocument(document) {
180
+ // Save old collections for document updates
181
+ const oldCollections = documents['' + document.id]?.collections || [];
182
+
183
+ documents['' + document.id] = document;
184
+
185
+ // Update collections
186
+ const newCollections = document.collections || [];
187
+
188
+ // Remove from old collections
189
+ for (let collectionId of oldCollections) {
190
+ const collection = findCollection(parseInt(collectionId));
191
+ if (collection && collection.document_ids) {
192
+ const index = collection.document_ids.indexOf(document.id);
193
+ if (index !== -1) {
194
+ collection.document_ids.splice(index, 1);
195
+ }
196
+ }
197
+ }
198
+
199
+ // Add to new collections
200
+ for (let collectionId of newCollections) {
201
+ const collection = findCollection(parseInt(collectionId));
202
+ if (collection) {
203
+ if (!collection.document_ids) {
204
+ collection.document_ids = [];
205
+ }
206
+ if (!collection.document_ids.includes(document.id)) {
207
+ collection.document_ids.push(document.id);
208
+ }
209
+ }
210
+ }
211
+
212
+ updateDocumentsList();
213
+ initDocumentsTreeview(null, function() {
214
+ highlightDocument(document.id);
215
+ });
216
+ }
217
+
218
+ function flattenCollections(collectionsTree) {
219
+ const flat = {};
220
+
221
+ function addToFlat(nodes) {
222
+ for (let node of nodes) {
223
+ flat[node.id] = node;
224
+ if (node.children && node.children.length > 0) {
225
+ addToFlat(node.children);
226
+ }
227
+ }
228
+ }
229
+
230
+ addToFlat(collectionsTree);
231
+ return flat;
232
+ }
233
+
234
+ function buildDocumentPath(documentId) {
235
+ const document = documents['' + documentId];
236
+ if (!document) return null;
237
+
238
+ let path = document.name;
239
+
240
+ if (document.collections && document.collections.length > 0) {
241
+ const collectionId = document.collections[0];
242
+ const collectionPath = buildCollectionPath(collectionId);
243
+ if (collectionPath) {
244
+ path = collectionPath + ' > ' + document.name;
245
+ }
246
+ }
247
+
248
+ return path;
249
+ }
250
+
251
+ function buildCollectionPath(collectionId) {
252
+ const collection = findCollection(collectionId);
253
+ if (!collection) return null;
254
+
255
+ let path = collection.name;
256
+ let currentCollection = collection;
257
+
258
+ while (currentCollection.parent) {
259
+ const parentCollection = findCollection(currentCollection.parent);
260
+ if (parentCollection) {
261
+ path = parentCollection.name + ' > ' + path;
262
+ currentCollection = parentCollection;
263
+ } else {
264
+ break;
265
+ }
266
+ }
267
+
268
+ return path;
269
+ }
270
+
271
+
272
+ // ADD COLLECTION WITH SELECTED DOCUMENTS
273
+ let selectedDocuments = new Set();
274
+ let originalSelectedDocuments = new Set();
275
+
276
+ function resetCollectionUI() {
277
+ selectedDocuments.clear();
278
+ originalSelectedDocuments.clear();
279
+
280
+ const searchInput = document.getElementById('collection-documents-search');
281
+ if (searchInput) {
282
+ searchInput.value = '';
283
+ }
284
+
285
+ updateSelectedDocumentsDisplay();
286
+
287
+ const form = document.getElementById('collection-add-form');
288
+ if (form) {
289
+ form.reset();
290
+ }
291
+ }
292
+
293
+ function populateDocumentsList(collectionId = null) {
294
+ const docListDiv = document.getElementById('collection-add-documents-list');
295
+ docListDiv.innerHTML = '';
296
+
297
+ if (collectionId) {
298
+ const collection = findCollection(parseInt(collectionId));
299
+ if (collection && collection.document_ids) {
300
+ selectedDocuments.clear();
301
+ originalSelectedDocuments.clear();
302
+ collection.document_ids.forEach(docId => {
303
+ selectedDocuments.add(docId);
304
+ originalSelectedDocuments.add(docId);
305
+ });
306
+ }
307
+ } else {
308
+ originalSelectedDocuments.clear();
309
+ }
310
+
311
+ // legend
312
+ const legendWithoutCollection = gettext(' = Documents without collection');
313
+ const legendWithCollection = gettext(' = Documents currently assigned to a collection');
314
+
315
+ const legendDiv = document.createElement('div');
316
+ legendDiv.className = 'mb-3 p-2 rounded';
317
+ legendDiv.innerHTML = `
318
+ <div class="d-flex flex-column align-items-start gap-2">
319
+ <div class="d-flex align-items-center gap-1">
320
+ <i class="fas fa-file-alt text-primary" style="vertical-align: middle;"></i>
321
+ <small>${legendWithoutCollection}</small>
322
+ </div>
323
+ <div class="d-flex align-items-center gap-1">
324
+ <i class="fas fa-folder text-warning" style="vertical-align: middle;"></i>
325
+ <small>${legendWithCollection}</small>
326
+ </div>
327
+ </div>
328
+ `;
329
+ docListDiv.appendChild(legendDiv);
330
+
331
+ const docEntries = Object.entries(documents);
332
+ docEntries.sort((a, b) => {
333
+ const aCollections = a[1].collections || [];
334
+ const bCollections = b[1].collections || [];
335
+
336
+ // Sort: documents without collection first, then documents with collection
337
+ if (aCollections.length === 0 && bCollections.length > 0) return -1;
338
+ if (aCollections.length > 0 && bCollections.length === 0) return 1;
339
+
340
+ // Within same category, sort by name
341
+ return a[1].name.localeCompare(b[1].name);
342
+ });
343
+
344
+ let availableDocuments = 0;
345
+
346
+ for (let i = 0; i < docEntries.length; ++i) {
347
+ let doc = docEntries[i][1];
348
+ const docCollections = doc.collections || [];
349
+
350
+ // Skip selected documents
351
+ if (selectedDocuments.has(doc.id)) {
352
+ continue;
353
+ }
354
+
355
+ // Get current collection name for documents that have collections
356
+ let currentCollectionName = '';
357
+ if (docCollections.length > 0) {
358
+ const currentCollection = findCollection(docCollections[0]);
359
+ currentCollectionName = currentCollection ? currentCollection.name : '';
360
+ }
361
+
362
+ availableDocuments++;
363
+
364
+ let docItem = document.createElement('div');
365
+ docItem.className = 'document-item p-2 mb-1 rounded d-flex align-items-center justify-content-between';
366
+ docItem.dataset.documentId = doc.id;
367
+
368
+ // Create content div
369
+ const contentDiv = document.createElement('div');
370
+ contentDiv.className = 'd-flex align-items-center gap-2 flex-grow-1';
371
+
372
+ // Add icon based on collection status
373
+ const iconDiv = document.createElement('div');
374
+ if (docCollections.length === 0) {
375
+ iconDiv.innerHTML = '<i class="fas fa-file-alt text-primary"></i>';
376
+ } else {
377
+ iconDiv.innerHTML = '<i class="fas fa-folder text-warning"></i>';
378
+ }
379
+
380
+ // Add document name and collection info
381
+ const textDiv = document.createElement('div');
382
+ textDiv.innerHTML = `
383
+ <div class="fw-bold" title="${doc.name}">${doc.name}</div>
384
+ ${currentCollectionName ? `<small class="text-muted d-block">${gettext('Currently in')}: ${currentCollectionName}</small>` : ''}
385
+ `;
386
+
387
+ contentDiv.appendChild(iconDiv);
388
+ contentDiv.appendChild(textDiv);
389
+ docItem.appendChild(contentDiv);
390
+
391
+ docItem.addEventListener('click', function() {
392
+ const docId = Number(this.dataset.documentId);
393
+
394
+ if (selectedDocuments.has(docId)) {
395
+ selectedDocuments.delete(docId);
396
+ } else {
397
+ selectedDocuments.add(docId);
398
+ }
399
+
400
+ // Update both lists
401
+ updateSelectedDocumentsDisplay();
402
+ populateDocumentsList();
403
+ });
404
+
405
+ docListDiv.appendChild(docItem);
406
+ }
407
+
408
+ if (availableDocuments === 0) {
409
+ const noDocsMessage = document.createElement('p');
410
+ noDocsMessage.className = 'text-muted text-center';
411
+ noDocsMessage.textContent = gettext('No documents available');
412
+ docListDiv.appendChild(noDocsMessage);
413
+ }
414
+
415
+ setupCollectionSearch();
416
+
417
+ updateSelectedDocumentsDisplay();
418
+ }
419
+
420
+ function setupCollectionSearch() {
421
+ const searchInput = document.getElementById('collection-documents-search');
422
+ if (searchInput) {
423
+ searchInput.removeEventListener('input', handleCollectionSearch);
424
+ searchInput.addEventListener('input', handleCollectionSearch);
425
+ }
426
+ }
427
+
428
+ function handleCollectionSearch() {
429
+ const searchTerm = this.value.toLowerCase();
430
+ const docListDiv = document.getElementById('collection-add-documents-list');
431
+ const docItems = docListDiv.querySelectorAll('.document-item');
432
+
433
+ docItems.forEach(item => {
434
+ const docId = item.dataset.documentId;
435
+ const doc = documents[docId];
436
+ if (doc) {
437
+ const docName = doc.name.toLowerCase();
438
+ const shouldShow = docName.includes(searchTerm);
439
+
440
+ if (shouldShow) {
441
+ item.style.setProperty('display', 'block', 'important');
442
+ } else {
443
+ item.style.setProperty('display', 'none', 'important');
444
+ }
445
+ }
446
+ });
447
+ }
448
+
449
+ function updateSelectedDocumentsDisplay() {
450
+ const selectedContainer = document.getElementById('collection-selected-documents');
451
+ const selectedList = document.getElementById('collection-selected-documents-list');
452
+ const resetButton = document.getElementById('collection-reset-selection');
453
+
454
+ if (selectedDocuments.size === 0) {
455
+ selectedContainer.style.display = 'none';
456
+ if (resetButton) {
457
+ resetButton.style.display = 'none';
458
+ }
459
+ return;
460
+ }
461
+
462
+ selectedContainer.style.display = 'block';
463
+ if (resetButton) {
464
+ resetButton.style.display = 'inline-block';
465
+ }
466
+ selectedList.innerHTML = '';
467
+
468
+ selectedDocuments.forEach(docId => {
469
+ const doc = documents[docId];
470
+ const docCollections = doc.collections || [];
471
+
472
+ let currentCollectionName = '';
473
+ if (docCollections.length > 0) {
474
+ const currentCollection = findCollection(docCollections[0]);
475
+ currentCollectionName = currentCollection ? currentCollection.name : '';
476
+ }
477
+
478
+ const badge = document.createElement('span');
479
+ badge.className = 'selected-document-badge';
480
+ badge.innerHTML = `
481
+ <div class="d-flex align-items-center gap-1">
482
+ <i class="fas ${docCollections.length === 0 ? 'fa-file-alt text-primary' : 'fa-folder text-warning'}"></i>
483
+ <span title="${doc.name}">${doc.name}</span>
484
+ <span class="remove-btn" data-document-id="${docId}">×</span>
485
+ </div>
486
+ ${currentCollectionName ? `<small class="text-muted d-block">${gettext('Currently in')}: ${currentCollectionName}</small>` : ''}
487
+ `;
488
+
489
+ badge.querySelector('.remove-btn').addEventListener('click', function() {
490
+ const docId = Number(this.dataset.documentId);
491
+ selectedDocuments.delete(docId);
492
+ updateSelectedDocumentsDisplay();
493
+ populateDocumentsList();
494
+ });
495
+
496
+ selectedList.appendChild(badge);
497
+ });
498
+ }
499
+
500
+ function resetDocumentSelection() {
501
+ const collectionId = document.getElementById('collection-add-id')?.value;
502
+
503
+ if (collectionId && originalSelectedDocuments.size > 0) {
504
+ selectedDocuments.clear();
505
+ originalSelectedDocuments.forEach(docId => {
506
+ selectedDocuments.add(docId);
507
+ });
508
+ } else {
509
+ selectedDocuments.clear();
510
+ }
511
+
512
+ updateSelectedDocumentsDisplay();
513
+ populateDocumentsList(collectionId || null);
514
+ }
515
+
516
+ // ADD DOCUMENT WITH DISPATCH INTERFACE
517
+
518
+ function displaySelectedDocuments(files) {
519
+ const container = document.getElementById('document-files-list-container');
520
+ const listDiv = document.getElementById('document-files-list');
521
+ const bulkActionsContainer = document.getElementById('bulk-actions-container');
522
+
523
+ if (!files || files.length === 0) {
524
+ container.style.display = 'none';
525
+ bulkActionsContainer.style.display = 'none';
526
+ return;
527
+ }
528
+
529
+ container.style.display = 'block';
530
+ bulkActionsContainer.style.display = 'block';
531
+ listDiv.innerHTML = '';
532
+
533
+ Array.from(files).forEach((file, index) => {
534
+ const fileItem = document.createElement('div');
535
+ fileItem.className = 'document-file-item d-flex align-items-center justify-content-between p-2 mb-2 border rounded';
536
+ fileItem.dataset.fileIndex = index;
537
+
538
+ const fileName = document.createElement('div');
539
+ fileName.className = 'd-flex align-items-center flex-grow-1';
540
+ fileName.innerHTML = `
541
+ <i class="fas fa-file-alt me-2 text-muted"></i>
542
+ <span class="file-name" title="${file.name}">${file.name}</span>
543
+ <small class="text-muted ms-2">(${formatFileSize(file.size)})</small>
544
+ `;
545
+
546
+ const collectionSelector = document.createElement('div');
547
+ collectionSelector.className = 'collection-selector d-flex align-items-end flex-shrink-0';
548
+ const noCollectionRootLabel = gettext('No collection (root)');
549
+ const createNewCollectionForFileLabel = gettext('Create new collection for this file');
550
+ collectionSelector.innerHTML = `
551
+ <select class="form-select form-select-sm me-2" data-file-index="${index}" style="width: 300px;">
552
+ <option value="">${noCollectionRootLabel}</option>
553
+ </select>
554
+ <button type="button" class="btn btn-outline-primary btn-sm create-new-for-file" data-file-index="${index}" title="${createNewCollectionForFileLabel}" style="width: 32px;">
555
+ <i class="fas fa-plus"></i>
556
+ </button>
557
+ `;
558
+
559
+ fileItem.appendChild(fileName);
560
+ fileItem.appendChild(collectionSelector);
561
+ listDiv.appendChild(fileItem);
562
+
563
+ const select = collectionSelector.querySelector('select');
564
+ select.addEventListener('change', function() {
565
+ if (this.dataset.createNew === 'true') {
566
+ resetSelectorStyle(this);
567
+ }
568
+ });
569
+
570
+ const createNewBtn = collectionSelector.querySelector('.create-new-for-file');
571
+ createNewBtn.addEventListener('click', function() {
572
+ const fileIndex = this.dataset.fileIndex;
573
+ const fileName = file.name;
574
+ showCollectionNameInput(fileIndex, fileName);
575
+ });
576
+ });
577
+
578
+ populateCollectionOptions();
579
+ populateAssignAllSelect();
580
+
581
+ setupBulkActions();
582
+ }
583
+
584
+ function populateAssignAllSelect() {
585
+ const assignAllSelect = document.getElementById('assign-all-collection-select');
586
+ if (!assignAllSelect) return;
587
+
588
+ while (assignAllSelect.children.length > 1) {
589
+ assignAllSelect.removeChild(assignAllSelect.children[1]);
590
+ }
591
+
592
+ if (typeof collections !== 'undefined' && collections.length > 0) {
593
+ function addCollectionOptions(collectionsList, level = 0) {
594
+ collectionsList.forEach(collection => {
595
+ const option = document.createElement('option');
596
+ option.value = collection.id;
597
+
598
+ let displayText = '';
599
+ if (level > 0) {
600
+ displayText = '└─ '.repeat(level) + collection.name;
601
+ } else {
602
+ displayText = '📁 ' + collection.name;
603
+ }
604
+
605
+ option.textContent = displayText;
606
+ option.style.paddingLeft = (level * 15) + 'px';
607
+ assignAllSelect.appendChild(option);
608
+
609
+ if (collection.children && collection.children.length > 0) {
610
+ addCollectionOptions(collection.children, level + 1);
611
+ }
612
+ });
613
+ }
614
+
615
+ addCollectionOptions(collections);
616
+ }
617
+ }
618
+
619
+ function populateCollectionOptions() {
620
+ const selectors = document.querySelectorAll('#document-files-list select');
621
+
622
+ selectors.forEach(select => {
623
+ while (select.children.length > 1) {
624
+ select.removeChild(select.children[1]);
625
+ }
626
+
627
+ if (typeof collections !== 'undefined' && collections.length > 0) {
628
+ function addCollectionOptions(collectionsList, level = 0) {
629
+ collectionsList.forEach(collection => {
630
+ const option = document.createElement('option');
631
+ option.value = collection.id;
632
+
633
+ let displayText = '';
634
+ if (level > 0) {
635
+ displayText = '└─ '.repeat(level) + collection.name;
636
+ } else {
637
+ displayText = '📁 ' + collection.name;
638
+ }
639
+
640
+ option.textContent = displayText;
641
+ option.style.paddingLeft = (level * 15) + 'px';
642
+ select.appendChild(option);
643
+
644
+ if (collection.children && collection.children.length > 0) {
645
+ addCollectionOptions(collection.children, level + 1);
646
+ }
647
+ });
648
+ }
649
+
650
+ addCollectionOptions(collections);
651
+ }
652
+ });
653
+ }
654
+
655
+ function formatFileSize(bytes) {
656
+ if (bytes === 0) return '0 Bytes';
657
+ const k = 1024;
658
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
659
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
660
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
661
+ }
662
+
663
+ function setupBulkActions() {
664
+ const assignAllSelect = document.getElementById('assign-all-collection-select');
665
+ if (assignAllSelect) {
666
+ assignAllSelect.addEventListener('change', function() {
667
+ const selectedValue = this.value;
668
+ const collectionSelectors = document.querySelectorAll('#document-files-list select');
669
+
670
+ collectionSelectors.forEach(select => {
671
+ select.value = selectedValue;
672
+ });
673
+ });
674
+ }
675
+
676
+ const resetButton = document.getElementById('reset-all-collections');
677
+ if (resetButton) {
678
+ resetButton.addEventListener('click', function() {
679
+ assignAllSelect.value = '';
680
+
681
+ const collectionSelectors = document.querySelectorAll('#document-files-list select');
682
+ collectionSelectors.forEach(select => {
683
+ select.value = '';
684
+ resetSelectorStyle(select);
685
+ });
686
+ });
687
+ }
688
+
689
+ document.getElementById('create-new-collection-for-all').addEventListener('click', function() {
690
+ showGlobalCollectionNameInput();
691
+ });
692
+ }
693
+
694
+ function resetSelectorStyle(select) {
695
+ select.style.backgroundColor = '';
696
+ select.style.fontStyle = '';
697
+ delete select.dataset.createNew;
698
+ delete select.dataset.newCollectionName;
699
+
700
+ const parentDiv = select.closest('.collection-selector');
701
+ const indicator = parentDiv.querySelector('.new-collection-indicator');
702
+ if (indicator) {
703
+ indicator.remove();
704
+ }
705
+ const inputContainer = parentDiv.querySelector('.collection-name-input-container');
706
+ if (inputContainer) {
707
+ inputContainer.remove();
708
+ }
709
+ }
710
+
711
+ function showCollectionNameInput(fileIndex, fileName) {
712
+ const fileItem = document.querySelector(`[data-file-index="${fileIndex}"]`);
713
+ if (!fileItem) return;
714
+
715
+ const existingInput = fileItem.querySelector('.collection-name-input-container');
716
+ if (existingInput) {
717
+ existingInput.remove();
718
+ }
719
+
720
+ const inputContainer = document.createElement('div');
721
+ inputContainer.className = 'collection-name-input-container mt-2 p-3 border rounded bg-light';
722
+ const newCollectionNameLabel = gettext('New collection name:');
723
+ const confirmLabel = gettext('Confirm');
724
+ const createLabel = gettext('Create');
725
+ const cancelLabel = gettext('Cancel');
726
+ inputContainer.innerHTML = `
727
+ <div class="d-flex align-items-center gap-2 mb-2">
728
+ <i class="fas fa-plus-circle text-primary"></i>
729
+ <label class="form-label mb-0 fw-bold text-primary">${newCollectionNameLabel}</label>
730
+ </div>
731
+ <div class="d-flex gap-2">
732
+ <input type="text" class="form-control collection-name-input"
733
+ placeholder="Collection for ${fileName}" value="Collection for ${fileName}">
734
+ <button type="button" class="btn btn-success confirm-collection-name" title="${confirmLabel}">
735
+ <i class="fas fa-check me-1"></i>${createLabel}
736
+ </button>
737
+ <button type="button" class="btn btn-outline-secondary cancel-collection-name" title="${cancelLabel}">
738
+ <i class="fas fa-times me-1"></i>${cancelLabel}
739
+ </button>
740
+ </div>
741
+ `;
742
+
743
+ fileItem.insertAdjacentElement('afterend', inputContainer);
744
+
745
+ const input = inputContainer.querySelector('.collection-name-input');
746
+ input.focus();
747
+ input.select();
748
+
749
+ const confirmBtn = inputContainer.querySelector('.confirm-collection-name');
750
+ const cancelBtn = inputContainer.querySelector('.cancel-collection-name');
751
+
752
+ const confirmAction = () => {
753
+ const collectionName = input.value.trim();
754
+ if (collectionName) {
755
+ applyNewCollectionToFile(fileIndex, collectionName);
756
+ }
757
+ inputContainer.remove();
758
+ };
759
+
760
+ const cancelAction = () => {
761
+ inputContainer.remove();
762
+ };
763
+
764
+ confirmBtn.addEventListener('click', confirmAction);
765
+ cancelBtn.addEventListener('click', cancelAction);
766
+
767
+ // Enter pour confirmer, Escape pour annuler
768
+ input.addEventListener('keydown', function(e) {
769
+ if (e.key === 'Enter') {
770
+ e.preventDefault();
771
+ confirmAction();
772
+ } else if (e.key === 'Escape') {
773
+ e.preventDefault();
774
+ cancelAction();
775
+ }
776
+ });
777
+ }
778
+
779
+ function applyNewCollectionToFile(fileIndex, collectionName) {
780
+ const targetSelect = document.querySelector(`#document-files-list select[data-file-index="${fileIndex}"]`);
781
+ if (targetSelect) {
782
+ resetSelectorStyle(targetSelect);
783
+
784
+ targetSelect.dataset.newCollectionName = collectionName;
785
+ targetSelect.dataset.createNew = 'true';
786
+ targetSelect.value = '';
787
+
788
+ targetSelect.style.backgroundColor = '#e3f2fd';
789
+ targetSelect.style.fontStyle = 'italic';
790
+
791
+ const parentDiv = targetSelect.closest('.collection-selector');
792
+ let indicator = parentDiv.querySelector('.new-collection-indicator');
793
+ if (!indicator) {
794
+ indicator = document.createElement('small');
795
+ indicator.className = 'new-collection-indicator text-primary mt-1';
796
+ indicator.style.display = 'block';
797
+ indicator.style.position = 'absolute';
798
+ indicator.style.fontSize = '0.75rem';
799
+ indicator.style.marginTop = '2px';
800
+ parentDiv.style.position = 'relative';
801
+ parentDiv.appendChild(indicator);
802
+ }
803
+ indicator.textContent = `→ ${collectionName}`;
804
+ }
805
+ }
806
+
807
+ function showGlobalCollectionNameInput() {
808
+ const bulkActionsContainer = document.getElementById('bulk-actions-container');
809
+ if (!bulkActionsContainer) return;
810
+
811
+ const existingInput = bulkActionsContainer.querySelector('.global-collection-input-container');
812
+ if (existingInput) {
813
+ existingInput.remove();
814
+ }
815
+
816
+ const inputContainer = document.createElement('div');
817
+ inputContainer.className = 'global-collection-input-container mt-3 p-3 border rounded bg-primary bg-opacity-10';
818
+ const createNewCollectionForAllFilesLabel = gettext('Create new collection for all files:');
819
+ const collectionNameLabel = gettext('Collection name');
820
+ const applyToAllLabel = gettext('Apply to all');
821
+ const applyLabel = gettext('Apply');
822
+ const cancelGlobalLabel = gettext('Cancel');
823
+ inputContainer.innerHTML = `
824
+ <div class="d-flex align-items-center gap-2 mb-2">
825
+ <i class="fas fa-layer-group text-primary"></i>
826
+ <label class="form-label mb-0 fw-bold text-primary">${createNewCollectionForAllFilesLabel}</label>
827
+ </div>
828
+ <div class="d-flex gap-2">
829
+ <input type="text" class="form-control form-control-sm global-collection-name-input"
830
+ placeholder="${collectionNameLabel}" value="New Collection">
831
+ <button type="button" class="btn btn-success btn-sm confirm-global-collection" title="${applyToAllLabel}">
832
+ <i class="fas fa-check"></i> ${applyLabel}
833
+ </button>
834
+ <button type="button" class="btn btn-secondary btn-sm cancel-global-collection" title="${cancelGlobalLabel}">
835
+ <i class="fas fa-times"></i>
836
+ </button>
837
+ </div>
838
+ `;
839
+
840
+ bulkActionsContainer.appendChild(inputContainer);
841
+
842
+ const input = inputContainer.querySelector('.global-collection-name-input');
843
+ input.focus();
844
+ input.select();
845
+
846
+ const confirmBtn = inputContainer.querySelector('.confirm-global-collection');
847
+ const cancelBtn = inputContainer.querySelector('.cancel-global-collection');
848
+
849
+ const confirmAction = () => {
850
+ const collectionName = input.value.trim();
851
+ if (collectionName) {
852
+ applyNewCollectionToAllFiles(collectionName);
853
+ }
854
+ inputContainer.remove();
855
+ };
856
+
857
+ const cancelAction = () => {
858
+ inputContainer.remove();
859
+ };
860
+
861
+ confirmBtn.addEventListener('click', confirmAction);
862
+ cancelBtn.addEventListener('click', cancelAction);
863
+
864
+
865
+ input.addEventListener('keydown', function(e) {
866
+ if (e.key === 'Enter') {
867
+ e.preventDefault();
868
+ confirmAction();
869
+ } else if (e.key === 'Escape') {
870
+ e.preventDefault();
871
+ cancelAction();
872
+ }
873
+ });
874
+ }
875
+
876
+ function applyNewCollectionToAllFiles(collectionName) {
877
+ const selectors = document.querySelectorAll('#document-files-list select:not(.text-direction-selector)');
878
+ selectors.forEach(select => {
879
+ resetSelectorStyle(select);
880
+
881
+ select.dataset.newCollectionName = collectionName;
882
+ select.dataset.createNew = 'true';
883
+ select.value = '';
884
+
885
+ select.style.backgroundColor = '#e3f2fd';
886
+ select.style.fontStyle = 'italic';
887
+
888
+ const parentDiv = select.closest('.collection-selector');
889
+ let indicator = parentDiv.querySelector('.new-collection-indicator');
890
+ if (!indicator) {
891
+ indicator = document.createElement('small');
892
+ indicator.className = 'new-collection-indicator text-primary mt-1';
893
+ indicator.style.display = 'block';
894
+ indicator.style.position = 'absolute';
895
+ indicator.style.fontSize = '0.75rem';
896
+ indicator.style.marginTop = '2px';
897
+ parentDiv.style.position = 'relative';
898
+ parentDiv.appendChild(indicator);
899
+ }
900
+ indicator.textContent = `→ ${collectionName}`;
901
+ });
902
+ }
903
+
904
+ function cleanupDocumentDispatchInterface() {
905
+ const container = document.getElementById('document-files-list-container');
906
+ const bulkActionsContainer = document.getElementById('bulk-actions-container');
907
+
908
+ if (container) {
909
+ container.style.display = 'none';
910
+ }
911
+ if (bulkActionsContainer) {
912
+ bulkActionsContainer.style.display = 'none';
913
+ }
914
+
915
+ const listDiv = document.getElementById('document-files-list');
916
+ if (listDiv) {
917
+ listDiv.innerHTML = '';
918
+ }
919
+
920
+ const globalInput = document.querySelector('.global-collection-input-container');
921
+ if (globalInput) {
922
+ globalInput.remove();
923
+ }
924
+
925
+ }
926
+
927
+ document.addEventListener('DOMContentLoaded', function() {
928
+ const fileInput = document.getElementById('document-add-file');
929
+ if (fileInput) {
930
+ fileInput.addEventListener('change', function(e) {
931
+ displaySelectedDocuments(e.target.files);
932
+ });
933
+ }
934
+
935
+ const modal = document.getElementById('document-add-modal');
936
+ if (modal) {
937
+ $(modal).on('hidden.bs.modal', function() {
938
+ cleanupDocumentDispatchInterface();
939
+ });
940
+ }
941
+ });
942
+
943
+ updateDocumentsList();