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,109 @@
|
|
|
1
|
+
let highlightResizeObserver = null;
|
|
2
|
+
|
|
3
|
+
function loadDocumentHighlights(document_id, highlight_id, splitParagraphs, search_text) {
|
|
4
|
+
if (document_id === null) {
|
|
5
|
+
document_contents.style.direction = 'ltr';
|
|
6
|
+
document_contents.innerHTML = '<p style="font-style: oblique; text-align: center;">' +
|
|
7
|
+
gettext("Load a document on the left") + '</p>';
|
|
8
|
+
toggleToolbarActions(false);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
showSpinner();
|
|
13
|
+
|
|
14
|
+
const info = getJSON(`/api/project/${project_id}/document/${document_id}`);
|
|
15
|
+
const contents = getJSON(`/api/project/${project_id}/document/${document_id}/contents`);
|
|
16
|
+
|
|
17
|
+
Promise.all([info, contents])
|
|
18
|
+
.then(function(results) {
|
|
19
|
+
const info = results[0];
|
|
20
|
+
const contents = results[1];
|
|
21
|
+
|
|
22
|
+
document_contents.innerHTML = '';
|
|
23
|
+
highlights = {};
|
|
24
|
+
|
|
25
|
+
if (highlightResizeObserver) {
|
|
26
|
+
highlightResizeObserver.disconnect();
|
|
27
|
+
highlightResizeObserver = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mainContainer = document.createElement('div');
|
|
31
|
+
mainContainer.style.position = 'relative';
|
|
32
|
+
mainContainer.style.width = '100%';
|
|
33
|
+
|
|
34
|
+
const contentDiv = document.createElement('div');
|
|
35
|
+
contentDiv.id = 'highlight-content';
|
|
36
|
+
contentDiv.style.userSelect = 'text';
|
|
37
|
+
contentDiv.style.position = 'relative';
|
|
38
|
+
contentDiv.style.direction = info.text_direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr';
|
|
39
|
+
contentDiv.style.padding = '20px';
|
|
40
|
+
contentDiv.style.lineHeight = '1.6';
|
|
41
|
+
|
|
42
|
+
const allHTML = contents.contents.map((chunk, index) => {
|
|
43
|
+
return `<div id="doc-offset-${chunk.offset}" class="document-chunk" data-chunk-index="${index}">${chunk.contents}</div>`;
|
|
44
|
+
}).join('\n');
|
|
45
|
+
|
|
46
|
+
contentDiv.innerHTML = allHTML;
|
|
47
|
+
disableEditing(contentDiv);
|
|
48
|
+
addDragFunctionality(contentDiv);
|
|
49
|
+
mainContainer.appendChild(contentDiv);
|
|
50
|
+
|
|
51
|
+
// SVG overlay drawn on top of text — pointer-events: none so it doesn't block selection
|
|
52
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
53
|
+
svg.id = 'highlights-overlay';
|
|
54
|
+
svg.style.position = 'absolute';
|
|
55
|
+
svg.style.top = '0';
|
|
56
|
+
svg.style.left = '0';
|
|
57
|
+
svg.style.width = '100%';
|
|
58
|
+
svg.style.height = '100%';
|
|
59
|
+
svg.style.pointerEvents = 'none';
|
|
60
|
+
svg.style.zIndex = '9';
|
|
61
|
+
svg.style.overflow = 'visible';
|
|
62
|
+
|
|
63
|
+
mainContainer.appendChild(svg);
|
|
64
|
+
document_contents.appendChild(mainContainer);
|
|
65
|
+
|
|
66
|
+
for (const element of info.highlights) {
|
|
67
|
+
const id = '' + element.id;
|
|
68
|
+
highlights[id] = element;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
try {
|
|
74
|
+
renderHighlights(Object.values(highlights));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
log("Error rendering highlights", 'error', error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let _resizeTimeout = null;
|
|
82
|
+
highlightResizeObserver = new ResizeObserver(() => {
|
|
83
|
+
if (_renderingInProgress) return;
|
|
84
|
+
if (_resizeTimeout) clearTimeout(_resizeTimeout);
|
|
85
|
+
_resizeTimeout = setTimeout(() => {
|
|
86
|
+
try {
|
|
87
|
+
renderHighlights(Object.values(highlights));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
log("Error re-rendering on resize", 'error', error);
|
|
90
|
+
}
|
|
91
|
+
}, 300);
|
|
92
|
+
});
|
|
93
|
+
highlightResizeObserver.observe(contentDiv);
|
|
94
|
+
|
|
95
|
+
current_document = document_id;
|
|
96
|
+
toggleToolbarActions(true);
|
|
97
|
+
updateDocumentLinkHighlight(current_document);
|
|
98
|
+
updateDocumentExportButton(project_id, document_id);
|
|
99
|
+
updateLinksTarget(document_contents);
|
|
100
|
+
|
|
101
|
+
setTimeout(() => scrollToHighlight(highlight_id, search_text), 200);
|
|
102
|
+
})
|
|
103
|
+
.catch(function(error) {
|
|
104
|
+
log("Failed to load document", 'error', error);
|
|
105
|
+
const loadDocumentErrorMessage = gettext("Error loading document!");
|
|
106
|
+
showAlert(`${loadDocumentErrorMessage} \n\n`, 'error');
|
|
107
|
+
})
|
|
108
|
+
.then(hideSpinner);
|
|
109
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
const LINE_HEIGHT = 3;
|
|
2
|
+
const LINE_GAP = 2;
|
|
3
|
+
const BASE_OFFSET = 2;
|
|
4
|
+
|
|
5
|
+
let _renderingInProgress = false;
|
|
6
|
+
|
|
7
|
+
function renderHighlights(hlList) {
|
|
8
|
+
const container = document.getElementById('highlight-content');
|
|
9
|
+
const svg = document.getElementById('highlights-overlay');
|
|
10
|
+
if (!svg || !container) return;
|
|
11
|
+
|
|
12
|
+
_renderingInProgress = true;
|
|
13
|
+
|
|
14
|
+
const targets = getBlockTargets(container);
|
|
15
|
+
targets.forEach(el => { el.style.lineHeight = ''; });
|
|
16
|
+
|
|
17
|
+
// Pre-compute shared data once
|
|
18
|
+
const textNodes = collectTextNodes(container);
|
|
19
|
+
const { fullText, charMap } = buildCharMap(textNodes);
|
|
20
|
+
const utf8Map = buildUTF8ToCharMap(fullText);
|
|
21
|
+
|
|
22
|
+
const targetMaxLanes = estimateMaxLanesPerTarget(hlList, targets, container, textNodes, fullText, utf8Map);
|
|
23
|
+
|
|
24
|
+
let layoutChanged = false;
|
|
25
|
+
targetMaxLanes.forEach((maxLanes, el) => {
|
|
26
|
+
const newLH = (1.6 + maxLanes * 0.18) + 'em';
|
|
27
|
+
if (el.style.lineHeight !== newLH) {
|
|
28
|
+
el.style.lineHeight = newLH;
|
|
29
|
+
layoutChanged = true;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (layoutChanged) {
|
|
34
|
+
requestAnimationFrame(() => {
|
|
35
|
+
requestAnimationFrame(() => {
|
|
36
|
+
drawHighlights(hlList, container, svg, charMap, fullText, utf8Map);
|
|
37
|
+
_renderingInProgress = false;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
drawHighlights(hlList, container, svg, charMap, fullText, utf8Map);
|
|
42
|
+
_renderingInProgress = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function estimateMaxLanesPerTarget(hlList, targets, container, textNodes, fullText, utf8Map) {
|
|
47
|
+
if (hlList.length === 0) return new Map();
|
|
48
|
+
|
|
49
|
+
const nodeToTarget = new Map();
|
|
50
|
+
textNodes.forEach(node => {
|
|
51
|
+
let el = node.parentElement;
|
|
52
|
+
while (el && el.id !== 'highlight-content') {
|
|
53
|
+
if (targets.includes(el)) {
|
|
54
|
+
nodeToTarget.set(node, el);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
el = el.parentElement;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const charToTarget = [];
|
|
62
|
+
textNodes.forEach(node => {
|
|
63
|
+
const target = nodeToTarget.get(node);
|
|
64
|
+
for (const _ of node.textContent) {
|
|
65
|
+
charToTarget.push(target);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const targetHighlights = new Map();
|
|
70
|
+
hlList.forEach(hl => {
|
|
71
|
+
const startChar = getCharIndexFromUTF8Index(fullText, hl.start_offset, utf8Map);
|
|
72
|
+
const endChar = getCharIndexFromUTF8Index(fullText, hl.end_offset, utf8Map);
|
|
73
|
+
const midChar = Math.max(0, Math.min(charToTarget.length - 1, Math.floor((startChar + endChar) / 2)));
|
|
74
|
+
const target = charToTarget[midChar];
|
|
75
|
+
if (target) {
|
|
76
|
+
if (!targetHighlights.has(target)) targetHighlights.set(target, []);
|
|
77
|
+
targetHighlights.get(target).push(hl);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = new Map();
|
|
82
|
+
targetHighlights.forEach((hls, target) => {
|
|
83
|
+
const sorted = hls.slice().sort((a, b) => a.start_offset - b.start_offset);
|
|
84
|
+
const laneEnds = [];
|
|
85
|
+
for (const hl of sorted) {
|
|
86
|
+
let placed = false;
|
|
87
|
+
for (let i = 0; i < laneEnds.length; i++) {
|
|
88
|
+
if (laneEnds[i] <= hl.start_offset) {
|
|
89
|
+
laneEnds[i] = hl.end_offset;
|
|
90
|
+
placed = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!placed) laneEnds.push(hl.end_offset);
|
|
95
|
+
}
|
|
96
|
+
result.set(target, laneEnds.length);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getBlockTargets(container) {
|
|
103
|
+
const targets = [];
|
|
104
|
+
const chunksWithBlocks = new Set();
|
|
105
|
+
const blockSelectors = '.document-chunk p, .document-chunk h1, .document-chunk h2, .document-chunk h3, .document-chunk h4, .document-chunk h5, .document-chunk h6, .document-chunk li, .document-chunk blockquote';
|
|
106
|
+
|
|
107
|
+
container.querySelectorAll(blockSelectors).forEach(el => {
|
|
108
|
+
targets.push(el);
|
|
109
|
+
const chunk = el.closest('.document-chunk');
|
|
110
|
+
if (chunk) chunksWithBlocks.add(chunk);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
container.querySelectorAll('.document-chunk').forEach(chunk => {
|
|
114
|
+
if (!chunksWithBlocks.has(chunk)) targets.push(chunk);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return targets;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function drawHighlights(hlList, container, svg, charMap, fullText, utf8Map) {
|
|
121
|
+
const startTime = performance.now();
|
|
122
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
123
|
+
svg.innerHTML = '';
|
|
124
|
+
|
|
125
|
+
// If called without pre-computed data, compute it
|
|
126
|
+
if (!charMap) {
|
|
127
|
+
const textNodes = collectTextNodes(container);
|
|
128
|
+
const built = buildCharMap(textNodes);
|
|
129
|
+
fullText = built.fullText;
|
|
130
|
+
charMap = built.charMap;
|
|
131
|
+
utf8Map = buildUTF8ToCharMap(fullText);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const containerRect = container.getBoundingClientRect();
|
|
135
|
+
|
|
136
|
+
// Batch: create all ranges and read all rects in one pass (single reflow)
|
|
137
|
+
const hlRanges = [];
|
|
138
|
+
for (const hl of hlList) {
|
|
139
|
+
if (hl.start_offset === undefined || hl.end_offset === undefined) continue;
|
|
140
|
+
const startChar = getCharIndexFromUTF8Index(fullText, hl.start_offset, utf8Map);
|
|
141
|
+
const endChar = getCharIndexFromUTF8Index(fullText, hl.end_offset, utf8Map);
|
|
142
|
+
if (startChar >= charMap.length || endChar <= 0 || startChar >= endChar) continue;
|
|
143
|
+
|
|
144
|
+
const clampedStart = Math.max(0, startChar);
|
|
145
|
+
const clampedEnd = Math.min(charMap.length, endChar);
|
|
146
|
+
const startEntry = charMap[clampedStart];
|
|
147
|
+
const endEntry = charMap[clampedEnd - 1];
|
|
148
|
+
if (!startEntry || !endEntry) continue;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const range = document.createRange();
|
|
152
|
+
range.setStart(startEntry.node, startEntry.offset);
|
|
153
|
+
range.setEnd(endEntry.node, endEntry.offset + 1);
|
|
154
|
+
hlRanges.push({ highlight: hl, range });
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// skip invalid ranges
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Single reflow: read all client rects
|
|
161
|
+
const allRects = [];
|
|
162
|
+
for (const { highlight, range } of hlRanges) {
|
|
163
|
+
const clientRects = range.getClientRects();
|
|
164
|
+
const raw = [];
|
|
165
|
+
for (const r of clientRects) {
|
|
166
|
+
if (r.width > 0 && r.height > 0) {
|
|
167
|
+
raw.push({
|
|
168
|
+
x: r.left - containerRect.left,
|
|
169
|
+
y: r.top - containerRect.top,
|
|
170
|
+
width: r.width,
|
|
171
|
+
height: r.height
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const rects = deduplicateRects(raw);
|
|
176
|
+
for (const rect of rects) {
|
|
177
|
+
allRects.push({ highlight, rect, stackIndex: 0 });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const lineGroups = new Map();
|
|
182
|
+
allRects.forEach(item => {
|
|
183
|
+
const lineIndex = Math.floor(item.rect.y / 25);
|
|
184
|
+
if (!lineGroups.has(lineIndex)) lineGroups.set(lineIndex, []);
|
|
185
|
+
lineGroups.get(lineIndex).push(item);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
lineGroups.forEach(rectsInLine => assignSmartLanes(rectsInLine));
|
|
189
|
+
|
|
190
|
+
// Batch DOM writes: build fragment then append once
|
|
191
|
+
const fragment = document.createDocumentFragment();
|
|
192
|
+
let lineCount = 0;
|
|
193
|
+
let gradientCount = 0;
|
|
194
|
+
|
|
195
|
+
allRects.forEach(item => {
|
|
196
|
+
const { highlight, rect, stackIndex } = item;
|
|
197
|
+
const tags = highlight.tags.map(tagId => findTag(tagId));
|
|
198
|
+
const colors = tags.map(tag => tag.color);
|
|
199
|
+
const lineY = rect.y + rect.height + BASE_OFFSET + (stackIndex * (LINE_HEIGHT + LINE_GAP));
|
|
200
|
+
|
|
201
|
+
if (colors.length === 1) {
|
|
202
|
+
const line = createSVGLine(rect.x, lineY, rect.x + rect.width, colors[0], highlight, false);
|
|
203
|
+
line.setAttribute("opacity", 0.9);
|
|
204
|
+
fragment.appendChild(line);
|
|
205
|
+
} else {
|
|
206
|
+
const group = document.createElementNS(svgNS, "g");
|
|
207
|
+
const gradientId = `gradient-${lineCount}-${gradientCount++}`;
|
|
208
|
+
group.appendChild(createSVGGradient(gradientId, colors));
|
|
209
|
+
|
|
210
|
+
const line = createSVGLine(rect.x, lineY, rect.x + rect.width, `url(#${gradientId})`, highlight, true);
|
|
211
|
+
line.setAttribute("opacity", 0.9);
|
|
212
|
+
group.appendChild(line);
|
|
213
|
+
fragment.appendChild(group);
|
|
214
|
+
}
|
|
215
|
+
lineCount++;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
svg.appendChild(fragment);
|
|
219
|
+
svg.setAttribute('height', container.scrollHeight);
|
|
220
|
+
svg.style.height = container.scrollHeight + 'px';
|
|
221
|
+
log(`renderHighlights: ${((performance.now() - startTime) / 1000).toFixed(3)}s, ${lineCount} lines`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function collectTextNodes(container) {
|
|
225
|
+
const nodes = [];
|
|
226
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
|
|
227
|
+
let node;
|
|
228
|
+
while (node = walker.nextNode()) nodes.push(node);
|
|
229
|
+
return nodes;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildCharMap(textNodes) {
|
|
233
|
+
let fullText = '';
|
|
234
|
+
const charMap = [];
|
|
235
|
+
textNodes.forEach(node => {
|
|
236
|
+
const text = node.textContent;
|
|
237
|
+
for (let i = 0; i < text.length; i++) {
|
|
238
|
+
charMap.push({ node, offset: i });
|
|
239
|
+
}
|
|
240
|
+
fullText += text;
|
|
241
|
+
});
|
|
242
|
+
return { fullText, charMap };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function deduplicateRects(rects) {
|
|
246
|
+
// Remove exact duplicates
|
|
247
|
+
const unique = [];
|
|
248
|
+
for (const r of rects) {
|
|
249
|
+
const isDupe = unique.some(u =>
|
|
250
|
+
Math.abs(u.x - r.x) < 1 && Math.abs(u.y - r.y) < 1 &&
|
|
251
|
+
Math.abs(u.width - r.width) < 1 && Math.abs(u.height - r.height) < 1
|
|
252
|
+
);
|
|
253
|
+
if (!isDupe) unique.push(r);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 2. Remove block-level rects (taller than text rects)
|
|
257
|
+
const medianHeight = unique.length > 0
|
|
258
|
+
? unique.map(r => r.height).sort((a, b) => a - b)[Math.floor(unique.length / 2)]
|
|
259
|
+
: 20;
|
|
260
|
+
const result = [];
|
|
261
|
+
for (const r of unique) {
|
|
262
|
+
if (r.height > medianHeight * 1.3) {
|
|
263
|
+
const hasOverlapping = unique.some(other => {
|
|
264
|
+
if (other === r) return false;
|
|
265
|
+
return Math.abs(r.y - other.y) < r.height &&
|
|
266
|
+
other.height <= medianHeight * 1.1;
|
|
267
|
+
});
|
|
268
|
+
if (hasOverlapping) continue; // skip this block-level rect
|
|
269
|
+
}
|
|
270
|
+
result.push(r);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Merge adjacent rects on the same visual line
|
|
274
|
+
result.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
275
|
+
const merged = [];
|
|
276
|
+
for (const rect of result) {
|
|
277
|
+
const last = merged[merged.length - 1];
|
|
278
|
+
if (last && Math.abs(last.y - rect.y) < 3) {
|
|
279
|
+
const gap = rect.x - (last.x + last.width);
|
|
280
|
+
if (gap < 2) {
|
|
281
|
+
last.width = Math.max(last.x + last.width, rect.x + rect.width) - last.x;
|
|
282
|
+
last.height = Math.max(last.height, rect.height);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
merged.push({ ...rect });
|
|
287
|
+
}
|
|
288
|
+
return merged;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function assignSmartLanes(rectsInLine) {
|
|
292
|
+
const lanes = [];
|
|
293
|
+
rectsInLine.sort((a, b) => a.rect.x - b.rect.x);
|
|
294
|
+
|
|
295
|
+
for (const current of rectsInLine) {
|
|
296
|
+
let placed = false;
|
|
297
|
+
for (let laneIdx = 0; laneIdx < lanes.length; laneIdx++) {
|
|
298
|
+
const overlaps = lanes[laneIdx].some(other => {
|
|
299
|
+
if (other.highlight.id === current.highlight.id) return false;
|
|
300
|
+
return !(current.rect.x >= other.rect.x + other.rect.width ||
|
|
301
|
+
other.rect.x >= current.rect.x + current.rect.width);
|
|
302
|
+
});
|
|
303
|
+
if (!overlaps) {
|
|
304
|
+
lanes[laneIdx].push(current);
|
|
305
|
+
current.stackIndex = laneIdx;
|
|
306
|
+
placed = true;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!placed) {
|
|
311
|
+
current.stackIndex = lanes.length;
|
|
312
|
+
lanes.push([current]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return lanes;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function createSVGLine(x1, y, x2, color, highlight, isGradient) {
|
|
319
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
320
|
+
const line = document.createElementNS(svgNS, "line");
|
|
321
|
+
|
|
322
|
+
line.setAttribute("x1", x1);
|
|
323
|
+
line.setAttribute("y1", y);
|
|
324
|
+
line.setAttribute("x2", x2);
|
|
325
|
+
line.setAttribute("y2", y + (isGradient ? 0.1 : 0));
|
|
326
|
+
line.setAttribute("stroke", color);
|
|
327
|
+
line.setAttribute("stroke-width", LINE_HEIGHT);
|
|
328
|
+
line.setAttribute("stroke-linecap", "round");
|
|
329
|
+
line.setAttribute("data-highlight-id", highlight.id);
|
|
330
|
+
line.style.pointerEvents = 'all';
|
|
331
|
+
line.style.cursor = 'pointer';
|
|
332
|
+
line.style.transition = 'stroke-width 0.2s';
|
|
333
|
+
|
|
334
|
+
line.addEventListener('mouseenter', function(e) {
|
|
335
|
+
line.setAttribute("stroke-width", "5");
|
|
336
|
+
showLineTooltip(e, highlight.id);
|
|
337
|
+
});
|
|
338
|
+
line.addEventListener('mouseleave', function() {
|
|
339
|
+
line.setAttribute("stroke-width", LINE_HEIGHT);
|
|
340
|
+
hideLineTooltip();
|
|
341
|
+
});
|
|
342
|
+
line.addEventListener('click', function() {
|
|
343
|
+
editHighlight.call(line);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return line;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function createSVGGradient(id, colors) {
|
|
350
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
351
|
+
const gradient = document.createElementNS(svgNS, "linearGradient");
|
|
352
|
+
gradient.setAttribute("id", id);
|
|
353
|
+
gradient.setAttribute("x1", "0%");
|
|
354
|
+
gradient.setAttribute("y1", "0%");
|
|
355
|
+
gradient.setAttribute("x2", "100%");
|
|
356
|
+
gradient.setAttribute("y2", "0%");
|
|
357
|
+
|
|
358
|
+
colors.forEach((color, index) => {
|
|
359
|
+
const stop = document.createElementNS(svgNS, "stop");
|
|
360
|
+
stop.setAttribute("offset", colors.length === 1 ? "100%" : `${(index / (colors.length - 1)) * 100}%`);
|
|
361
|
+
stop.setAttribute("stop-color", color);
|
|
362
|
+
gradient.appendChild(stop);
|
|
363
|
+
});
|
|
364
|
+
return gradient;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function showLineTooltip(event, highlightId) {
|
|
368
|
+
let tooltip = document.getElementById('highlight-tooltip');
|
|
369
|
+
|
|
370
|
+
if (!tooltip) {
|
|
371
|
+
tooltip = document.createElement('div');
|
|
372
|
+
tooltip.id = 'highlight-tooltip';
|
|
373
|
+
document.body.appendChild(tooltip);
|
|
374
|
+
Object.assign(tooltip.style, {
|
|
375
|
+
position: 'fixed', padding: '10px', background: '#fff',
|
|
376
|
+
border: '2px solid #333', borderRadius: '6px',
|
|
377
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.2)', fontSize: '13px',
|
|
378
|
+
pointerEvents: 'none', zIndex: '10000', maxWidth: '300px',
|
|
379
|
+
wordWrap: 'break-word', overflowWrap: 'break-word'
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const hl = highlights[highlightId];
|
|
384
|
+
if (!hl) return;
|
|
385
|
+
|
|
386
|
+
const tags = hl.tags.map(tagId => findTag(tagId));
|
|
387
|
+
tooltip.innerHTML = `
|
|
388
|
+
<div style="display: flex; gap: 4px; flex-wrap: wrap; max-width: 280px;">
|
|
389
|
+
${tags.map(tag => `
|
|
390
|
+
<div style="display: inline-block; padding: 4px 8px; border-radius: 3px;
|
|
391
|
+
background-color: ${tag.color}; color: white; font-size: 11px;">
|
|
392
|
+
${tag.path || tag.name}
|
|
393
|
+
</div>
|
|
394
|
+
`).join('')}
|
|
395
|
+
</div>
|
|
396
|
+
`;
|
|
397
|
+
tooltip.style.display = 'block';
|
|
398
|
+
tooltip.style.left = (event.clientX + 10) + 'px';
|
|
399
|
+
tooltip.style.top = (event.clientY + 10) + 'px';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function hideLineTooltip() {
|
|
403
|
+
const tooltip = document.getElementById('highlight-tooltip');
|
|
404
|
+
if (tooltip) tooltip.style.display = 'none';
|
|
405
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
$(document).ready(function() {
|
|
2
|
+
$('#entity_id-select').hierarchySelect({
|
|
3
|
+
hierarchy: false,
|
|
4
|
+
search: true,
|
|
5
|
+
width: 'auto',
|
|
6
|
+
initialValueSet: true
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
$("#cilogon").click(function(){
|
|
10
|
+
let entity_id = encodeURIComponent($("#entity_id").val()),
|
|
11
|
+
url = $(this).attr('href').split('?')[0],
|
|
12
|
+
url_with_params = `${url}?entity_id=${entity_id}`;
|
|
13
|
+
$(this).attr('href', url_with_params);
|
|
14
|
+
});
|
|
15
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Members
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function addMember(login, privileges) {
|
|
6
|
+
members[login] = {privileges: privileges};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function removeMember(login) {
|
|
10
|
+
delete members[login];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let members_initial = {};
|
|
14
|
+
let members_displayed = {};
|
|
15
|
+
|
|
16
|
+
function _memberRow(login, user) {
|
|
17
|
+
let elem = document.createElement('div');
|
|
18
|
+
elem.className = 'row members-item';
|
|
19
|
+
elem.innerHTML = '<div class="col-md-4 text-center">' +
|
|
20
|
+
' <i class="fas fa-user fa-2x"></i>' +
|
|
21
|
+
' <p class="members-item-login">' + login + '</p>' +
|
|
22
|
+
'</div>' +
|
|
23
|
+
'<div class="col-md-4 form-group">' +
|
|
24
|
+
' <select class="form-control">' +
|
|
25
|
+
' <option value="PROJECT_OWNER">' + gettext("Can create projects") + '</option>' +
|
|
26
|
+
' <option value="TEAM_MEMBER">' + gettext("Can't create projects") + '</option>' +
|
|
27
|
+
' </select>' +
|
|
28
|
+
'</div>' +
|
|
29
|
+
'<hr class="custom-hr" />';
|
|
30
|
+
|
|
31
|
+
[].forEach.call(elem.querySelectorAll('option'), function (e) {
|
|
32
|
+
if (e.value === user.privileges) {
|
|
33
|
+
e.selected = true;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return elem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showMembers() {
|
|
41
|
+
document.getElementById('members-add').reset();
|
|
42
|
+
document.getElementById('members-add-fields').removeAttribute('disabled');
|
|
43
|
+
|
|
44
|
+
let entries = Object.entries(members);
|
|
45
|
+
sortByKey(entries, function (e) {
|
|
46
|
+
return e[0];
|
|
47
|
+
});
|
|
48
|
+
log("Members:", entries.map(function (e) {
|
|
49
|
+
return e[0] + " (" + e[1].privileges + ")";
|
|
50
|
+
})
|
|
51
|
+
.join(", "));
|
|
52
|
+
|
|
53
|
+
// Empty the list
|
|
54
|
+
let current_members = document.getElementById('members-current');
|
|
55
|
+
current_members.innerHTML = '';
|
|
56
|
+
|
|
57
|
+
// Fill it back up
|
|
58
|
+
for (let i = 0; i < entries.length; ++i) {
|
|
59
|
+
const login = entries[i][0];
|
|
60
|
+
const user = entries[i][1];
|
|
61
|
+
const elem = _memberRow(login, user);
|
|
62
|
+
if(login !== user_login) {
|
|
63
|
+
current_members.appendChild(elem);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Store current state so that we can compare later
|
|
68
|
+
members_initial = Object.assign({}, members);
|
|
69
|
+
members_displayed = Object.assign({}, members);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
document.getElementById('members-add').addEventListener('submit', function (e) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
|
|
75
|
+
const login = document.getElementById('member-add-name').value.toLowerCase();
|
|
76
|
+
if (!login) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const privileges = document.getElementById('member-add-privileges').value;
|
|
80
|
+
|
|
81
|
+
// Check login
|
|
82
|
+
if (login in members_displayed) {
|
|
83
|
+
showAlert(gettext("Already a member!"));
|
|
84
|
+
document.getElementById('members-add').reset();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
postJSON('/api/check_user', {login: login, permission: true})
|
|
88
|
+
.then(function (result) {
|
|
89
|
+
if (result.exists) {
|
|
90
|
+
// Add it at the top
|
|
91
|
+
const elem = _memberRow(login, {privileges: privileges});
|
|
92
|
+
const current_members = document.getElementById('members-current');
|
|
93
|
+
current_members.insertBefore(elem, current_members.firstChild);
|
|
94
|
+
members_displayed[login] = true;
|
|
95
|
+
|
|
96
|
+
document.getElementById('members-add').reset();
|
|
97
|
+
} else {
|
|
98
|
+
showAlert(gettext("Cannot add this user<br/>Reasons : <ul><li>User cannot be found</li><li>User is already sponsored</li><li>User role cannot be changed</li></ul>"));
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
function sendMembersPatch() {
|
|
104
|
+
let patch = {};
|
|
105
|
+
|
|
106
|
+
let members_before = Object.assign({}, members_initial);
|
|
107
|
+
|
|
108
|
+
const rows = document.getElementById('members-current').querySelectorAll('.members-item');
|
|
109
|
+
for (let i = 0; i < rows.length; ++i) {
|
|
110
|
+
const row = rows[i];
|
|
111
|
+
const login = row.querySelector('.members-item-login').textContent;
|
|
112
|
+
const privileges = row.querySelector('select').value;
|
|
113
|
+
|
|
114
|
+
// Add to patch, if different from stored version
|
|
115
|
+
if (!members_before[login] || members_before[login].privileges !== privileges) {
|
|
116
|
+
patch[login] = {privileges: privileges};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove from object, so what's left are the removed members
|
|
120
|
+
delete members_before[login];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Remove the members that are left
|
|
124
|
+
const entries = Object.entries(members_before);
|
|
125
|
+
for (let i = 0; i < entries.length; ++i) {
|
|
126
|
+
patch[entries[i][0]] = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
log("Patching users list");
|
|
130
|
+
showSpinner();
|
|
131
|
+
patchJSON('/api/permissions', patch)
|
|
132
|
+
.then(function () {
|
|
133
|
+
log("Users list patched");
|
|
134
|
+
})
|
|
135
|
+
.catch(function (error) {
|
|
136
|
+
log("Failed to patch users list", 'error', {error});
|
|
137
|
+
const updatePermissionsErrorMessage = gettext("Couldn't update permissions!");
|
|
138
|
+
showAlert(`${updatePermissionsErrorMessage} \n \n`, 'error', {error})
|
|
139
|
+
})
|
|
140
|
+
.then(hideSpinner);
|
|
141
|
+
|
|
142
|
+
members_initial = {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
document.getElementById('members-submit').addEventListener('click', function (e) {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
sendMembersPatch();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
document.getElementById('members-current').addEventListener('submit', function (e) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
sendMembersPatch();
|
|
153
|
+
});
|