Skip to content

[Bug] : draw.dt script re-execution handler skips external scripts when src attribute is an absolute URL #5948

@hhhc

Description

@hhhc

Environment

  • Backpack CRUD: 7.0.22
  • Basset: 2.0.2
  • DataTables: 2.1.8 (bundled)
  • PHP: 8.3
  • Laravel: 12
  • BASSET_DISK: s3 (CloudFront CDN)

Description

All @bassetBlock-powered button scripts (e.g. deleteEntry, quickButtonAction) fail with Uncaught ReferenceError: deleteEntry is not defined on CRUD list views when Basset is configured with an external disk like S3/CloudFront that produces absolute src URLs.

The show view works fine. The issue is specific to list views where buttons are loaded via the DataTables AJAX response.

Root Cause

The draw.dt handler in datatable_logic.blade.php (line ~740) re-executes <script> tags that DataTables 2.x no longer auto-executes due to replaceChildren(). However, the deduplication check finds the script element currently being iterated on (still in the <td>) before removing it:

document.getElementById(tableId).querySelectorAll('script').forEach(function(script) {
    const scriptsToLoad = [];
    if (script.src) {
        const srcUrl = script.src;

        // BUG: This querySelector searches the ENTIRE document, including the
        // script we're currently processing (still inside the <td>).
        // When the src attribute is absolute, it matches script.src exactly,
        // so the script is found and skipped.
        if (!document.querySelector(`script[src="${srcUrl}"]`)) {
            // ... append to <head> (never reached)
        }

        // Script is removed from table but was never added to <head>
        script.parentNode.removeChild(script);
    }
});

document.querySelector(script[src="${srcUrl}"]) matches against the src attribute value (not the resolved URL). This means:

  • BASSET_DISK=public (local disk): The src attribute is a relative path (/storage/basset/.../delete-button.js?abc123), but script.src (DOM property) resolves to an absolute URL (https://myapp.test/storage/basset/.../delete-button.js?abc123). The querySelector doesn't find a match (relative != absolute) → dedup passes → script loads correctly.

  • BASSET_DISK=s3 (external CDN): The src attribute is already absolute (https://cdn.example.com/basset/.../delete-button.js?abc123), identical to script.src. The querySelector finds the script currently being iterated → dedup incorrectly concludes it's already loaded → script is removed from the table but never added to <head>.

Steps to Reproduce

  1. Configure Basset with an external disk that produces absolute URLs:
    BASSET_DEV_MODE=false
    BASSET_DISK=s3
    
  2. Run php artisan basset:cache to populate assets on S3
  3. Visit any CRUD list view that has a delete button (e.g. /admin/entry)
  4. Click the delete button
  5. Expected: SweetAlert confirmation dialog appears
  6. Actual: Uncaught ReferenceError: deleteEntry is not defined

The same issue affects any button that uses @bassetBlock for its JavaScript.

Note: Also reproducible with BASSET_DISK=public by setting BASSET_RELATIVE_PATHS=false (which forces absolute URLs).

Proposed Fix

Move script.parentNode.removeChild(script) before the dedup querySelector check. This removes the script from the DOM first, so the querySelector won't find it:

tableElement.querySelectorAll('script').forEach(function(script) {
    // Remove from table FIRST to prevent the dedup querySelector from finding itself
    if (script.parentNode) {
        script.parentNode.removeChild(script);
    }

    if (script.src) {
        const srcUrl = script.src;

        // Now this correctly only finds scripts already loaded in <head>
        if (!document.querySelector(`script[src="${srcUrl}"]`)) {
            const newScript = document.createElement('script');
            Array.from(script.attributes).forEach(attr => {
                newScript.setAttribute(attr.name, attr.value);
            });
            newScript.onerror = function(e) {
                console.warn('Error loading script:', srcUrl, e);
            };
            try {
                document.head.appendChild(newScript);
            } catch (e) {
                console.warn('Error appending external script:', e);
            }
        }
    } else {
        const newScript = document.createElement('script');
        Array.from(script.attributes).forEach(attr => {
            newScript.setAttribute(attr.name, attr.value);
        });
        newScript.textContent = script.textContent;
        try {
            document.head.appendChild(newScript);
        } catch (e) {
            console.warn('Error appending inline script:', e);
        }
    }
});

This also removes the unused scriptsToLoad array (declared inside the forEach callback and never consumed).

Workaround

Publish datatable_logic.blade.php and apply the fix above:

# Copy the view to your project
mkdir -p resources/views/vendor/backpack/crud/components/datatable
cp vendor/backpack/crud/src/resources/views/crud/components/datatable/datatable_logic.blade.php \
   resources/views/vendor/backpack/crud/components/datatable/datatable_logic.blade.php

Then edit the published file with the fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions