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,71 @@
1
+ const collection_hierarchy_select = $('#collection-hierarchy-select');
2
+ const document_collection_hierarchy_select = $('#document-collection-hierarchy-select');
3
+ const hs_menu_inner_collection = $('.hs-menu-inner');
4
+ const hs_menu_inner_document = $('.hs-menu-inner');
5
+
6
+ const COLLECTION_DEFAULT_LEVEL = 1;
7
+
8
+ // Creates a default 'None' option for collections
9
+ function addDefaultValueCollection(parentContainer) {
10
+ const li = $("<li>");
11
+ const a = $("<a>").addClass("dropdown-item")
12
+ .attr("data-value", '')
13
+ .attr("data-default-selected", '')
14
+ .attr("data-level", COLLECTION_DEFAULT_LEVEL)
15
+ .text(gettext('None'));
16
+ li.append(a);
17
+
18
+ parentContainer.append(li);
19
+ }
20
+
21
+ // Recursively generates items from collection hierarchy data
22
+ function generateCollectionHierarchyItem(data, parentContainer, level) {
23
+ $.each(data, function (index, item) {
24
+ if (!item) return;
25
+
26
+ const li = $("<li>");
27
+ const a = $("<a>").addClass("dropdown-item")
28
+ .attr("data-value", item.id)
29
+ .attr("data-level", level)
30
+ .attr("href", "#")
31
+ .text(item.name);
32
+
33
+ li.append(a);
34
+ parentContainer.append(li);
35
+
36
+ if (item.children && item.children.length > 0) {
37
+ generateCollectionHierarchyItem(item.children, parentContainer, level + COLLECTION_DEFAULT_LEVEL);
38
+ }
39
+ });
40
+ }
41
+
42
+ // Init collection hierarchy select dropdown for create collection
43
+ function initCollectionHierarchySelect() {
44
+ initHierarchySelectCollection(hs_menu_inner_collection, collection_hierarchy_select, true);
45
+ }
46
+
47
+ // Sets up a hierarchy select dropdown for collections
48
+ function initHierarchySelectCollection(menu, select, withDefault = true) {
49
+ menu.html('');
50
+
51
+ if(withDefault) {
52
+ addDefaultValueCollection(menu);
53
+ }
54
+
55
+ generateCollectionHierarchyItem(collections, menu, COLLECTION_DEFAULT_LEVEL);
56
+
57
+ select.hierarchySelect({
58
+ width: 'auto'
59
+ });
60
+ }
61
+
62
+
63
+ function initDocumentCollectionHierarchySelect(initialValueSet = false) {
64
+ initHierarchySelectCollection(hs_menu_inner_document, document_collection_hierarchy_select, true, initialValueSet);
65
+ }
66
+
67
+ // Init document change collection hierarchy select dropdown
68
+ function initDocumentChangeCollectionHierarchySelect(initialValueSet = false) {
69
+ const document_change_collection_hierarchy_select = $('#document-change-collection-hierarchy-select');
70
+ initHierarchySelectCollection(hs_menu_inner_document, document_change_collection_hierarchy_select, true, initialValueSet);
71
+ }
@@ -0,0 +1,217 @@
1
+ // Collection management: add, update, remove
2
+
3
+ function addCollection(collectionData) {
4
+ // Check if collection already exists to prevent duplicates
5
+ const existingCollection = findCollection(parseInt(collectionData.id));
6
+ if (existingCollection) {
7
+ // Update existing collection instead of creating duplicate
8
+ existingCollection.name = collectionData.name;
9
+ existingCollection.description = collectionData.description;
10
+ existingCollection.document_ids = collectionData.document_ids;
11
+ initDocumentsTreeview();
12
+ return;
13
+ }
14
+
15
+ const newCollection = {
16
+ id: collectionData.id,
17
+ name: collectionData.name,
18
+ description: collectionData.description,
19
+ parent: collectionData.parent_id,
20
+ document_ids: collectionData.document_ids,
21
+ children: []
22
+ };
23
+
24
+ if (!collections) {
25
+ collections = [];
26
+ }
27
+
28
+ // Check if collection has parent
29
+ if (collectionData.parent_id) {
30
+ const parentCollection = findCollection(parseInt(collectionData.parent_id));
31
+ if (parentCollection) {
32
+ if (!parentCollection.children) {
33
+ parentCollection.children = [];
34
+ }
35
+ parentCollection.children.push(newCollection);
36
+ } else {
37
+ // Parent not found, add to root level
38
+ collections.push(newCollection);
39
+ }
40
+ } else {
41
+ // Root level collection
42
+ collections.push(newCollection);
43
+ }
44
+
45
+ // Update document collections
46
+ if (collectionData.document_ids && collectionData.document_ids.length > 0) {
47
+ for (let docId of collectionData.document_ids) {
48
+ const document = documents[docId.toString()];
49
+ if (document) {
50
+ // Remove document from all existing collections first
51
+ if (document.collections && document.collections.length > 0) {
52
+ for (let existingCollectionId of document.collections) {
53
+ const existingCollection = findCollection(existingCollectionId);
54
+ if (existingCollection && existingCollection.document_ids) {
55
+ const docIndex = existingCollection.document_ids.indexOf(parseInt(docId));
56
+ if (docIndex !== -1) {
57
+ existingCollection.document_ids.splice(docIndex, 1);
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ // Add document to new collection
64
+ if (!document.collections) {
65
+ document.collections = [];
66
+ }
67
+ if (!document.collections.includes(collectionData.id)) {
68
+ document.collections.push(collectionData.id);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ initDocumentsTreeview(null, function() {
75
+ highlightCollection(collectionData.id);
76
+ });
77
+ }
78
+
79
+ function updateCollection(collectionData) {
80
+ // Find existing collection in hierarchy
81
+ let existingCollection = findCollection(parseInt(collectionData.id));
82
+ if (!existingCollection) {
83
+ addCollection(collectionData);
84
+ return;
85
+ }
86
+
87
+ const oldDocumentIds = existingCollection.document_ids || [];
88
+ const oldParentId = existingCollection.parent ? parseInt(existingCollection.parent) : null;
89
+
90
+ // Update basic properties
91
+ existingCollection.name = collectionData.name;
92
+ existingCollection.description = collectionData.description;
93
+ existingCollection.document_ids = collectionData.document_ids;
94
+
95
+ // Handle parent change if necessary
96
+ const newParentId = collectionData.parent_id ? parseInt(collectionData.parent_id) : null;
97
+ const currentParentId = existingCollection.parent ? parseInt(existingCollection.parent) : null;
98
+ if (currentParentId !== newParentId) {
99
+ existingCollection.parent = newParentId;
100
+
101
+ // Remove from old parent
102
+ if (oldParentId) {
103
+ const oldParent = findCollection(oldParentId);
104
+ if (oldParent && oldParent.children) {
105
+ const index = oldParent.children.findIndex(c => c.id === parseInt(collectionData.id));
106
+ if (index !== -1) {
107
+ oldParent.children.splice(index, 1);
108
+ }
109
+ }
110
+ } else {
111
+ // Remove from root list
112
+ const index = collections.findIndex(c => c.id === parseInt(collectionData.id));
113
+ if (index !== -1) {
114
+ collections.splice(index, 1);
115
+ }
116
+ }
117
+
118
+ // Add to new parent
119
+ if (collectionData.parent_id) {
120
+ const newParent = findCollection(parseInt(collectionData.parent_id));
121
+ if (newParent) {
122
+ if (!newParent.children) {
123
+ newParent.children = [];
124
+ }
125
+ newParent.children.push(existingCollection);
126
+ } else {
127
+ // New parent not found, add to root level
128
+ const isAlreadyInRoot = collections.some(col => col.id === existingCollection.id);
129
+ if (!isAlreadyInRoot) {
130
+ collections.push(existingCollection);
131
+ }
132
+ }
133
+ } else {
134
+ // Add to root level
135
+ const isAlreadyInRoot = collections.some(col => col.id === existingCollection.id);
136
+ if (!isAlreadyInRoot) {
137
+ collections.push(existingCollection);
138
+ }
139
+ }
140
+ }
141
+
142
+ // Update document collections
143
+ // Remove from old documents
144
+ for (let docId of oldDocumentIds) {
145
+ const document = documents[docId.toString()];
146
+ if (document && document.collections) {
147
+ const index = document.collections.indexOf(parseInt(collectionData.id));
148
+ if (index !== -1) {
149
+ document.collections.splice(index, 1);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Add to new documents
155
+ if (collectionData.document_ids && collectionData.document_ids.length > 0) {
156
+ for (let docId of collectionData.document_ids) {
157
+ const document = documents[docId.toString()];
158
+ if (document) {
159
+ if (!document.collections) {
160
+ document.collections = [];
161
+ }
162
+ if (!document.collections.includes(parseInt(collectionData.id))) {
163
+ document.collections.push(parseInt(collectionData.id));
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ initDocumentsTreeview(null, function() {
170
+ highlightCollection(collectionData.id);
171
+ });
172
+ }
173
+
174
+ function removeCollection(collectionId) {
175
+ // Find collection in hierarchysss
176
+ const collectionToRemove = findCollection(parseInt(collectionId));
177
+ if (!collectionToRemove) {
178
+ return;
179
+ }
180
+
181
+ // Remove from parent or root list
182
+ if (collectionToRemove.parent) {
183
+ const parent = findCollection(parseInt(collectionToRemove.parent));
184
+ if (parent && parent.children) {
185
+ const index = parent.children.findIndex(c => c.id === parseInt(collectionId));
186
+ if (index !== -1) {
187
+ parent.children.splice(index, 1);
188
+ }
189
+ }
190
+ } else {
191
+ // Remove from root list
192
+ const index = collections.findIndex(c => c.id === parseInt(collectionId));
193
+ if (index !== -1) {
194
+ collections.splice(index, 1);
195
+ }
196
+ }
197
+
198
+ // Remove from documents that belong to it
199
+ if (collectionToRemove.document_ids && collectionToRemove.document_ids.length > 0) {
200
+ for (let docId of collectionToRemove.document_ids) {
201
+ const document = documents[docId.toString()];
202
+ if (document && document.collections) {
203
+ const index = document.collections.indexOf(parseInt(collectionId));
204
+ if (index !== -1) {
205
+ document.collections.splice(index, 1);
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ initDocumentsTreeview(null, function() {
212
+ if (collectionToRemove.parent) {
213
+ highlightCollection(parseInt(collectionToRemove.parent));
214
+ }
215
+ });
216
+ }
217
+
@@ -0,0 +1,92 @@
1
+ function addDocumentContextMenu() {
2
+ $.contextMenu({
3
+ selector: '.document-context-menu',
4
+ build: function ($trigger) {
5
+ const nodeId = $trigger.attr('id');
6
+ const documentId = extractDocumentId(nodeId);
7
+
8
+ if (!documentId) return false;
9
+
10
+ const document = documents[documentId.toString()];
11
+ if (!document) return false;
12
+
13
+ let items = generateDocumentContextMenuItems(document);
14
+
15
+ return {
16
+ callback: (key, options) => documentContextMenuAction(key, options.$trigger.attr("id")),
17
+ items
18
+ };
19
+ }
20
+ });
21
+ }
22
+
23
+ function generateDocumentContextMenuItems(document) {
24
+ const isFavorite = document.favorite === true;
25
+ const favoriteLabel = isFavorite ? gettext('Remove from Bookmarks') : gettext('Add to Bookmarks');
26
+ const favoriteIcon = isFavorite ? 'fas fa-star' : 'far fa-star';
27
+
28
+ const items = {
29
+ "edit": {
30
+ name: gettext('Edit document'),
31
+ icon: "fas fa-edit",
32
+ callback: (key, options) => documentContextMenuAction('edit', document.id)
33
+ },
34
+ "favorite": {
35
+ name: favoriteLabel,
36
+ icon: favoriteIcon,
37
+ callback: (key, options) => documentContextMenuAction('favorite', document.id)
38
+ },
39
+ "separator1": "---------",
40
+ "delete": {
41
+ name: gettext('Delete document'),
42
+ icon: "fas fa-trash",
43
+ callback: (key, options) => documentContextMenuAction('delete', document.id)
44
+ }
45
+ };
46
+
47
+ return items;
48
+ }
49
+
50
+ function documentContextMenuAction(action, param) {
51
+ const actionFunction = documentActions[action];
52
+
53
+ if (actionFunction) {
54
+ actionFunction(param);
55
+ } else {
56
+ log('No action found for:', action);
57
+ }
58
+ }
59
+
60
+ function toggleDocumentFavorite(documentId) {
61
+ const document = documents[documentId.toString()];
62
+ if (!document) return;
63
+
64
+ const updatedDocument = {
65
+ id: parseInt(documentId),
66
+ favorite: !document.favorite
67
+ };
68
+
69
+ submitDocumentUpdate(updatedDocument, function(response) {
70
+ updateDocumentsList();
71
+ initDocumentsTreeview();
72
+ }, function(error) {
73
+ log('Error updating document favorite:', 'error', error);
74
+ });
75
+ }
76
+
77
+ const documentActions = {
78
+ edit: documentId => editDocument(documentId),
79
+ favorite: documentId => toggleDocumentFavorite(documentId),
80
+ delete: documentId => deleteDocument(documentId)
81
+ };
82
+
83
+ function extractDocumentId(nodeId) {
84
+ if (!nodeId) return null;
85
+
86
+ const match = nodeId.match(/document-link-(\d+)/);
87
+ return match ? parseInt(match[1]) : null;
88
+ }
89
+
90
+ $(document).ready(function() {
91
+ addDocumentContextMenu();
92
+ });
@@ -0,0 +1,82 @@
1
+ // Custom Select Dropdown with Toolbar Actions
2
+ // Based on https://blog.logrocket.com/creating-custom-select-dropdown-css/
3
+
4
+ // DOM Elements
5
+ const customSelect = document.querySelector(".custom-select");
6
+ const selectBtn = document.querySelector(".select-button");
7
+ const selectedValue = document.querySelector(".selected-value");
8
+ const toolbarActionButtons = document.querySelector(".toolbar-action-buttons");
9
+
10
+ // Toggle dropdown on button click
11
+ selectBtn.addEventListener("click", () => {
12
+ customSelect.classList.toggle("active");
13
+ selectBtn.setAttribute(
14
+ "aria-expanded",
15
+ selectBtn.getAttribute("aria-expanded") === "true" ? "false" : "true"
16
+ );
17
+ });
18
+
19
+ // Add event listeners to options
20
+ function addEventListenerToOption() {
21
+ const optionsList = document.querySelectorAll(".select-dropdown li");
22
+
23
+ optionsList.forEach((option) => {
24
+ option.addEventListener("keyup", createHandler(option));
25
+ option.addEventListener("click", createHandler(option));
26
+ });
27
+ }
28
+
29
+ // Create event handler
30
+ function createHandler(option) {
31
+ return function(e) {
32
+ // Handle click events
33
+ if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
34
+ const text = this.children[1].textContent;
35
+ updateSelectedValue(text);
36
+
37
+ const input = option.querySelector('input[type="radio"]');
38
+ const docId = input.id.split('-')[1];
39
+
40
+ linkDocument(input, docId);
41
+ toggleToolbarActions(true);
42
+ emptyTagSelection(true);
43
+ }
44
+
45
+ // Handle keyboard events
46
+ if (e.key === "Enter") {
47
+ const text = this.textContent;
48
+ updateSelectedValue(text);
49
+ customSelect.classList.remove("active");
50
+ }
51
+ };
52
+ }
53
+
54
+ // Update selected value
55
+ function updateSelectedValue(text) {
56
+ selectedValue.textContent = text;
57
+ selectedValue.title = text;
58
+ customSelect.classList.toggle('empty', !text);
59
+ }
60
+
61
+ // Toggle toolbar actions
62
+ function toggleToolbarActions(show = false) {
63
+ if(show) {
64
+ customSelect.classList.remove("active");
65
+ toolbarActionButtons.style.display = '';
66
+
67
+ if(current_document) {
68
+ const doc = documents[current_document];
69
+ updateSelectedValue(doc ? doc.name : '');
70
+ } else {
71
+ updateSelectedValue('');
72
+ }
73
+ } else {
74
+ toolbarActionButtons.style.setProperty('display', 'none', 'important');
75
+ updateSelectedValue('');
76
+ }
77
+ }
78
+
79
+ // Initialize event listeners
80
+ addEventListenerToOption();
81
+ // Toggle Toolbars actions
82
+ toggleToolbarActions(false);