wagtail-nest / wagtail-review

A Wagtail extension for gathering annotations and feedback on pages before publication
BSD 3-Clause "New" or "Revised" License
49 stars 19 forks source link

Issue with mobile devices and deleting annotation one by one #51

Open oladhari opened 1 year ago

oladhari commented 1 year ago

the best solution that I suggest is to replace Annotator.js package in wagtail-review by Recogito.js package which can support both deleting annotation one by one and also using on mobile and tablet

oladhari commented 1 day ago

@gasman I do not why you ignore this serious issue by the way, the fact that we can not delete the comments (annotation) one by one is very bad for the UX of our clients I am trying to add manually by overriding the HTML through javascript but can not find yet a solution how to delete the specific annotation I added a designed button to click on it through the design here is my example:

<head>

    <style>
        .annotator-controls {
            position: relative;
            cursor: pointer;
            font-size: 20px;
            color: white !important;    /* X color */
            background-color: red;       /* Button background */
            padding: 5px;
            border-radius: 50%;          /* Make it a circle */
            width: 30px;                 /* Set width and height to create a circular shape */
            height: 30px;
            display: flex;
            align-items: center;         /* Center the X vertically */
            justify-content: center;     /* Center the X horizontally */
            font-weight: bold;           /* Make the X bold */
            transition: background-color 0.3s ease; /* Optional: Transition effect */
        }

        /* Add the X using the ::before pseudo-element */
        .annotator-controls::before {
            content: '×';                 /* The X mark */
            color: white;
        }

        /* Optional hover effect for better UX */
        .annotator-controls:hover {
            background-color: darkred;    /* Darker red on hover */
        }
    </style>
</head>
<script>
        const app = new annotator.App();
        {% if allow_annotations %}
            app.include(annotator.ui.main, {
                viewerExtensions: [annotatorExt.viewerWithUsernames]
            });
        {% else %}
            app.include(annotatorExt.viewerModeUi);
        {% endif %}
        app.include(annotator.storage.http, {
            'prefix': "{% url 'wagtail_review:annotations_api_root' %}",
            'urls': {
                create: 'annotations/',
                update: 'annotations/{id}/',
                destroy: 'annotations/{id}/',
                search: 'search/'
            },
            'headers': {
                {% if allow_annotations %}
                    'X-CSRFToken': document.getElementById('wagtailreview-respond-form').csrfmiddlewaretoken.value,
                {% endif %}
                'X-WagtailReview-mode': '{{ mode }}',
                'X-WagtailReview-reviewer': '{{ reviewer.id }}',
                'X-WagtailReview-token': '{{ token }}',
            }
        });
        app.start().then(function () {
            app.annotations.load();
        });

        {% if allow_responses %}

            document.addEventListener('DOMContentLoaded', function() {
                function deleteAnnotation() {
                    // Assuming you have access to the annotator.js instance and its methods
                    // Call the annotator.js method to delete the annotation
                    console.log("it is clicked")
                    console.log(this)
                }

                // Function to inject text and add click event to the span
                function injectTextAndAddEvent(span) {                    
                    span.addEventListener('click', deleteAnnotation); // Add click event to call delete function
                }
                document.querySelectorAll('.annotator-outer.annotator-viewer').forEach(parentDiv => {
                    parentDiv.addEventListener('mouseenter', function() {
                        // When the parent is hovered, find the span with class 'annotator-controls'
                        const span = parentDiv.querySelector('.annotator-controls');
                        if (span) {
                            injectTextAndAddEvent(span);
                        }
                    });

                    parentDiv.addEventListener('mouseleave', function() {
                        // You can optionally remove the click event or take another action when hover is removed
                        const span = parentDiv.querySelector('.annotator-controls');
                        if (span) {
                            span.removeEventListener('click', deleteAnnotation); // Optional clean-up
                        }
                    });
                });
                const form = document.getElementById('wagtailreview-respond-form');
                form.style.display = 'none';
                formShown = false;

                {# Auto-update the form's hidden input when clicking on one of the buttons #}
                form.querySelectorAll("button").forEach(button =>
                    button.addEventListener("click", (event) =>
                        document.querySelector("#id_result").value =
                            (event.currentTarget.id === "approve_text")
                                {# values from wagtail_review/models.py RESULT_CHOICES #}
                                ? "approve" : "comment"
                    )
                );

                let button_close_modal=document.getElementById("close_modal");
                let overlay = document.getElementById("my-overlay");
                let button_respond_review=document.getElementById('wagtailreview-respond-button')
                button_close_modal.addEventListener('click', function() {
                    if (formShown) {
                        form.style.display = 'none';
                        overlay.style.display = "none";
                        formShown = false;
                    }
                })
                window.onclick = function(event) {
                    if (event.target == overlay && formShown) {
                        form.style.display = 'none';
                        overlay.style.display = "none";
                        formShown = false;
                    }
                }

                button_respond_review.addEventListener('click', function() {
                        if (formShown) {
                            form.style.display = 'none';
                            overlay.style.display = "none";
                            formShown = false;
                        } else {
                            form.style.display = 'block';
                            overlay.style.display = "block";
                            if (document.querySelector(".annotator-hl")){
                                let approved = document.getElementById('approve_text')
                                let refused = document.getElementById('refuse_text')
                                approved.classList.add("hidden");
                                refused.classList.remove("hidden");
                            }
                            formShown = true;
                        }
                        return false;
                    });
            });
            // TODO: add error alert if approve and comment are not selected
       {% endif %}
    </script>
{% endif %}
oladhari commented 1 day ago

Screencast from 10-21-2024 09:51:14 AM.webm

oladhari commented 1 day ago

here related question in stackoverflow: https://stackoverflow.com/q/79108407/5792426

oladhari commented 1 day ago

@gasman here I update the code with this fix, but still can not access the API URL to delete the annotation: N.B: 158 is an id number of one of the annotations, I am trying to use it as test

// Function to delete the annotation by sending an API DELETE request
function deleteAnnotation(annotationId) {
    // Use the correct API root (prefix) for Wagtail Review annotations
    const prefix = app.annotations.store.options.prefix;  // This is '/review/api/'
    const deleteUrl = `${prefix}annotations/${annotationId}/`;  // Construct the full delete URL

    // Retrieve the CSRF token directly from app.annotations.store
    const csrfToken = app.annotations.store.options.headers["X-CSRFToken"];

    fetch(deleteUrl, {
        method: 'DELETE',
        headers: {
            'X-CSRFToken': csrfToken,  // Use the CSRF token from app.annotations.store
            'Content-Type': 'application/json'
        }
    })
    .then(response => {
        if (response.ok) {
            console.log(`Annotation ${annotationId} deleted successfully`);
        } else {
            console.error(`Failed to delete annotation ${annotationId}: ${response.status}`);
        }
    })
    .catch(error => {
        console.error('Error during deletion:', error);
    });
}

// Function to inject text and add click event to the span
function injectTextAndAddEvent(span, annotationId) {
    span.addEventListener('click', function() {
        deleteAnnotation(annotationId); // Call delete function with the correct ID
    });
}

document.querySelectorAll('.annotator-outer.annotator-viewer').forEach(parentDiv => {
    parentDiv.addEventListener('mouseenter', function() {
        // When the parent is hovered, find the span with class 'annotator-controls'
        const span = parentDiv.querySelector('.annotator-controls');
        if (span) {
            // Pass the correct annotation ID to the function
            const annotationId = 158;  // Replace this with the correct ID dynamically
            injectTextAndAddEvent(span, annotationId);
        }
    });

    parentDiv.addEventListener('mouseleave', function() {
        // You can optionally remove the click event or take another action when hover is removed
        const span = parentDiv.querySelector('.annotator-controls');
        if (span) {
            span.removeEventListener('click', deleteAnnotation); // Optional clean-up
        }
    });
});

also I can access all annotation through this code :

app.annotations.query().then(response => {
                    // Now you have access to the results
                    const annotations = response.results;

                    // Log the annotations array to inspect it
                    console.log('Annotations:', annotations);

                });
oladhari commented 1 day ago

by the way we need to update the item view to become like this (so it can handle delete URL requests:

@never_cache
def item(request, id):
    reviewer, mode = _check_reviewer_credentials(request)

    if request.method == 'GET':
        annotation = get_object_or_404(Annotation, id=id)

        # only allow retrieving annotations within the same review as the current user's credentials
        if reviewer.review != annotation.reviewer.review:
            raise PermissionDenied

        return JsonResponse(annotation.as_json_data())

    elif request.method == 'DELETE':
        annotation = get_object_or_404(Annotation, id=id)

        # Check if the reviewer is allowed to delete the annotation
        if reviewer.review != annotation.reviewer.review:
            raise PermissionDenied

        # If the reviewer has permission, delete the annotation
        annotation.delete()

        return JsonResponse({'status': 'Annotation deleted successfully'}, status=204)

    else:
        return HttpResponseNotAllowed(['GET', 'DELETE'], "Method not allowed")
oladhari commented 1 day ago

@gasman the only challenge that I face now is how I can get the annotationId dynamically I almost resolved all, except this last step

oladhari commented 1 day ago

almost resolved it:

 document.addEventListener('DOMContentLoaded', function() {

                const csrfToken = document.getElementById('wagtailreview-respond-form').csrfmiddlewaretoken.value;

                // Function to remove the highlight from the text corresponding to the deleted annotation
                function removeHighlight(annotationId) {
                    // Assuming the highlight has a specific class or attribute related to the annotation ID
                    const highlights = document.querySelectorAll(`[data-annotation-id="${annotationId}"]`);

                    highlights.forEach(function(highlight) {
                        highlight.classList.remove('annotator-hl');  // Option 1: Reset background color
                        // Option 2: Remove the highlight element entirely
                        // highlight.parentNode.removeChild(highlight);
                    });

                }

                // Function to delete the annotation by ID
                // Function to delete the annotation by sending an API DELETE request
                function deleteAnnotation(annotationId) {
                    const prefix = app.annotations.store.options.prefix;
                    const deleteUrl = `${prefix}annotations/${annotationId}/`;

                    // Retrieve necessary headers
                    const mode = app.annotations.store.options.headers["X-WagtailReview-mode"];
                    const reviewerId = app.annotations.store.options.headers["X-WagtailReview-reviewer"];
                    const token = app.annotations.store.options.headers["X-WagtailReview-token"];

                    fetch(deleteUrl, {
                        method: 'DELETE',
                        headers: {
                            'X-CSRFToken': csrfToken,
                            'X-WagtailReview-Mode': mode,
                            'X-WagtailReview-Reviewer': reviewerId,
                            'X-WagtailReview-Token': token,
                            'Content-Type': 'application/json'
                        }
                    })
                    .then(response => {
                        if (response.ok) {
                            console.log(`Annotation ${annotationId} deleted successfully`);
                            removeHighlight(annotationId);  // Call this function to remove the highlight
                        } else {
                            console.error(`Failed to delete annotation ${annotationId}: ${response.status}`);
                        }
                    })
                    .catch(error => {
                        console.error('Error during deletion:', error);
                    });
                }

                // Function to inject text and add click event to the span
                function injectTextAndAddEvent(span, annotationId) {
                    span.addEventListener('click', function() {
                        deleteAnnotation(annotationId); // Call delete function with the correct ID
                    });
                }

                document.querySelectorAll('.annotator-outer.annotator-viewer').forEach(parentDiv => {
                    parentDiv.addEventListener('mouseenter', function() {
                        // When the parent is hovered, find the span with class 'annotator-controls'
                        // Find the inner div inside the structure that contains the text
                        const textDiv = parentDiv.querySelector('ul li div');
                            // Exclude the reviewer name and only get the annotation text after the <p> tag
                        const annotationText = textDiv.childNodes[1].textContent.trim();  // Get only the second child node, which is the text

                        app.annotations.query().then(response => {
                            // Assign the results to the broader scope variable
                            annotations = response.results;

                            // Loop through the annotations and find the one that matches the text

                            const span = parentDiv.querySelector('.annotator-controls');
                            annotations.forEach(annotation => {
                                if (annotation.text === annotationText) {  // Ensure 'annotationText' is defined before comparison
                                    annotationId = annotation.id;  // Assign the annotationId
                                }
                            });

                            if (span) {
                                // Pass the correct annotation ID to the function
                                injectTextAndAddEvent(span, annotationId);
                            }
                        });

                    });

                    parentDiv.addEventListener('mouseleave', function() {
                        // You can optionally remove the click event or take another action when hover is removed
                        const span = parentDiv.querySelector('.annotator-controls');
                        if (span) {
                            span.removeEventListener('click', deleteAnnotation); // Optional clean-up
                        }
                    });
                });

})
gasman commented 1 day ago

@gasman I do not why you ignore this serious issue by the way, the fact that we can not delete the comments (annotation) one by one is very bad for the UX of our clients

Hi @oladhari - I don't remember seeing this issue before, but I probably skipped over it because your message never stated what the issue actually is. "Issue with mobile devices" could mean anything at all, ranging from "it is completely broken" to "the animation isn't very smooth". There are hundreds of open issues across all Wagtail projects, and if I need to do extra work (or research a new JS library) just to learn what the problem is, then this one will end up very low on my priority list.

Unfortunately I do not have the resources to keep track of the issue list for add-on packages in the same way that I would do for Wagtail itself.

Chasing me up simultaneously on Github, Slack, email and Stack Overflow is not appropriate, and will not get this addressed any faster.

oladhari commented 18 hours ago

@gasman I do not why you ignore this serious issue by the way, the fact that we can not delete the comments (annotation) one by one is very bad for the UX of our clients

Hi @oladhari - I don't remember seeing this issue before, but I probably skipped over it because your message never stated what the issue actually is. "Issue with mobile devices" could mean anything at all, ranging from "it is completely broken" to "the animation isn't very smooth". There are hundreds of open issues across all Wagtail projects, and if I need to do extra work (or research a new JS library) just to learn what the problem is, then this one will end up very low on my priority list.

Unfortunately I do not have the resources to keep track of the issue list for add-on packages in the same way that I would do for Wagtail itself.

Chasing me up simultaneously on Github, Slack, email and Stack Overflow is not appropriate, and will not get this addressed any faster.

Thank you very much for your reply and sorry for the harrassement I was little bit frustrated, I was able to handle the delete functionality as you see in the code above and you can test it, if you have time of course need some changes in the view of the file annotations_api.py if you see that is a good change and make the experience of using wagtail review more better, I can do a PR to handle these changes have a good day