Kaynağa Gözat

Fix: drop unreliable SRI hashes + guard sortable() calls

Symptom: browser console errors "TypeError: $tbody.sortable is not a
function" on /sprints/{id}, and the "Add task" button does nothing.

Root cause: the layout had hand-typed `integrity="sha256-..."` attributes
on the jQuery UI (and jQuery) CDN scripts. At least one of them didn't
match the actual file hash, so the browser refused to execute that
script. jQuery UI never loaded, `$.fn.sortable` was undefined, and when
sprint-planner.js hit the very first `.sortable(...)` call the whole
IIFE threw — which meant every event binding AFTER that point (all
task handlers: Add/edit/delete/assign, filters, sort) was never
registered.

Fix:
- Drop integrity/crossorigin from both CDN script tags in layout.php.
  For a small internal tool this trades off SRI for not-breaking-on-
  typos; reinstate when we have a build step or pinned vendored files.
- Guard every `.sortable(...)` call with `typeof $.fn.sortable ===
  'function'`. If jQuery UI still fails to load (ad blocker, offline
  CDN), drag reorder is silently disabled and a console warning is
  logged, but every other behaviour keeps working.

Touched: views/layout.php, public/assets/js/sprint-planner.js,
public/assets/js/sprint-settings.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa 2 hafta önce
ebeveyn
işleme
927b7084b0

+ 10 - 2
public/assets/js/sprint-planner.js

@@ -277,8 +277,16 @@
     // Worker row drag-reorder (admin only — tbody only exists with handles)
     // ---------------------------------------------------------------------
 
+    const sortableAvailable = typeof $.fn.sortable === 'function';
+    if (!sortableAvailable) {
+        // jQuery UI didn't load (SRI mismatch, offline CDN, ad blocker).
+        // Drag-reorder is unavailable but the rest of the page still works.
+        // eslint-disable-next-line no-console
+        console.warn('[sprint-planner] jQuery UI not loaded — drag reorder disabled.');
+    }
+
     const $tbody = $root.find('[data-tbody]');
-    if ($tbody.find('.handle').length > 0) {
+    if (sortableAvailable && $tbody.find('.handle').length > 0) {
         $tbody.sortable({
             handle: '.handle',
             items:  'tr[data-sw-row]',
@@ -572,7 +580,7 @@
 
     // --- Task reorder (drag) ----------------------------------------------
 
-    if (hasTaskUi && $taskTbody.find('.handle').length > 0) {
+    if (sortableAvailable && hasTaskUi && $taskTbody.find('.handle').length > 0) {
         $taskTbody.sortable({
             handle: '.handle',
             items:  'tr[data-task-row]',

+ 25 - 20
public/assets/js/sprint-settings.js

@@ -200,26 +200,31 @@
             .catch(function (e) { flash(e.message, true); });
     });
 
-    // Drag reorder
-    $inSprint.sortable({
-        handle: '.handle',
-        axis: 'y',
-        placeholder: 'bg-slate-100 h-10',
-        update: function () {
-            const ordering = $inSprint.children('li').map(function (i, el) {
-                return {
-                    sprint_worker_id: parseInt($(el).data('sw-id'), 10),
-                    sort_order: i + 1,
-                };
-            }).get();
-
-            request('POST', '/sprints/' + sprintId + '/workers/reorder', ordering)
-                .then(function (data) {
-                    flash(data.moved ? 'Order saved' : 'No changes');
-                })
-                .catch(function (e) { flash(e.message, true); });
-        },
-    });
+    // Drag reorder (requires jQuery UI)
+    if (typeof $.fn.sortable === 'function') {
+        $inSprint.sortable({
+            handle: '.handle',
+            axis: 'y',
+            placeholder: 'bg-slate-100 h-10',
+            update: function () {
+                const ordering = $inSprint.children('li').map(function (i, el) {
+                    return {
+                        sprint_worker_id: parseInt($(el).data('sw-id'), 10),
+                        sort_order: i + 1,
+                    };
+                }).get();
+
+                request('POST', '/sprints/' + sprintId + '/workers/reorder', ordering)
+                    .then(function (data) {
+                        flash(data.moved ? 'Order saved' : 'No changes');
+                    })
+                    .catch(function (e) { flash(e.message, true); });
+            },
+        });
+    } else {
+        // eslint-disable-next-line no-console
+        console.warn('[sprint-settings] jQuery UI not loaded — drag reorder disabled.');
+    }
 
     function refreshEmptyStates() {
         $root.find('[data-empty-available]').toggle($available.children('li').length === 0);

+ 2 - 6
views/layout.php

@@ -14,14 +14,10 @@ $csrfToken   = $csrfToken   ?? '';
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <title><?= e($title ?? 'Sprint Planner') ?></title>
     <script src="https://cdn.tailwindcss.com"></script>
-    <script src="https://code.jquery.com/jquery-3.7.1.min.js"
-            integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
-            crossorigin="anonymous"></script>
+    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
     <link rel="stylesheet"
           href="https://code.jquery.com/ui/1.13.3/themes/base/jquery-ui.css">
-    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"
-            integrity="sha256-sw0iNNXmOJbQhYFuC9OF2kOlD5KQKe1y5lfBn4C4Sjg="
-            crossorigin="anonymous"></script>
+    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"></script>
 </head>
 <body class="bg-slate-50 text-slate-900 antialiased">
     <header class="border-b bg-white">