Changeset 3469660
- Timestamp:
- 02/25/2026 05:57:48 PM (4 weeks ago)
- Location:
- qa-assistant
- Files:
-
- 52 added
- 30 edited
- 1 copied
-
tags/2.0.0 (copied) (copied from qa-assistant/trunk)
-
tags/2.0.0/assets/css/admin.css (modified) (5 diffs)
-
tags/2.0.0/assets/js/admin.js (modified) (17 diffs)
-
tags/2.0.0/composer.json (modified) (2 diffs)
-
tags/2.0.0/includes/Admin/AdminBar.php (added)
-
tags/2.0.0/includes/Admin/Menu.php (modified) (7 diffs)
-
tags/2.0.0/includes/Ajax.php (modified) (4 diffs)
-
tags/2.0.0/includes/Assets.php (modified) (5 diffs)
-
tags/2.0.0/includes/GitManager.php (modified) (2 diffs)
-
tags/2.0.0/includes/capabilities.php (added)
-
tags/2.0.0/postcss.config.js (added)
-
tags/2.0.0/qa-assistant.php (modified) (9 diffs)
-
tags/2.0.0/readme.txt (modified) (2 diffs)
-
tags/2.0.0/src (added)
-
tags/2.0.0/src/QAAssistantDashboard.jsx (added)
-
tags/2.0.0/src/git-drawer (added)
-
tags/2.0.0/src/git-drawer/App.jsx (added)
-
tags/2.0.0/src/git-drawer/components (added)
-
tags/2.0.0/src/git-drawer/components/BranchList.jsx (added)
-
tags/2.0.0/src/git-drawer/components/Header.jsx (added)
-
tags/2.0.0/src/git-drawer/components/LoadingSpinner.jsx (added)
-
tags/2.0.0/src/git-drawer/components/RepositoryList.jsx (added)
-
tags/2.0.0/src/git-drawer/components/SearchInput.jsx (added)
-
tags/2.0.0/src/git-drawer/components/ToastContainer.jsx (added)
-
tags/2.0.0/src/git-drawer/components/UncommittedModal.jsx (added)
-
tags/2.0.0/src/git-drawer/context (added)
-
tags/2.0.0/src/git-drawer/context/DrawerContext.jsx (added)
-
tags/2.0.0/src/git-drawer/hooks (added)
-
tags/2.0.0/src/git-drawer/hooks/useDebounce.js (added)
-
tags/2.0.0/src/git-drawer/index.js (added)
-
tags/2.0.0/src/git-drawer/main.css (added)
-
tags/2.0.0/src/git-drawer/utils (added)
-
tags/2.0.0/src/git-drawer/utils/api.js (added)
-
tags/2.0.0/src/index.js (added)
-
tags/2.0.0/src/main.css (added)
-
tags/2.0.0/tailwind.config.js (added)
-
tags/2.0.0/templates/settings-page.php (modified) (1 diff)
-
tags/2.0.0/vendor/composer/autoload_classmap.php (modified) (1 diff)
-
tags/2.0.0/vendor/composer/autoload_static.php (modified) (3 diffs)
-
tags/2.0.0/vendor/composer/installed.json (modified) (1 diff)
-
tags/2.0.0/vendor/composer/installed.php (modified) (2 diffs)
-
tags/2.0.0/vendor/composer/platform_check.php (modified) (1 diff)
-
trunk/assets/css/admin.css (modified) (5 diffs)
-
trunk/assets/js/admin.js (modified) (17 diffs)
-
trunk/composer.json (modified) (2 diffs)
-
trunk/includes/Admin/AdminBar.php (added)
-
trunk/includes/Admin/Menu.php (modified) (7 diffs)
-
trunk/includes/Ajax.php (modified) (4 diffs)
-
trunk/includes/Assets.php (modified) (5 diffs)
-
trunk/includes/GitManager.php (modified) (2 diffs)
-
trunk/includes/capabilities.php (added)
-
trunk/postcss.config.js (added)
-
trunk/qa-assistant.php (modified) (9 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src (added)
-
trunk/src/QAAssistantDashboard.jsx (added)
-
trunk/src/git-drawer (added)
-
trunk/src/git-drawer/App.jsx (added)
-
trunk/src/git-drawer/components (added)
-
trunk/src/git-drawer/components/BranchList.jsx (added)
-
trunk/src/git-drawer/components/Header.jsx (added)
-
trunk/src/git-drawer/components/LoadingSpinner.jsx (added)
-
trunk/src/git-drawer/components/RepositoryList.jsx (added)
-
trunk/src/git-drawer/components/SearchInput.jsx (added)
-
trunk/src/git-drawer/components/ToastContainer.jsx (added)
-
trunk/src/git-drawer/components/UncommittedModal.jsx (added)
-
trunk/src/git-drawer/context (added)
-
trunk/src/git-drawer/context/DrawerContext.jsx (added)
-
trunk/src/git-drawer/hooks (added)
-
trunk/src/git-drawer/hooks/useDebounce.js (added)
-
trunk/src/git-drawer/index.js (added)
-
trunk/src/git-drawer/main.css (added)
-
trunk/src/git-drawer/utils (added)
-
trunk/src/git-drawer/utils/api.js (added)
-
trunk/src/index.js (added)
-
trunk/src/main.css (added)
-
trunk/tailwind.config.js (added)
-
trunk/templates/settings-page.php (modified) (1 diff)
-
trunk/vendor/composer/autoload_classmap.php (modified) (1 diff)
-
trunk/vendor/composer/autoload_static.php (modified) (3 diffs)
-
trunk/vendor/composer/installed.json (modified) (1 diff)
-
trunk/vendor/composer/installed.php (modified) (2 diffs)
-
trunk/vendor/composer/platform_check.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
qa-assistant/tags/2.0.0/assets/css/admin.css
r3370854 r3469660 1 /* Enhanced Git Branch Dropdown Styles */ 2 .qa_assistant_git-branch .ab-sub-wrapper { 3 width: auto; 4 height: 320px !important; 5 overflow-y: auto; 6 border-radius: 12px !important; 7 box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1) !important; 1 /* 2 * QA Assistant Admin Bar Styles 3 * Theme: Tailwind/Shadcn inspired (Slate 50-900) 4 */ 5 6 /* --- Dropdown Container --- */ 7 #wpadminbar .qa_assistant_git-branch-group .ab-sub-wrapper, 8 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper, 9 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper { 10 background: #ffffff !important; 8 11 border: 1px solid #e2e8f0 !important; 9 background: #ffffff !important; 10 backdrop-filter: blur(10px) !important; 11 } 12 13 /* Custom scrollbar for dropdown */ 14 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar { 15 width: 6px; 16 } 17 18 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar-track { 19 background: #f1f5f9; 20 border-radius: 3px; 21 } 22 23 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar-thumb { 24 background: linear-gradient(135deg, #cbd5e1, #94a3b8); 25 border-radius: 3px; 26 transition: background 0.2s ease; 27 } 28 29 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar-thumb:hover { 30 background: linear-gradient(135deg, #94a3b8, #64748b); 31 } 32 33 /* Enhanced branch item styling */ 34 .qa_assistant_git-branch-list-items { 35 position: relative; 36 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 37 margin: 2px 4px !important; 12 /* Slate-200 */ 38 13 border-radius: 8px !important; 39 overflow: hidden !important; 40 } 41 42 .qa_assistant_git-branch-list-items:hover > div { 43 cursor: pointer !important; 44 border: 1px solid #e5e7eb !important; 45 background: linear-gradient(135deg, #f9fafb, #f3f4f6) !important; 46 transform: translateX(2px) !important; 47 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08) !important; 48 border-left: 3px solid #3b82f6 !important; 49 } 50 51 .qa_assistant_git-branch-list-items:hover .ab-item { 52 color: #1f2937 !important; 53 font-weight: 600 !important; 54 } 55 56 /* Enhanced hover effect for regular branch items (excluding current branch) */ 57 .qa_assistant_git-branch-list-items:not(.current-branch):hover > div { 58 background: linear-gradient(135deg, #f8fafc, #f1f5f9) !important; 59 border: 1px solid #e2e8f0 !important; 60 border-left: 3px solid #3b82f6 !important; 61 transform: translateX(2px) !important; 62 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1) !important; 63 } 64 65 /* Enhanced current branch indicator */ 66 .qa_assistant_git-branch-list-items.current-branch > div { 67 background: linear-gradient(135deg, #dcfce7, #bbf7d0) !important; 68 border: 1px solid #22c55e !important; 69 border-left: 4px solid #16a34a !important; 70 font-weight: 600 !important; 71 color: #15803d !important; 72 position: relative !important; 73 box-shadow: 0 2px 8px rgba(34, 197, 94, 0.15) !important; 74 } 75 76 .qa_assistant_git-branch-list-items.current-branch .ab-item { 77 color: #15803d !important; 78 font-weight: 600 !important; 79 } 80 81 /* Enhanced hover effect for current branch */ 82 .qa_assistant_git-branch-list-items.current-branch:hover > div { 83 background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important; 84 border: 1px solid #22c55e !important; 85 border-left: 4px solid #16a34a !important; 86 transform: translateX(2px) !important; 87 box-shadow: 0 3px 8px rgba(34, 197, 94, 0.2) !important; 88 } 89 90 .qa_assistant_git-branch-list-items.current-branch:hover .ab-item { 91 color: #14532d !important; 92 } 93 94 .qa_assistant_git-branch-list-items.current-branch > div::before { 95 content: "✓"; 96 color: #16a34a; 97 font-weight: 700; 98 margin-right: 8px; 99 font-size: 14px; 100 filter: drop-shadow(0 1px 2px rgba(22, 163, 74, 0.3)); 101 } 102 103 .qa_assistant_git-branch-list-items.current-branch > div::after { 104 content: "CURRENT"; 105 position: absolute; 106 right: 8px; 107 top: 50%; 108 transform: translateY(-50%); 109 background: #16a34a; 110 color: white; 111 font-size: 9px; 112 font-weight: 700; 113 padding: 2px 6px; 114 border-radius: 4px; 115 letter-spacing: 0.5px; 116 } 117 118 /* Branch switching state */ 119 .qa_assistant_git-branch-list-items.switching-branch { 120 opacity: 0.6; 121 pointer-events: none; 122 } 123 124 .qa_assistant_git-branch-list-items.switching-branch > div { 125 background-color: rgba(255, 193, 7, 0.2)!important; 126 border-left: 3px solid #ffc107!important; 127 } 128 129 .qa_assistant_git-branch-list-items.switching-branch .ab-item { 130 color: #92400e !important; 131 font-weight: 600 !important; 132 } 133 134 /* Enhanced loader with SVG spinner */ 135 .qa-branch-loader { 136 display: inline-block; 137 margin-left: 8px; 138 width: 16px; 139 height: 16px; 140 } 141 142 .qa-spinner { 143 width: 16px; 144 height: 16px; 145 animation: qa-spin 1s linear infinite; 146 } 147 148 .qa-spinner-path { 149 animation: qa-spinner-dash 1.5s ease-in-out infinite; 150 } 151 152 @keyframes qa-spin { 153 0% { transform: rotate(0deg); } 154 100% { transform: rotate(360deg); } 155 } 156 157 @keyframes qa-spinner-dash { 158 0% { 159 stroke-dasharray: 1, 150; 160 stroke-dashoffset: 0; 161 } 162 50% { 163 stroke-dasharray: 90, 150; 164 stroke-dashoffset: -35; 165 } 166 100% { 167 stroke-dasharray: 90, 150; 168 stroke-dashoffset: -124; 169 } 170 } 171 172 /* Enhanced keyboard-based branch search styling */ 173 .qa-branch-search-hint { 174 background: linear-gradient(135deg, #f8fafc, #e2e8f0) !important; 175 border: 1px solid #cbd5e1 !important; 176 border-radius: 6px !important; 177 margin: 4px !important; 178 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; 179 cursor: default !important; 180 pointer-events: none !important; 181 position: relative !important; 182 overflow: hidden !important; 183 } 184 185 .qa-branch-search-hint::before { 186 content: ""; 187 position: absolute; 188 top: 0; 189 left: 0; 190 right: 0; 191 height: 2px; 192 background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4) !important; 193 animation: qa-search-gradient 3s ease-in-out infinite !important; 194 } 195 196 .qa-branch-search-hint .ab-item { 197 padding: 12px 16px !important; 198 font-size: 13px !important; 199 color: #475569 !important; 200 font-weight: 500 !important; 201 font-style: normal !important; 202 } 203 204 /* Remove duplicate search icon - original text already has 🔍 */ 205 206 @keyframes qa-search-gradient { 207 0%, 100% { transform: translateX(-100%); } 208 50% { transform: translateX(100%); } 209 } 210 211 /* Enhanced branch items styling */ 212 .qa_assistant_git-branch .ab-sub-wrapper .qa_assistant_git-branch-list-items .ab-item { 213 padding: 10px 12px !important; 214 font-size: 13px !important; 215 line-height: 1.4 !important; 216 border: none !important; 217 margin: 0 !important; 218 transition: all 0.2s ease !important; 219 color: #374151 !important; 220 font-weight: 500 !important; 221 } 222 223 /* Consistent spacing for all branch dropdown items */ 224 .qa_assistant_git-branch .ab-sub-wrapper .ab-item { 225 padding: 10px 12px !important; 226 font-size: 13px !important; 227 line-height: 1.4 !important; 228 white-space: nowrap !important; 229 overflow: hidden !important; 230 text-overflow: ellipsis !important; 231 color: #374151 !important; 232 font-weight: 500 !important; 233 } 234 235 /* Remove any conflicting margins/padding */ 236 .qa_assistant_git-branch .ab-sub-wrapper li { 237 margin: 0 !important; 238 padding: 0 !important; 239 } 240 241 /* Ensure all branch items have good text color visibility */ 242 .qa_assistant_git-branch .ab-sub-wrapper .ab-item, 243 .qa_assistant_git-branch .ab-sub-wrapper a { 244 color: #374151 !important; 245 text-decoration: none !important; 246 } 247 248 /* Override WordPress admin bar default colors */ 249 .qa_assistant_git-branch .ab-sub-wrapper .ab-item:hover, 250 .qa_assistant_git-branch .ab-sub-wrapper a:hover { 251 color: #1e40af !important; 252 } 253 254 /* Ensure consistent width for dropdown */ 255 .qa_assistant_git-branch .ab-sub-wrapper { 256 min-width: 220px !important; 257 max-width: 300px !important; 258 } 259 260 /* Highlight matching characters during search */ 261 .qa-branch-highlight { 262 background-color: #fff3cd !important; 263 color: #856404 !important; 264 font-weight: bold !important; 265 } 266 267 /* Hidden branches during search */ 268 .qa-branch-hidden { 269 display: none !important; 270 } 271 272 /* Enhanced search active state */ 273 .qa-branch-search-active .qa-branch-search-hint { 274 background: linear-gradient(135deg, #dbeafe, #bfdbfe) !important; 275 border-color: #3b82f6 !important; 276 box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15) !important; 277 } 278 279 .qa-branch-search-active .qa-branch-search-hint::before { 280 background: linear-gradient(90deg, #1d4ed8, #3b82f6, #06b6d4) !important; 281 animation: qa-search-active-gradient 2s ease-in-out infinite !important; 282 } 283 284 .qa-branch-search-active .qa-branch-search-hint .ab-item { 285 color: #1e40af !important; 286 font-weight: 600 !important; 287 } 288 289 /* Remove duplicate active search icon - will keep original 🔍 */ 290 291 @keyframes qa-search-active-gradient { 292 0%, 100% { transform: translateX(-100%) scaleX(1); } 293 50% { transform: translateX(100%) scaleX(1.2); } 294 } 295 296 /* Enhanced blinking cursor animation */ 297 .qa-search-cursor { 298 display: inline-block; 299 margin-left: 4px; 300 animation: qa-cursor-blink 1.2s infinite; 301 font-weight: 600 !important; 14 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; 15 padding: 6px !important; 16 min-width: 260px !important; 17 max-width: 320px !important; 18 } 19 20 /* --- Menu Items General --- */ 21 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper .ab-item, 22 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper a.ab-item, 23 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper .ab-item, 24 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper a.ab-item { 302 25 color: #64748b !important; 303 font-size: 14px !important; 304 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important; 305 } 306 307 @keyframes qa-cursor-blink { 308 0%, 45% { 309 opacity: 1; 310 transform: scaleY(1); 311 } 312 46%, 100% { 313 opacity: 0; 314 transform: scaleY(0.8); 315 } 316 } 317 318 /* Enhanced cursor styling in different states */ 319 .qa-branch-search-hint .qa-search-cursor { 320 color: #64748b !important; 321 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; 322 } 323 324 .qa-branch-search-active .qa-search-cursor { 325 color: #1e40af !important; 326 text-shadow: 0 1px 3px rgba(30, 64, 175, 0.3) !important; 327 animation: qa-cursor-active-blink 1s infinite !important; 328 } 329 330 @keyframes qa-cursor-active-blink { 331 0%, 40% { 332 opacity: 1; 333 transform: scaleY(1) scaleX(1); 334 } 335 41%, 100% { 336 opacity: 0; 337 transform: scaleY(0.9) scaleX(1.1); 338 } 339 } 340 341 /* Modern minimal pull button styling */ 342 .qa-pull-button { 343 background: #ffffff !important; 344 border: 1px solid #e5e7eb !important; 345 border-radius: 6px !important; 346 color: #374151 !important; 347 font-weight: 500 !important; 348 transition: all 0.2s ease !important; 349 margin: 4px !important; 350 position: relative !important; 351 overflow: hidden !important; 352 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; 353 } 354 355 .qa-pull-button::before { 356 content: ""; 357 position: absolute; 358 top: 0; 359 left: -100%; 360 width: 100%; 361 height: 100%; 362 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important; 363 transition: left 0.5s ease !important; 364 } 365 366 .qa-pull-button .ab-item { 367 padding: 10px 14px !important; 368 color: #374151 !important; 26 /* Slate-500 */ 369 27 font-size: 13px !important; 370 28 font-weight: 500 !important; 371 position: relative !important; 372 z-index: 1 !important; 29 padding: 6px 10px !important; 30 border-radius: 6px !important; 31 transition: all 0.15s ease !important; 32 background: transparent !important; 33 height: auto !important; 34 line-height: 1.5 !important; 35 margin-bottom: 2px !important; 373 36 display: flex !important; 374 37 align-items: center !important; … … 376 39 } 377 40 378 .qa-pull-button:hover { 379 background: #f9fafb !important; 380 border-color: #10b981 !important; 381 transform: translateY(-1px) !important; 382 box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15) !important; 383 } 384 385 .qa-pull-button:hover .ab-item { 386 color: #10b981 !important; 387 } 388 389 .qa-pull-button:active { 390 transform: translateY(0px) !important; 391 box-shadow: 0 1px 3px rgba(16, 185, 129, 0.2) !important; 392 } 393 394 /* Modern loading and disabled states */ 395 .qa-pull-button[disabled], 396 .qa-pull-button.qa-pull-loading { 397 background: #f3f4f6 !important; 398 border-color: #d1d5db !important; 399 cursor: not-allowed !important; 400 transform: none !important; 401 box-shadow: none !important; 402 opacity: 0.7 !important; 403 } 404 405 .qa-pull-button.qa-pull-loading .ab-item { 406 color: #6b7280 !important; 407 } 408 409 /* Enhanced pull loader animation */ 410 .qa-pull-loader { 411 display: inline-block; 412 animation: qa-pull-spin 1.2s linear infinite; 413 margin-right: 6px; 414 font-size: 16px !important; 415 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)) !important; 416 } 417 418 @keyframes qa-pull-spin { 419 0% { transform: rotate(0deg); } 420 100% { transform: rotate(360deg); } 421 } 422 423 @keyframes qa-loading-shimmer { 424 0% { left: -100%; } 425 50% { left: 0%; } 426 100% { left: 100%; } 427 } 428 429 /* Enhanced switching state */ 430 .qa_assistant_git-branch-list-items.switching-branch > div { 431 background-color: rgba(255, 193, 7, 0.3) !important; 432 border-left: 3px solid #ffc107 !important; 433 position: relative !important; 434 } 435 436 .qa_assistant_git-branch-list-items.switching-branch > div::after { 437 content: ""; 438 position: absolute; 439 top: 0; 440 left: 0; 441 right: 0; 442 bottom: 0; 443 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); 444 animation: qa-switching-shimmer 1.5s infinite; 445 } 446 447 @keyframes qa-switching-shimmer { 448 0% { transform: translateX(-100%); } 449 100% { transform: translateX(100%); } 450 } 451 452 /* Modern minimal refresh button styling */ 453 .qa-refresh-button { 454 background: #ffffff !important; 455 border: 1px solid #e5e7eb !important; 456 border-radius: 6px !important; 457 color: #374151 !important; 458 font-weight: 500 !important; 459 transition: all 0.2s ease !important; 460 margin: 4px !important; 461 position: relative !important; 462 overflow: hidden !important; 463 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; 464 } 465 466 .qa-refresh-button::before { 467 content: ""; 468 position: absolute; 469 top: 0; 470 left: -100%; 471 width: 100%; 472 height: 100%; 473 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important; 474 transition: left 0.5s ease !important; 475 } 476 477 .qa-refresh-button .ab-item { 478 padding: 10px 14px !important; 479 color: #374151 !important; 480 font-size: 13px !important; 481 font-weight: 500 !important; 482 position: relative !important; 483 z-index: 1 !important; 41 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper .ab-item:hover, 42 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper a.ab-item:hover, 43 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper .ab-item:hover, 44 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper a.ab-item:hover { 45 background-color: #f1f5f9 !important; 46 /* Slate-100 */ 47 color: #0f172a !important; 48 /* Slate-900 */ 49 } 50 51 /* --- Key Elements --- */ 52 53 /* Root Icon */ 54 .qa-admin-bar-icon { 55 display: inline-flex !important; 56 align-items: center !important; 57 margin-right: 4px !important; 58 color: #a1a1aa !important; 59 /* Zinc-400 */ 60 margin-top: 2px !important; 61 /* Visual alignment fix */ 62 } 63 64 #wpadminbar:hover .qa-admin-bar-icon { 65 color: #fff !important; 66 } 67 68 /* Parent Item Adjustment */ 69 #wpadminbar .qa-admin-bar-root>.ab-item, 70 #wpadminbar .qa_assistant_git-branch>.ab-item { 484 71 display: flex !important; 485 72 align-items: center !important; 486 gap: 8px !important; 487 } 488 489 .qa-refresh-button:hover { 490 background: #f9fafb !important; 491 border-color: #3b82f6 !important; 492 transform: translateY(-1px) !important; 493 box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15) !important; 494 } 495 496 .qa-refresh-button:hover .ab-item { 497 color: #3b82f6 !important; 498 } 499 500 .qa-refresh-button:active { 501 transform: translateY(0px) !important; 502 box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2) !important; 503 } 504 505 /* Modern refresh loading and disabled states */ 506 .qa-refresh-button[disabled], 507 .qa-refresh-button.qa-refresh-loading { 508 background: #f3f4f6 !important; 509 border-color: #d1d5db !important; 510 cursor: not-allowed !important; 511 transform: none !important; 512 box-shadow: none !important; 513 opacity: 0.7 !important; 514 } 515 516 .qa-refresh-button.qa-refresh-loading .ab-item { 517 color: #6b7280 !important; 518 } 519 520 /* Enhanced refresh loader animation */ 521 .qa-refresh-loader { 522 display: inline-block; 523 animation: qa-refresh-spin 1.2s linear infinite; 524 margin-right: 6px; 525 font-size: 16px !important; 526 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)) !important; 527 } 528 529 @keyframes qa-refresh-spin { 530 0% { transform: rotate(0deg); } 531 100% { transform: rotate(360deg); } 532 } 533 534 /* Modern sleek icon styling */ 535 .qa-icon { 536 width: 16px !important; 537 height: 16px !important; 538 display: inline-block !important; 539 vertical-align: middle !important; 540 margin-left: 8px !important; 541 transition: all 0.2s ease !important; 542 flex-shrink: 0 !important; 543 } 544 545 .qa-pull-icon { 546 stroke: currentColor !important; 547 stroke-width: 2 !important; 548 } 549 550 .qa-refresh-icon { 551 stroke: currentColor !important; 552 stroke-width: 2 !important; 553 } 554 555 /* Icon hover animations */ 556 .qa-pull-button:hover .qa-pull-icon { 557 transform: translateY(1px) !important; 558 stroke: #10b981 !important; 559 } 560 561 .qa-refresh-button:hover .qa-refresh-icon { 562 transform: rotate(90deg) !important; 563 stroke: #3b82f6 !important; 564 } 565 566 /* Loading state icon animations */ 567 .qa-pull-button.qa-pull-loading .qa-pull-icon { 568 animation: qa-pull-bounce 1s ease-in-out infinite !important; 569 } 570 571 .qa-refresh-button.qa-refresh-loading .qa-refresh-icon { 572 animation: qa-refresh-spin 1s linear infinite !important; 573 } 574 575 @keyframes qa-pull-bounce { 576 0%, 100% { transform: translateY(0px); } 577 50% { transform: translateY(-2px); } 578 } 579 580 /* Settings Page Styles */ 581 .qa-assistant-content { 582 max-width: 800px; 583 margin: 2rem 0; 584 padding: 2rem; 585 background: #fff; 586 border-radius: 8px; 587 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 588 } 589 590 .qa-assistant-content h1 { 591 font-size: 2rem; 73 } 74 75 /* Fix arrow alignment on parent items */ 76 #wpadminbar .qa-admin-bar-root>.ab-item::after, 77 #wpadminbar .qa_assistant_git-branch>.ab-item::after { 78 top: 50% !important; 79 transform: translateY(-50%) !important; 80 margin-top: 0 !important; 81 } 82 83 /* Repo Name & Badge */ 84 .qa-repo-name { 85 color: #0f172a !important; 86 /* Slate-900 */ 87 font-weight: 600 !important; 88 margin-right: auto !important; 89 } 90 91 .qa-branch-badge { 92 background: #f1f5f9 !important; 93 /* Slate-100 */ 94 color: #475569 !important; 95 /* Slate-600 */ 96 font-size: 11px !important; 97 padding: 2px 6px !important; 98 border-radius: 4px !important; 99 font-family: monospace !important; 100 border: 1px solid #e2e8f0 !important; 101 } 102 103 /* --- Toolbar (Pull & Refresh) --- */ 104 #wpadminbar .qa-toolbar-container { 105 padding: 8px 8px 8px 8px; 106 border-bottom: 1px solid #e2e8f0; 107 /* Slate-200 */ 108 background-color: #ffffff; 109 /* Slate-50 */ 110 border-radius: 8px 8px 0 0; 111 margin-bottom: 4px; 112 } 113 114 #wpadminbar .qa-branch-toolbar { 115 display: flex; 116 align-items: center; 117 gap: 6px; 118 } 119 120 #wpadminbar .qa-toolbar-btn { 121 flex: 1; 122 display: flex; 123 align-items: center; 124 justify-content: center; 125 gap: 6px; 126 background: #f8fafc; 127 /* Subtle background */ 128 border: 1px solid #e2e8f0; 129 /* Define border */ 130 cursor: pointer; 131 color: #475569; 132 /* Slate-600 */ 133 font-size: 12px; 134 font-weight: 500; 135 padding: 6px 12px; 136 border-radius: 6px; 137 transition: all 0.2s; 138 line-height: 1; 139 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 140 } 141 142 #wpadminbar .qa-toolbar-btn:hover { 143 color: #0f172a; 144 /* Slate-900 */ 145 background-color: #ffffff; 146 border-color: #cbd5e1; 147 box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 148 } 149 150 #wpadminbar .qa-toolbar-btn:active { 151 background-color: #f1f5f9; 152 box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.05); 153 } 154 155 #wpadminbar .qa-toolbar-btn svg { 156 display: block; 157 color: #64748b; 158 } 159 160 #wpadminbar .qa-toolbar-btn:hover svg { 161 color: #3b82f6; 162 /* Blue-500 hover */ 163 } 164 165 /* Loading state for buttons */ 166 #wpadminbar .qa-toolbar-btn.qa-pull-loading, 167 #wpadminbar .qa-toolbar-btn.qa-refresh-loading { 168 opacity: 0.7; 169 cursor: wait; 170 background-color: #f1f5f9; 171 } 172 173 /* --- Search Input --- */ 174 #wpadminbar .qa-search-node { 175 padding: 0 8px 8px 8px; 176 background-color: #ffffff; 177 border-bottom: 1px solid #f1f5f9; 178 /* Slate-100 */ 179 } 180 181 #wpadminbar .qa-search-container { 182 position: relative; 183 display: flex; 184 align-items: center; 185 } 186 187 #wpadminbar .qa-search-icon { 188 position: absolute; 189 left: 10px; 190 color: #94a3b8; 191 /* Slate-400 */ 192 pointer-events: none; 193 width: 14px; 194 height: 14px; 195 } 196 197 #wpadminbar .qa-branch-search-input { 198 width: 100%; 199 padding: 6px 10px 6px 30px; 200 /* Left padding for icon */ 201 font-size: 13px; 202 line-height: 1.5; 203 color: #0f172a; 204 /* Slate-900 */ 205 background-color: #ffffff; 206 border: 1px solid #e2e8f0; 207 /* Slate-200 */ 208 border-radius: 6px; 209 transition: all 0.2s; 210 outline: none; 211 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 212 height: 32px; 213 min-height: 32px; 214 } 215 216 #wpadminbar .qa-branch-search-input:focus { 217 border-color: #3b82f6; 218 /* Blue-500 */ 219 box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); 220 } 221 222 #wpadminbar .qa-branch-search-input::placeholder { 223 color: #94a3b8; 224 /* Slate-400 */ 225 } 226 227 /* --- Scrollable List --- */ 228 #wpadminbar .qa-branch-list-scrollable, 229 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper { 230 max-height: 300px !important; 231 overflow-y: auto !important; 232 overflow-x: hidden; 233 padding-top: 4px; 234 padding-bottom: 4px; 235 border-radius: 0 0 8px 8px; 236 } 237 238 /* --- Branch Items --- */ 239 #wpadminbar .qa_assistant_git-branch-item .ab-item { 240 height: auto !important; 241 padding: 6px 10px !important; 242 line-height: 1.5 !important; 243 border-left: 2px solid transparent !important; 244 /* Prepare for active border */ 245 } 246 247 #wpadminbar .qa-branch-row { 248 display: flex; 249 align-items: center; 250 justify-content: space-between; 251 width: 100%; 252 } 253 254 #wpadminbar .qa-branch-name { 255 font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace; 256 font-size: 12px; 257 color: #475569; 258 /* Slate-600 */ 259 } 260 261 #wpadminbar .qa-branch-row:hover .qa-branch-name { 262 color: #0f172a; 263 /* Slate-900 */ 264 } 265 266 /* Main Branch styling */ 267 #wpadminbar .qa_assistant_git-branch-item.main-branch .qa-branch-name { 592 268 font-weight: 600; 593 color: #23282d; 594 margin-bottom: 1.5rem; 595 line-height: 1.3; 596 } 597 598 .qa-assistant-tabs { 599 border-bottom: 2px solid #e2e4e7; 600 margin-bottom: 2rem; 601 } 602 603 .qa-assistant-tab-link { 604 display: inline-block; 605 padding: 0.75rem 1.5rem; 606 font-size: 1rem; 607 font-weight: 500; 608 color: #646970; 609 text-decoration: none; 610 border-bottom: 2px solid transparent; 611 margin-bottom: -2px; 612 transition: all 0.3s ease; 613 } 614 615 .qa-assistant-tab-link:hover, 616 .qa-assistant-tab-link.active { 617 color: #2271b1; 618 border-bottom-color: #2271b1; 619 } 620 621 .qa-assistant-form { 622 display: flex; 623 flex-direction: column; 624 gap: 1.5rem; 625 } 626 627 .qa-assistant-form h2 { 628 font-size: 1.25rem; 629 font-weight: 500; 630 color: #1d2327; 631 margin: 0; 632 } 633 634 #qa-assistant__description { 635 font-size: 0.9rem; 636 color: #646970; 637 margin: 0.5rem 0; 638 } 639 640 /* Feature info section */ 641 .qa-assistant-feature-info { 642 background: #f8f9fa; 643 border: 1px solid #e2e4e7; 644 border-radius: 6px; 645 padding: 1.5rem; 646 margin: 1.5rem 0; 647 } 648 649 .qa-assistant-feature-info h3 { 650 margin: 0 0 1rem 0; 651 color: #1d2327; 652 font-size: 1.1rem; 653 } 654 655 .qa-assistant-feature-info ul { 656 margin: 0; 657 padding-left: 0; 658 list-style: none; 659 } 660 661 .qa-assistant-feature-info li { 662 margin: 0.75rem 0; 663 padding: 0.5rem 0; 664 border-bottom: 1px solid #e2e4e7; 665 font-size: 0.9rem; 666 line-height: 1.4; 667 } 668 669 .qa-assistant-feature-info li:last-child { 670 border-bottom: none; 671 } 672 673 /* Selected plugins display */ 674 .qa-assistant-selected-plugins { 675 margin-top: 2rem; 676 padding: 1.5rem; 677 background: #fff; 678 border: 1px solid #e2e4e7; 679 border-radius: 6px; 680 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 681 } 682 683 .qa-assistant-selected-plugins h3 { 684 margin: 0 0 1.5rem 0; 685 color: #1d2327; 686 font-size: 1.2rem; 269 color: #334155; 270 } 271 272 /* Current Branch Indicator & Active Row */ 273 #wpadminbar .qa_assistant_git-branch-item.current .ab-item { 274 background-color: #e0f2fe !important; 275 /* Sky-100 (darker than previous slate-50/sky-50) */ 276 border-left-color: #0284c7 !important; 277 /* Sky-600 */ 278 } 279 280 #wpadminbar .qa_assistant_git-branch-item.current .qa-branch-name { 281 color: #0369a1; 282 /* Sky-700 */ 283 font-weight: 700; 284 } 285 286 #wpadminbar .qa-current-indicator { 287 display: inline-flex; 288 align-items: center; 289 padding: 2px 6px; 290 border-radius: 4px; 291 background-color: #e0f2fe; 292 /* Sky-100 */ 293 color: #0369a1; 294 /* Sky-700 */ 295 font-size: 10px; 687 296 font-weight: 600; 688 } 689 690 .qa-selected-plugins-grid { 691 display: grid; 692 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 693 gap: 1rem; 694 } 695 696 .qa-plugin-card { 697 background: #f8f9fa; 698 border: 1px solid #e2e4e7; 699 border-radius: 6px; 700 padding: 1rem; 701 transition: all 0.2s ease; 702 } 703 704 .qa-plugin-card:hover { 705 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 706 transform: translateY(-1px); 707 } 708 709 .qa-plugin-card.has-git { 710 border-left: 4px solid #2ea043; 711 } 712 713 .qa-plugin-card.no-git { 714 border-left: 4px solid #f85149; 715 } 716 717 .qa-plugin-header h4 { 718 margin: 0 0 0.5rem 0; 719 font-size: 1rem; 297 line-height: 1; 298 text-transform: uppercase; 299 letter-spacing: 0.025em; 300 } 301 302 /* Highlight */ 303 .qa-branch-highlight { 304 background-color: #fef08a; 305 /* Yellow-200 */ 306 color: #854d0e; 307 /* Yellow-800 */ 308 padding: 0 2px; 309 border-radius: 2px; 720 310 font-weight: 600; 721 color: #1d2327; 722 } 723 724 .qa-plugin-dir { 725 font-size: 0.85rem; 726 color: #646970; 727 font-family: monospace; 728 background: #e2e4e7; 729 padding: 2px 6px; 730 border-radius: 3px; 731 } 732 733 .qa-plugin-status { 734 margin-top: 0.75rem; 735 } 736 737 .qa-git-status { 738 display: flex; 739 align-items: center; 740 font-size: 0.9rem; 741 font-weight: 500; 742 } 743 744 .qa-git-status .dashicons { 745 margin-right: 0.5rem; 746 font-size: 16px; 747 } 748 749 .qa-git-status.has-git { 750 color: #2ea043; 751 } 752 753 .qa-git-status.no-git { 754 color: #f85149; 755 } 756 757 /* Select2 Enhancements */ 758 .select2-container--default .select2-selection--multiple { 759 border-color: #8c8f94; 760 border-radius: 4px; 761 min-height: 36px; 762 transition: border-color 0.3s ease; 763 width: 100% !important; 764 } 765 766 .select2-container--default.select2-container--focus .select2-selection--multiple { 767 border-color: #2271b1; 768 box-shadow: 0 0 0 1px #2271b1; 769 } 770 771 .select2-container--default .select2-selection--multiple .select2-selection__choice { 772 background-color: #f0f0f1; 773 border: 1px solid #c3c4c7; 774 border-radius: 3px; 775 padding: 4px 8px; 776 margin: 4px; 311 } 312 313 /* Empty State */ 314 .qa-no-branches { 315 padding: 12px !important; 316 text-align: center; 317 color: #94a3b8 !important; 318 font-style: italic; 777 319 font-size: 13px; 778 transition: all 0.2s ease; 779 } 780 781 .select2-container--default .select2-selection--multiple .select2-selection__choice:hover { 782 background-color: #e5e5e5; 783 } 784 785 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { 786 color: #646970; 787 margin-right: 6px; 788 transition: color 0.2s ease; 789 } 790 791 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { 792 color: #d63638; 793 } 794 795 .select2-dropdown { 796 border-color: #8c8f94; 797 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); 798 animation: select2DropdownFade 0.2s ease-in-out; 799 transform-origin: top; 800 } 801 802 @keyframes select2DropdownFade { 803 from { 804 opacity: 0; 805 transform: translateY(-10px); 320 cursor: default; 321 } 322 323 .qa-no-branches:hover { 324 background-color: transparent !important; 325 } 326 327 /* Scrollbar Styling */ 328 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar { 329 width: 6px; 330 } 331 332 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar-track { 333 background: transparent; 334 } 335 336 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar-thumb { 337 background-color: #cbd5e1; 338 /* Slate-300 */ 339 border-radius: 20px; 340 } 341 342 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar-thumb:hover { 343 background-color: #94a3b8; 344 /* Slate-400 */ 345 } 346 347 /* Animations */ 348 @keyframes spin { 349 to { 350 transform: rotate(360deg); 806 351 } 807 to { 808 opacity: 1; 809 transform: translateY(0); 810 } 811 } 812 813 .select2-container { 814 width: 100% !important; 815 } 816 817 .select2-search--dropdown { 818 padding: 8px; 819 transition: all 0.2s ease; 820 } 821 822 .select2-search--dropdown .select2-search__field { 823 padding: 6px; 824 border-radius: 4px; 825 transition: all 0.2s ease; 826 } 827 828 .select2-search--dropdown .select2-search__field:focus { 829 border-color: #2271b1; 830 box-shadow: 0 0 0 1px #2271b1; 831 outline: none; 832 } 833 834 .select2-container--default .select2-results__option--highlighted[aria-selected] { 835 background-color: #2271b1; 836 } 837 838 /* Save Button and Spinner */ 839 .qa-assistant-form .button-primary { 840 align-self: flex-start; 841 margin-top: 1rem; 842 padding: 0.5rem 1.5rem; 843 height: auto; 844 transition: all 0.2s ease; 845 } 846 847 .qa-assistant-form .button-primary:hover { 848 background-color: #135e96; 849 } 850 851 #qa-assistant__spinner { 852 margin-left: 1rem; 853 margin-top: 1.25rem; 854 } 855 856 /* Modern Enhanced Notification System */ 352 } 353 354 .qa-spin { 355 animation: spin 1s linear infinite; 356 } 357 358 /* --- Notification System --- */ 857 359 .qa-notification { 858 360 position: fixed; 859 top: 80px; 860 right: 20px; 361 bottom: 24px; 362 right: 24px; 363 background: #ffffff; 364 border: 1px solid #e2e8f0; 365 border-radius: 8px; 366 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 367 width: 320px; 861 368 z-index: 999999; 862 min-width: 320px; 863 max-width: 420px; 864 background: white; 865 border-radius: 12px; 866 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1); 369 transform: translateX(120%); 370 transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); 867 371 overflow: hidden; 868 transform: translateX(100%);869 opacity: 0;870 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);871 372 } 872 373 873 374 .qa-notification-show { 874 375 transform: translateX(0); 875 opacity: 1;876 376 } 877 377 878 378 .qa-notification-hide { 879 transform: translateX(100%); 880 opacity: 0; 379 transform: translateX(120%); 881 380 } 882 381 … … 884 383 display: flex; 885 384 align-items: flex-start; 886 padding: 16px 20px;385 padding: 16px; 887 386 gap: 12px; 888 387 } … … 892 391 width: 24px; 893 392 height: 24px; 894 border-radius: 50%;895 display: flex;896 align-items: center;897 justify-content: center;898 393 margin-top: 2px; 899 }900 901 .qa-notification-svg {902 width: 16px;903 height: 16px;904 }905 906 .qa-notification-success .qa-notification-icon {907 background: #dcfce7;908 color: #16a34a;909 }910 911 .qa-notification-error .qa-notification-icon {912 background: #fef2f2;913 color: #dc2626;914 }915 916 .qa-notification-info .qa-notification-icon {917 background: #dbeafe;918 color: #2563eb;919 }920 921 .qa-notification-warning .qa-notification-icon {922 background: #fef3c7;923 color: #d97706;924 394 } 925 395 … … 932 402 font-size: 14px; 933 403 font-weight: 600; 934 color: #111827; 935 margin-bottom: 2px; 936 line-height: 1.4; 404 color: #0f172a; 405 margin-bottom: 4px; 937 406 } 938 407 939 408 .qa-notification-message { 940 409 font-size: 13px; 941 color: #6b7280; 942 line-height: 1.4; 943 word-wrap: break-word; 410 color: #475569; 411 line-height: 1.5; 944 412 } 945 413 946 414 .qa-notification-close { 947 415 flex-shrink: 0; 416 width: 20px; 417 height: 20px; 418 padding: 2px; 419 margin: -2px -2px 0 0; 420 background: transparent; 421 border: none; 422 color: #94a3b8; 423 cursor: pointer; 424 border-radius: 4px; 425 transition: all 0.2s; 426 display: flex; 427 align-items: center; 428 justify-content: center; 429 } 430 431 .qa-notification-close:hover { 432 color: #0f172a; 433 background: #f1f5f9; 434 } 435 436 .qa-notification-svg { 437 width: 100%; 438 height: 100%; 439 } 440 441 .qa-notification-success .qa-notification-icon { 442 color: #10b981; 443 } 444 445 .qa-notification-error .qa-notification-icon { 446 color: #ef4444; 447 } 448 449 .qa-notification-warning .qa-notification-icon { 450 color: #f59e0b; 451 } 452 453 .qa-notification-info .qa-notification-icon { 454 color: #3b82f6; 455 } 456 457 .qa-notification-progress { 458 height: 3px; 459 background: #e2e8f0; 460 width: 100%; 461 transform-origin: left; 462 } 463 464 .qa-notification-progress-animate { 465 animation: qa-progress 5s linear forwards; 466 } 467 468 @keyframes qa-progress { 469 0% { 470 transform: scaleX(1); 471 } 472 473 100% { 474 transform: scaleX(0); 475 } 476 } 477 478 .qa-notification-success .qa-notification-progress-animate { 479 background: #10b981; 480 } 481 482 .qa-notification-error .qa-notification-progress-animate { 483 background: #ef4444; 484 } 485 486 .qa-notification-warning .qa-notification-progress-animate { 487 background: #f59e0b; 488 } 489 490 .qa-notification-info .qa-notification-progress-animate { 491 background: #3b82f6; 492 } 493 494 /* --- Action Modal --- */ 495 .qa-action-modal-overlay { 496 position: fixed; 497 top: 0; 498 left: 0; 499 width: 100vw; 500 height: 100vh; 501 background: rgba(15, 23, 42, 0.6); 502 backdrop-filter: blur(4px); 503 display: flex; 504 align-items: center; 505 justify-content: center; 506 z-index: 999999; 507 animation: qaModalFadeIn 0.2s ease-out; 508 } 509 510 .qa-action-modal-content { 511 background: #ffffff; 512 border-radius: 8px; 513 width: 90%; 514 max-width: 450px; 515 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 516 overflow: hidden; 517 display: flex; 518 flex-direction: column; 519 animation: qaModalSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); 520 } 521 522 .qa-action-modal-header { 523 padding: 16px 24px; 524 border-bottom: 1px solid #e2e8f0; 525 display: flex; 526 align-items: center; 527 justify-content: space-between; 528 } 529 530 .qa-action-modal-title { 531 margin: 0; 532 font-size: 16px; 533 font-weight: 600; 534 color: #0f172a; 535 } 536 537 .qa-action-modal-close { 948 538 background: none; 949 539 border: none; 540 color: #64748b; 541 font-size: 24px; 542 line-height: 1; 950 543 cursor: pointer; 951 padding: 4px; 544 padding: 0; 545 display: flex; 546 align-items: center; 547 justify-content: center; 548 transition: color 0.2s; 549 } 550 551 .qa-action-modal-close:hover { 552 color: #0f172a; 553 } 554 555 .qa-action-modal-body { 556 padding: 24px; 557 color: #334155; 558 font-size: 14px; 559 line-height: 1.5; 560 } 561 562 .qa-action-modal-body p { 563 margin-top: 0; 564 margin-bottom: 16px; 565 } 566 567 .qa-commit-section { 568 display: flex; 569 flex-direction: column; 570 gap: 6px; 571 margin-top: 16px; 572 padding-top: 16px; 573 border-top: 1px dashed #cbd5e1; 574 } 575 576 .qa-commit-section label { 577 font-weight: 500; 578 color: #0f172a; 579 font-size: 13px; 580 } 581 582 .qa-commit-input { 583 padding: 8px 12px; 584 border: 1px solid #cbd5e1; 952 585 border-radius: 6px; 953 color: #9ca3af; 954 transition: all 0.2s ease; 955 width: 24px; 956 height: 24px; 957 display: flex; 586 font-size: 14px; 587 width: 100%; 588 box-sizing: border-box; 589 transition: border-color 0.2s, box-shadow 0.2s; 590 } 591 592 .qa-commit-input:focus { 593 outline: none; 594 border-color: #3b82f6; 595 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); 596 } 597 598 .qa-action-modal-footer { 599 padding: 16px 24px; 600 background: #f8fafc; 601 border-top: 1px solid #e2e8f0; 602 display: flex; 603 align-items: center; 604 justify-content: flex-end; 605 gap: 12px; 606 } 607 608 .qa-btn { 609 padding: 8px 16px; 610 border-radius: 6px; 611 font-size: 14px; 612 font-weight: 500; 613 cursor: pointer; 614 border: none; 615 transition: all 0.2s; 616 display: inline-flex; 958 617 align-items: center; 959 618 justify-content: center; 960 619 } 961 620 962 .qa-notification-close:hover { 963 background: #f3f4f6; 964 color: #374151; 965 } 966 967 .qa-notification-close svg { 968 width: 14px; 969 height: 14px; 970 } 971 972 .qa-notification-progress { 973 height: 3px; 974 background: #f3f4f6; 975 position: relative; 976 overflow: hidden; 977 } 978 979 .qa-notification-progress::before { 980 content: ''; 981 position: absolute; 982 top: 0; 983 left: 0; 984 height: 100%; 985 width: 0; 986 transition: width 5s linear; 987 } 988 989 .qa-notification-progress-animate::before { 990 width: 100%; 991 } 992 993 .qa-notification-success .qa-notification-progress::before { 994 background: #16a34a; 995 } 996 997 .qa-notification-error .qa-notification-progress::before { 998 background: #dc2626; 999 } 1000 1001 .qa-notification-info .qa-notification-progress::before { 621 .qa-btn:disabled { 622 opacity: 0.6; 623 cursor: not-allowed; 624 } 625 626 .qa-btn-secondary { 627 background: #ffffff; 628 border: 1px solid #cbd5e1; 629 color: #334155; 630 } 631 632 .qa-btn-secondary:hover:not(:disabled) { 633 background: #f1f5f9; 634 color: #0f172a; 635 } 636 637 .qa-btn-primary { 638 background: #3b82f6; 639 color: #ffffff; 640 } 641 642 .qa-btn-primary:hover:not(:disabled) { 1002 643 background: #2563eb; 1003 644 } 1004 645 1005 .qa-notification-warning .qa-notification-progress::before { 646 .qa-btn-warning { 647 background: #f59e0b; 648 color: #ffffff; 649 } 650 651 .qa-btn-warning:hover:not(:disabled) { 1006 652 background: #d97706; 1007 653 } 1008 654 1009 /* Git Branch Status Icons */ 1010 .qa-git-status-icon { 1011 margin-right: 4px; 1012 font-size: 12px; 1013 } 1014 1015 .qa-git-status-clean::before { 1016 content: "✓"; 1017 color: #2ea043; 1018 } 1019 1020 .qa-git-status-dirty::before { 1021 content: "●"; 1022 color: #fb8500; 1023 } 1024 1025 .qa-git-status-ahead::before { 1026 content: "↑"; 1027 color: #0969da; 1028 } 1029 1030 .qa-git-status-behind::before { 1031 content: "↓"; 1032 color: #cf222e; 1033 } 1034 1035 /* Git Repository Validation Styles */ 1036 .qa-plugin-card.no-git { 1037 border-left: 4px solid #f59e0b; 1038 background-color: #fef3c7; 1039 } 1040 1041 .qa-plugin-card.has-git { 1042 border-left: 4px solid #10b981; 1043 background-color: #d1fae5; 1044 } 1045 1046 .qa-git-status.no-git { 1047 color: #d97706; 1048 font-weight: 600; 1049 } 1050 1051 .qa-git-status.has-git { 1052 color: #059669; 1053 font-weight: 600; 1054 } 1055 1056 .qa-git-status .dashicons { 1057 font-size: 16px; 1058 width: 16px; 1059 height: 16px; 1060 margin-right: 4px; 1061 vertical-align: text-top; 1062 } 1063 1064 /* Enhanced plugin card styling */ 1065 .qa-plugin-card { 1066 padding: 16px; 1067 margin-bottom: 12px; 1068 border-radius: 8px; 1069 border: 1px solid #e5e7eb; 1070 transition: all 0.2s ease; 1071 } 1072 1073 .qa-plugin-card:hover { 1074 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 1075 } 1076 1077 .qa-plugin-header h4 { 1078 margin: 0 0 4px 0; 1079 font-size: 16px; 1080 font-weight: 600; 1081 color: #1f2937; 1082 } 1083 1084 .qa-plugin-dir { 1085 font-size: 12px; 1086 color: #6b7280; 1087 font-family: monospace; 1088 background: #f3f4f6; 1089 padding: 2px 6px; 1090 border-radius: 4px; 1091 } 1092 1093 .qa-plugin-status { 1094 margin-top: 8px; 1095 } 655 @keyframes qaModalFadeIn { 656 from { 657 opacity: 0; 658 } 659 660 to { 661 opacity: 1; 662 } 663 } 664 665 @keyframes qaModalSlideUp { 666 from { 667 opacity: 0; 668 transform: translateY(20px) scale(0.95); 669 } 670 671 to { 672 opacity: 1; 673 transform: translateY(0) scale(1); 674 } 675 } -
qa-assistant/tags/2.0.0/assets/js/admin.js
r3370854 r3469660 1 ;(function($) { 2 3 $(document).ready(function() { 4 $('.qa-assistant-select2').select2(); 1 ; (function ($) { 2 3 $(document).ready(function () { 4 $('.qa-assistant-select2').select2({ 5 width: '100%' 6 }); 7 8 // Tab Switching Logic 9 $('.qa-assistant-tab-link').on('click', function (e) { 10 e.preventDefault(); 11 12 // Remove active class from all links and panes 13 $('.qa-assistant-tab-link').removeClass('active').attr('aria-selected', 'false'); 14 $('.qa-assistant-tab-pane').removeClass('active'); 15 16 // Add active class to clicked link 17 $(this).addClass('active').attr('aria-selected', 'true'); 18 19 // Show corresponding pane 20 let target = $(this).attr('href'); 21 $(target).addClass('active'); 22 }); 23 24 // Clone Repository Form Handling 25 $('#qa-assistant-clone-form').on('submit', function (e) { 26 e.preventDefault(); 27 28 let $form = $(this); 29 let $btn = $('#qa-clone-btn'); 30 let $spinner = $('#qa-clone-spinner'); 31 let $status = $('#qa-clone-status'); 32 let repoUrl = $('#qa-repo-url').val().trim(); 33 // Get the nonce specifically for cloning 34 let cloneNonce = $('#qa_assistant_clone_nonce').val() || qaAssistant.nonce; 35 36 if (!repoUrl) { 37 showNotification('Please enter a repository URL', 'error'); 38 return; 39 } 40 41 // Reset status 42 $status.hide().removeClass('notice-success notice-error notice-warning').html(''); 43 44 // Show loading state 45 $btn.prop('disabled', true); 46 $spinner.addClass('is-active'); 47 48 $.ajax({ 49 url: qaAssistant.ajaxUrl, 50 method: "POST", 51 data: { 52 action: "qa_assistant_clone_repo", 53 nonce: cloneNonce, 54 repo_url: repoUrl 55 }, 56 success: function (response) { 57 if (response.success) { 58 $status.addClass('notice-success') 59 .html(`<p><strong>Success!</strong> ${response.data.message}<br><strong>Next Step:</strong> Please add the plugin "${response.data.repo_name}" to the 'Git Branch Display' list below and save settings to enable admin bar features.</p>`) 60 .show(); 61 62 showNotification('Repository cloned successfully', 'success'); 63 64 // Clear input 65 $('#qa-repo-url').val(''); 66 67 // Reload after a delay to show the new plugin in the list 68 setTimeout(() => { 69 location.reload(); 70 }, 3000); // Increased delay so user can read message 71 } else { 72 let msg = response.data.message || 'Unknown error occurred'; 73 $status.addClass('notice-error') 74 .html(`<p><strong>Error:</strong> ${msg}</p>`) 75 .show(); 76 77 if (response.data.target_exists) { 78 showNotification('Target directory already exists', 'warning'); 79 } else { 80 showNotification(msg, 'error'); 81 } 82 } 83 }, 84 error: function (xhr, status, error) { 85 let errorMsg = 'Network error occurred. Please try again.'; 86 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { 87 errorMsg = xhr.responseJSON.data.message; 88 } 89 90 $status.addClass('notice-error') 91 .html(`<p><strong>Error:</strong> ${errorMsg}</p>`) 92 .show(); 93 94 showNotification(errorMsg, 'error'); 95 }, 96 complete: function () { 97 $btn.prop('disabled', false); 98 $spinner.removeClass('is-active'); 99 } 100 }); 101 }); 5 102 6 103 // Add Git repository validation for plugin selection 7 104 initializeGitValidation(); 8 105 9 // Keyboard-based branch search functionality 10 let searchBuffer = ''; 11 12 // Handle keypress events on branch dropdowns 13 $(document).on('keydown', function(e) { 14 // Only activate when a branch dropdown is open 15 let openDropdown = $('.qa_assistant_git-branch .ab-sub-wrapper:visible'); 16 if (openDropdown.length === 0) return; 17 18 // Handle alphanumeric keys for search 19 if (e.key.length === 1 && e.key.match(/[a-zA-Z0-9\-_]/)) { 20 e.preventDefault(); 21 22 // Add character to search buffer 23 searchBuffer += e.key.toLowerCase(); 24 25 // Perform search 26 performBranchSearch(openDropdown, searchBuffer); 27 } 28 29 // Handle Escape to clear search 30 if (e.key === 'Escape') { 31 searchBuffer = ''; 32 clearBranchSearch(openDropdown); 33 } 34 35 // Handle Backspace to remove last character 36 if (e.key === 'Backspace' && searchBuffer.length > 0) { 37 e.preventDefault(); 38 searchBuffer = searchBuffer.slice(0, -1); 39 if (searchBuffer.length > 0) { 40 performBranchSearch(openDropdown, searchBuffer); 106 // --- NEW SEARCH LOGIC --- 107 108 // Prevent dropdown from closing when clicking inside search or toolbar 109 $(document).on('click', '.qa-branch-search-container, .qa-toolbar-container, .qa-branch-search-input', function (e) { 110 e.stopPropagation(); 111 }); 112 113 // Focus search input when dropdown opens 114 $('.qa_assistant_git-branch').hover(function () { 115 let $input = $(this).find('.qa-branch-search-input'); 116 if ($input.length) { 117 setTimeout(() => $input.focus(), 100); 118 } 119 }); 120 121 // Handle Input Event (Real-time filtering) 122 $(document.body).on('input', '.qa-branch-search-input', function (e) { 123 let searchTerm = $(this).val().toLowerCase().trim(); 124 // console.log('Search input triggered:', searchTerm); 125 126 let $input = $(this); 127 // DOM Structure: 128 // div.ab-sub-wrapper 129 // > ul.ab-submenu (contains Search LI) 130 // > ul.qa-branch-list-scrollable (contains Branch LIs) 131 132 // 1. Find the UL containing the search input 133 let $searchUL = $input.closest('ul.ab-submenu'); 134 135 // 2. Find the sibling UL that contains the branches 136 let $groupContainer = $searchUL.siblings('.qa-branch-list-scrollable'); 137 138 // Fallback: If not found, try to find it within the same .ab-sub-wrapper parent 139 if ($groupContainer.length === 0) { 140 $groupContainer = $input.closest('.ab-sub-wrapper').find('.qa-branch-list-scrollable'); 141 } 142 143 // console.log('Group Container found:', $groupContainer.length); 144 145 let hasMatches = false; 146 147 // Loop through branch items 148 $groupContainer.find('.qa_assistant_git-branch-item').each(function () { 149 let $item = $(this); 150 let branchName = $item.find('.qa-branch-name').text(); // Use text() finding the name span directly is safer 151 152 // If data attribute exists use it, otherwise text 153 if ($item.data('branch-name')) { 154 branchName = $item.data('branch-name'); 155 } 156 157 if (!branchName) return; 158 159 branchName = branchName.toString().toLowerCase(); 160 161 if (branchName.includes(searchTerm)) { 162 $item.show(); 163 hasMatches = true; 164 165 // Highlight logic 166 let originalName = $item.data('branch-name') || $item.find('.qa-branch-name').text(); 167 let highlighted = highlightSearchTerm(originalName, searchTerm); 168 $item.find('.qa-branch-name').html(highlighted); 41 169 } else { 42 clearBranchSearch(openDropdown); 43 } 44 } 45 }); 46 47 function performBranchSearch(dropdown, searchTerm) { 48 let branchContainer = dropdown.closest('.qa_assistant_git-branch'); 49 let searchHint = branchContainer.find('.qa-branch-search-hint'); 50 let hasMatches = false; 51 52 // Update search hint with blinking cursor 53 if (searchHint.length > 0) { 54 searchHint.find('.ab-item').html(`🔍 Searching: "${searchTerm}<span class="qa-search-cursor">|</span>"`); 55 branchContainer.addClass('qa-branch-search-active'); 56 } 57 58 // Filter and highlight branches 59 branchContainer.find('.qa_assistant_git-branch-list-items').each(function() { 60 let $item = $(this); 61 let branchName = $item.find('.ab-item').text().toLowerCase(); 62 63 if (branchName.includes(searchTerm)) { 64 $item.removeClass('qa-branch-hidden').show(); 65 66 // Highlight matching text 67 let originalText = $item.find('.ab-item').text(); 68 let highlightedText = highlightSearchTerm(originalText, searchTerm); 69 $item.find('.ab-item').html(highlightedText); 70 71 hasMatches = true; 170 $item.hide(); 171 } 172 }); 173 174 // Empty state handling 175 let $noResultsMsg = $groupContainer.find('.qa-no-branches-dynamic'); 176 let $serverNoResults = $groupContainer.find('.qa-no-branches'); 177 178 if (!hasMatches) { 179 if ($noResultsMsg.length === 0) { 180 // Check if a server-side one exists 181 if ($serverNoResults.length > 0) { 182 $serverNoResults.show(); 183 } else { 184 // Create dynamic one 185 $groupContainer.append('<li class="qa-no-branches-dynamic ab-item" style="padding: 12px; text-align: center; color: #94a3b8; font-style: italic; list-style: none;">No branches found</li>'); 186 } 72 187 } else { 73 $item.addClass('qa-branch-hidden').hide(); 74 } 75 }); 76 77 // Show "no matches" if needed 78 if (!hasMatches && searchHint.length > 0) { 79 searchHint.find('.ab-item').html(`🔍 No matches for "${searchTerm}<span class="qa-search-cursor">|</span>"`); 80 } 81 } 82 83 function clearBranchSearch(dropdown) { 84 let branchContainer = dropdown.closest('.qa_assistant_git-branch'); 85 let searchHint = branchContainer.find('.qa-branch-search-hint'); 86 87 // Reset search hint 88 if (searchHint.length > 0) { 89 searchHint.find('.ab-item').html('🔍 Type to search branches...<span class="qa-search-cursor">|</span>'); 90 branchContainer.removeClass('qa-branch-search-active'); 91 } 92 93 // Show all branches and remove highlighting 94 branchContainer.find('.qa_assistant_git-branch-list-items').each(function() { 95 let $item = $(this); 96 $item.removeClass('qa-branch-hidden').show(); 97 98 // Remove highlighting 99 let originalText = $item.find('.ab-item').text(); 100 $item.find('.ab-item').text(originalText); 101 }); 102 } 188 $noResultsMsg.show(); 189 } 190 } else { 191 // Hide dynamic message 192 if ($noResultsMsg.length > 0) { 193 $noResultsMsg.hide(); 194 } 195 // Also hide server-side one if it exists 196 if ($serverNoResults.length > 0) { 197 $serverNoResults.hide(); 198 } 199 } 200 }); 103 201 104 202 function highlightSearchTerm(text, searchTerm) { 105 203 if (!searchTerm) return text; 106 107 let regex = new RegExp(`(${searchTerm})`, 'gi'); 204 // Escape special regex chars in search term 205 let safeTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 206 let regex = new RegExp(`(${safeTerm})`, 'gi'); 108 207 return text.replace(regex, '<span class="qa-branch-highlight">$1</span>'); 109 208 } 110 209 111 // Clear search when dropdown closes 112 $(document).on('click', function(e) { 113 if (!$(e.target).closest('.qa_assistant_git-branch').length) { 114 searchBuffer = ''; 115 $('.qa_assistant_git-branch').each(function() { 116 clearBranchSearch($(this).find('.ab-sub-wrapper')); 117 }); 118 } 119 }); 210 // --- END NEW SEARCH LOGIC --- 120 211 121 212 // Enhanced branch switching with immediate feedback 122 $(document).on('click', '.qa_assistant_git-branch- list-items', function(e) {213 $(document).on('click', '.qa_assistant_git-branch-item', function (e) { 123 214 e.preventDefault(); 124 215 … … 161 252 162 253 switchBranch(pluginDir, branchName, false) 163 .done(function (response) {254 .done(function (response) { 164 255 if (response.success) { 165 256 showNotification(`Successfully switched to branch: ${response.data.current_branch}`, 'success'); … … 176 267 } 177 268 }) 178 .fail(function (xhr, status, error) {269 .fail(function (xhr, status, error) { 179 270 showNotification('Network error occurred. Please try again.', 'error'); 180 271 console.error('Branch switch failed:', error); 181 272 }) 182 .always(function () {273 .always(function () { 183 274 // Remove loader and re-enable item 184 275 loader.remove(); … … 195 286 if (confirm(`You have uncommitted changes. Do you want to discard them and switch to ${branchName}?`)) { 196 287 switchBranch(pluginDir, branchName, true) 197 .done(function (response) {288 .done(function (response) { 198 289 if (response.success) { 199 290 showNotification(`Force switched to branch: ${response.data.current_branch}`, 'success'); … … 229 320 230 321 // Remove current-branch class from all items 231 branchContainer.find('.qa_assistant_git-branch- list-items').removeClass('current-branch');322 branchContainer.find('.qa_assistant_git-branch-item').removeClass('current-branch'); 232 323 233 324 // Add current-branch class to the new current branch … … 288 379 289 380 // Manual close 290 notification.find('.qa-notification-close').on('click', function () {381 notification.find('.qa-notification-close').on('click', function () { 291 382 hideNotification(notification); 292 383 }); 384 385 return notification; 293 386 } 294 387 … … 311 404 312 405 // Global function for pull operations (called via onclick) 313 window.qaAssistantPull = function (pluginDir) {314 // Show i mmediate feedback315 showNotification('Pulling latest changes...', 'info');406 window.qaAssistantPull = function (pluginDir) { 407 // Show initial feedback that we can track and hide if needed 408 let pullingNotification = showNotification('Pulling latest changes...', 'info'); 316 409 317 410 // Find the button that was clicked and add loading state 318 let $button = $('.qa-pull-button').filter(function () {411 let $button = $('.qa-pull-button').filter(function () { 319 412 return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir); 320 413 }); … … 326 419 327 420 pullBranch(pluginDir) 328 .done(function (response) {329 if (response .success) {421 .done(function (response) { 422 if (response && response.success) { 330 423 showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success'); 331 424 // Reload page to show updated state 332 425 setTimeout(() => location.reload(), 1500); 333 426 } else { 334 if (response.data.has_changes) { 335 showNotification('You have uncommitted changes. Please commit or stash them before pulling.', 'warning'); 427 if (response && response.data && response.data.has_changes) { 428 hideNotification(pullingNotification); // Hide the info toast 429 showUncommittedChangesModal(pluginDir); 336 430 } else { 337 showNotification(response.data.message || 'Failed to pull changes', 'error'); 431 let errMsg = (response && response.data && response.data.message) ? response.data.message : 'Failed to pull changes'; 432 showNotification(errMsg, 'error'); 338 433 } 339 434 } 340 435 }) 341 .fail(function (xhr, status, error) {436 .fail(function (xhr, status, error) { 342 437 showNotification('Network error occurred during pull. Please try again.', 'error'); 343 438 console.error('Pull failed:', error); 344 439 }) 345 .always(function () {440 .always(function () { 346 441 // Restore button 347 442 $button.find('.ab-item').html(originalText); … … 351 446 // Fallback if button not found 352 447 pullBranch(pluginDir) 353 .done(function (response) {354 if (response .success) {448 .done(function (response) { 449 if (response && response.success) { 355 450 showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success'); 356 451 setTimeout(() => location.reload(), 1500); 357 452 } else { 358 showNotification(response.data.message || 'Failed to pull changes', 'error'); 453 if (response && response.data && response.data.has_changes) { 454 hideNotification(pullingNotification); // Hide the info toast 455 showUncommittedChangesModal(pluginDir); 456 } else { 457 let errMsg = (response && response.data && response.data.message) ? response.data.message : 'Failed to pull changes'; 458 showNotification(errMsg, 'error'); 459 } 359 460 } 360 461 }) 361 .fail(function (xhr, status, error) {462 .fail(function (xhr, status, error) { 362 463 showNotification('Network error occurred during pull. Please try again.', 'error'); 363 464 console.error('Pull failed:', error); … … 394 495 395 496 // Global function for refreshing branches (called via onclick) 396 window.qaAssistantRefresh = function (pluginDir) {497 window.qaAssistantRefresh = function (pluginDir) { 397 498 // Show immediate feedback 398 499 showNotification('Fetching latest branches from remote...', 'info'); 399 500 400 501 // Find the button that was clicked and add loading state 401 let $button = $('.qa-refresh-button').filter(function () {502 let $button = $('.qa-refresh-button').filter(function () { 402 503 return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir); 403 504 }); … … 409 510 410 511 refreshBranches(pluginDir) 411 .done(function (response) {512 .done(function (response) { 412 513 if (response.success) { 413 514 let message = response.data.fetch_success … … 423 524 } 424 525 }) 425 .fail(function (xhr, status, error) {526 .fail(function (xhr, status, error) { 426 527 showNotification('Network error occurred during refresh. Please try again.', 'error'); 427 528 console.error('Refresh failed:', error); 428 529 }) 429 .always(function () {530 .always(function () { 430 531 // Restore button 431 532 $button.find('.ab-item').html(originalText); … … 435 536 // Fallback if button not found 436 537 refreshBranches(pluginDir) 437 .done(function (response) {538 .done(function (response) { 438 539 if (response.success) { 439 540 let message = response.data.fetch_success … … 446 547 } 447 548 }) 448 .fail(function (xhr, status, error) {549 .fail(function (xhr, status, error) { 449 550 showNotification('Network error occurred during refresh. Please try again.', 'error'); 450 551 console.error('Refresh failed:', error); … … 471 572 } 472 573 574 // --- Uncommitted Changes Modal Logic --- 575 function showUncommittedChangesModal(pluginDir) { 576 $('.qa-action-modal').remove(); 577 578 let modalHtml = $(` 579 <div class="qa-action-modal-overlay qa-action-modal" id="qaUncommittedModal"> 580 <div class="qa-action-modal-content"> 581 <div class="qa-action-modal-header"> 582 <h3 class="qa-action-modal-title">Uncommitted Changes Found</h3> 583 <button class="qa-action-modal-close" aria-label="Close modal">×</button> 584 </div> 585 <div class="qa-action-modal-body"> 586 <p>You have uncommitted local changes that prevent pulling. How would you like to proceed?</p> 587 588 <!-- Commit Section --> 589 <div class="qa-commit-section"> 590 <label for="qaCommitMessage">Commit Message:</label> 591 <input type="text" id="qaCommitMessage" class="qa-commit-input" placeholder="e.g. Fixed minor bug"> 592 </div> 593 </div> 594 <div class="qa-action-modal-footer"> 595 <button class="qa-btn qa-btn-secondary qa-close-modal">Cancel</button> 596 <button class="qa-btn qa-btn-warning qa-stash-btn">Stash & Pull</button> 597 <button class="qa-btn qa-btn-primary qa-commit-btn">Commit & Pull</button> 598 </div> 599 </div> 600 </div> 601 `); 602 603 $('body').append(modalHtml); 604 let $modal = modalHtml; 605 606 // Close actions 607 $modal.find('.qa-action-modal-close, .qa-close-modal').on('click', function () { 608 $modal.remove(); 609 }); 610 611 // Stash action 612 $modal.find('.qa-stash-btn').on('click', function () { 613 let $btn = $(this); 614 $btn.prop('disabled', true).text('Stashing...'); 615 $modal.find('.qa-btn').prop('disabled', true); 616 617 stashChanges(pluginDir).done(function (response) { 618 if (response.success) { 619 $modal.remove(); 620 // Proceed with pull 621 showNotification('Pulling latest changes...', 'info'); 622 window.qaAssistantPull(pluginDir); 623 } else { 624 showNotification(response.data.message || 'Failed to stash changes.', 'error'); 625 $modal.find('.qa-btn').prop('disabled', false); 626 $btn.text('Stash & Pull'); 627 } 628 }).fail(function () { 629 showNotification('Network error occurred during stash.', 'error'); 630 $modal.find('.qa-btn').prop('disabled', false); 631 $btn.text('Stash & Pull'); 632 }); 633 }); 634 635 // Commit action 636 $modal.find('.qa-commit-btn').on('click', function () { 637 let message = $modal.find('#qaCommitMessage').val().trim(); 638 if (!message) { 639 showNotification('Please enter a commit message.', 'warning'); 640 $modal.find('#qaCommitMessage').focus(); 641 return; 642 } 643 644 let $btn = $(this); 645 $btn.prop('disabled', true).text('Committing...'); 646 $modal.find('.qa-btn').prop('disabled', true); 647 648 commitChanges(pluginDir, message).done(function (response) { 649 if (response.success) { 650 $modal.remove(); 651 // Proceed with pull 652 showNotification('Pulling latest changes...', 'info'); 653 window.qaAssistantPull(pluginDir); 654 } else { 655 showNotification(response.data.message || 'Failed to commit changes.', 'error'); 656 $modal.find('.qa-btn').prop('disabled', false); 657 $btn.text('Commit & Pull'); 658 } 659 }).fail(function () { 660 showNotification('Network error occurred during commit.', 'error'); 661 $modal.find('.qa-btn').prop('disabled', false); 662 $btn.text('Commit & Pull'); 663 }); 664 }); 665 } 666 667 function stashChanges(pluginDir) { 668 return $.ajax({ 669 url: qaAssistant.ajaxUrl, 670 method: "POST", 671 data: { 672 action: "qa_assistant_stash_changes", 673 nonce: qaAssistant.nonce, 674 plugin_dir: pluginDir 675 } 676 }); 677 } 678 679 function commitChanges(pluginDir, message) { 680 return $.ajax({ 681 url: qaAssistant.ajaxUrl, 682 method: "POST", 683 data: { 684 action: "qa_assistant_commit_changes", 685 nonce: qaAssistant.nonce, 686 plugin_dir: pluginDir, 687 commit_message: message 688 } 689 }); 690 } 691 473 692 }); 474 693 … … 478 697 function initializeGitValidation() { 479 698 // Add change event listener to plugin selection dropdown 480 $('.qa-assistant-select2').on('change', function () {699 $('.qa-assistant-select2').on('change', function () { 481 700 validateSelectedPlugins(); 482 701 }); 483 702 484 703 // Add form submission validation 485 $('.qa-assistant-form').on('submit', function (e) {704 $('.qa-assistant-form').on('submit', function (e) { 486 705 if (!validateSelectedPlugins()) { 487 706 e.preventDefault(); … … 499 718 500 719 // Check each selected plugin 501 selectedValues.forEach(function (pluginDir) {720 selectedValues.forEach(function (pluginDir) { 502 721 let pluginCard = $(`.qa-plugin-card[data-plugin-dir="${pluginDir}"]`); 503 722 if (pluginCard.length > 0) { -
qa-assistant/tags/2.0.0/composer.json
r3370854 r3469660 12 12 "minimum-stability": "stable", 13 13 "require": { 14 "php": ">=8.0", 14 15 "czproject/git-php": "^4.0" 15 16 }, … … 18 19 "QaAssistant\\": "includes/" 19 20 }, 20 "files": [ "includes/functions.php" ] 21 "files": [ 22 "includes/functions.php" 23 ] 21 24 } 22 25 } -
qa-assistant/tags/2.0.0/includes/Admin/Menu.php
r3370854 r3469660 11 11 * The Menu handler class 12 12 */ 13 class Menu { 13 class Menu 14 { 14 15 15 16 /** 16 17 * Initialize the class 17 18 */ 18 function __construct( ) { 19 add_action( 'admin_menu', [ $this, 'admin_menu' ] ); 19 function __construct() 20 { 21 add_action('admin_menu', [$this, 'admin_menu']); 20 22 } 21 23 … … 25 27 * @return void 26 28 */ 27 public function admin_menu() { 29 public function admin_menu() 30 { 28 31 $parent_slug = 'qa-assistant'; 29 $capability = apply_filters('qa -assistant/menu/capability', 'manage_options');32 $capability = apply_filters('qa_assistant_menu_capability', 'manage_options'); 30 33 31 34 // $hook = add_menu_page(__('Options Table', 'nhrrob-options-table-manager'), __('Options Table', 'nhrrob-options-table-manager'), $capability, $parent_slug, [$this, 'settings_page'], 'dashicons-admin-post'); 32 35 // add_submenu_page( $parent_slug, __( 'Settings', 'nhrrob-options-table-manager' ), __( 'Settings', 'nhrrob-options-table-manager' ), $capability, 'nhrotm-options-table-manager-settings', [ $this, 'settings_page' ] ); 33 $hook = add_submenu_page( 'tools.php', __( 'QA Assistant', 'qa-assistant' ), __( 'QA Assistant', 'qa-assistant' ), $capability, $parent_slug, [ $this, 'settings_page' ]);36 $hook = add_submenu_page('tools.php', __('QA Assistant', 'qa-assistant'), __('QA Assistant', 'qa-assistant'), $capability, $parent_slug, [$this, 'settings_page']); 34 37 35 38 add_action('admin_head-' . $hook, [$this, 'enqueue_assets']); … … 41 44 * @return void 42 45 */ 43 public function settings_page() { 46 public function settings_page() 47 { 44 48 // Check user capabilities 45 49 if (!current_user_can('manage_options')) { … … 49 53 $settings = new Settings(); 50 54 51 wp_enqueue_style( 'qa-assistant-select2-style');52 wp_enqueue_script( 'qa-assistant-select2-script');53 wp_enqueue_style( 'qa-assistant-bootstrap-style');54 wp_enqueue_script( 'qa-assistant-bootstrap-script');55 wp_enqueue_script( 'qa-assistant-popper-js-script');56 wp_enqueue_script( 'qa-assistant-jquery-slim-script');55 wp_enqueue_style('qa-assistant-select2-style'); 56 wp_enqueue_script('qa-assistant-select2-script'); 57 wp_enqueue_style('qa-assistant-bootstrap-style'); 58 wp_enqueue_script('qa-assistant-bootstrap-script'); 59 wp_enqueue_script('qa-assistant-popper-js-script'); 60 wp_enqueue_script('qa-assistant-jquery-slim-script'); 57 61 58 62 $available_plugins = $settings->get_available_plugins(); … … 64 68 } 65 69 66 // Get currently selected plugins for the dropdown67 $current_settings = maybe_unserialize(get_option('qa_assistant_settings', array()));68 $selected_plugins = isset($current_settings['selected_plugins']) ? $current_settings['selected_plugins'] : array();70 // Get currently selected plugins for the dropdown 71 $current_settings = maybe_unserialize(get_option('qa_assistant_settings', array())); 72 $selected_plugins = isset($current_settings['selected_plugins']) ? $current_settings['selected_plugins'] : array(); 69 73 70 74 // Save settings data 71 if ( isset( $_POST['qa_assistant_settings_form_nonce'] ) && wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['qa_assistant_settings_form_nonce'])), 'qa_assistant_settings_form_action' )) {75 if (isset($_POST['qa_assistant_settings_form_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['qa_assistant_settings_form_nonce'])), 'qa_assistant_settings_form_action')) { 72 76 // get posted data and sanitize 73 77 $selected_plugins = []; … … 141 145 } 142 146 143 require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php';147 require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php'; 144 148 } 145 149 … … 149 153 * @return void 150 154 */ 151 public function enqueue_assets() { 152 wp_enqueue_style( 'qa-assistant-admin-style' ); 153 wp_enqueue_script( 'qa-assistant-admin-script' ); 155 public function enqueue_assets() 156 { 157 wp_enqueue_style('qa-assistant-admin-style'); 158 wp_enqueue_script('qa-assistant-admin-script'); 154 159 } 155 160 } -
qa-assistant/tags/2.0.0/includes/Ajax.php
r3370854 r3469660 42 42 add_action('wp_ajax_qa_assistant_refresh_branches', [$this, 'refresh_branches']); 43 43 44 // Stash and commit 45 add_action('wp_ajax_qa_assistant_stash_changes', [$this, 'stash_changes']); 46 add_action('wp_ajax_qa_assistant_commit_changes', [$this, 'commit_changes']); 47 44 48 // Legacy support 45 49 add_action('wp_ajax_qa_assistant_get_branch_data', [$this, 'get_branch_data']); 46 50 51 // Clone repository 52 add_action('wp_ajax_qa_assistant_clone_repo', [$this, 'clone_repository']); 53 54 // Toggle monitor status 55 add_action('wp_ajax_qa_assistant_toggle_monitor', [$this, 'toggle_monitor_plugin']); 56 57 // Get installed git plugins 58 add_action('wp_ajax_qa_assistant_get_plugins', [$this, 'get_git_plugins']); 59 60 // Save display settings (aliases and monitoring) 61 add_action('wp_ajax_qa_assistant_save_display_settings', [$this, 'save_display_settings']); 62 63 // Git drawer endpoints 64 add_action('wp_ajax_qa_assistant_get_repositories', [$this, 'get_repositories']); 65 add_action('wp_ajax_qa_assistant_get_branches', [$this, 'get_branches_for_repo']); 66 67 // Settings page endpoints 68 add_action('wp_ajax_qa_assistant_get_system_status', [$this, 'get_system_status']); 69 add_action('wp_ajax_qa_assistant_get_activity_logs', [$this, 'get_activity_logs']); 70 add_action('wp_ajax_qa_assistant_clear_activity_logs', [$this, 'clear_activity_logs']); 71 47 72 $this->gitManager = new GitManager(); 73 } 74 75 /** 76 * Get list of plugins that are git repositories 77 */ 78 public function get_git_plugins() 79 { 80 // Verify nonce usually, but for initial fetch we might just check permissions 81 // or use the common admin nonce 82 if (!current_user_can('manage_options')) { 83 wp_send_json_error(['message' => 'Unauthorized']); 84 } 85 86 $plugins = []; 87 $plugin_dirs = array_filter(glob(WP_PLUGIN_DIR . '/*'), 'is_dir'); 88 $monitored_plugins = get_option('qa_assistant_monitored_plugins', []); 89 90 // Retrieve settings for aliases 91 $settings_option = get_option('qa_assistant_settings', []); 92 $settings_option = maybe_unserialize($settings_option); 93 $selected_plugins_settings = isset($settings_option['selected_plugins']) ? $settings_option['selected_plugins'] : []; 94 95 foreach ($plugin_dirs as $dir) { 96 $slug = basename($dir); 97 // Check if it's a git repo 98 if ($this->gitManager->isGitRepository($dir)) { 99 $status = $this->gitManager->getRepositoryStatus($dir); 100 $branch = $this->gitManager->getCurrentBranch($dir); 101 102 // Get Plugin Name from main file if possible 103 $name = $slug; 104 $main_file = $dir . '/' . $slug . '.php'; 105 if (file_exists($main_file)) { 106 $plugin_data = get_plugin_data($main_file, false, false); 107 if (!empty($plugin_data['Name'])) { 108 $name = $plugin_data['Name']; 109 } 110 } 111 112 // Determine Alias 113 $alias = ''; 114 // Check new associated array format 115 if (isset($selected_plugins_settings[$slug]) && is_array($selected_plugins_settings[$slug])) { 116 $alias = isset($selected_plugins_settings[$slug]['alias']) ? $selected_plugins_settings[$slug]['alias'] : ''; 117 } elseif (isset($selected_plugins_settings[$slug]) && !is_array($selected_plugins_settings[$slug])) { 118 // Check legacy simple key-value (if any, though legacy was simple array of values) 119 // If it was simple array [0 => 'slug'], keys are integers. 120 // If it was assoc [slug => slug], value is string. 121 // We assume new format mostly, but 'alias' defaults to empty string. 122 } 123 124 $plugins[] = [ 125 'id' => $slug, // Use slug as ID 126 'name' => $name, 127 'slug' => $slug, 128 'currentBranch' => $branch, 129 'status' => $status['has_changes'] ? 'modified' : 'stable', 130 'path' => $dir, 131 'is_monitored' => in_array($slug, $monitored_plugins), 132 'alias' => $alias 133 ]; 134 } 135 } 136 137 wp_send_json_success(['plugins' => $plugins]); 138 } 139 140 /** 141 * Toggle monitor status for a plugin 142 */ 143 public function toggle_monitor_plugin() 144 { 145 // Verify nonce 146 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 147 wp_send_json_error(['message' => 'Security check failed.']); 148 } 149 150 $slug = sanitize_text_field(wp_unslash($_POST['slug'] ?? '')); 151 $monitor = filter_var(wp_unslash($_POST['monitor'] ?? false), FILTER_VALIDATE_BOOLEAN); 152 153 if (empty($slug)) { 154 wp_send_json_error(['message' => 'Plugin slug is required.']); 155 } 156 157 $monitored = get_option('qa_assistant_monitored_plugins', []); 158 159 if ($monitor) { 160 if (!in_array($slug, $monitored)) { 161 $monitored[] = $slug; 162 } 163 } else { 164 $monitored = array_diff($monitored, [$slug]); 165 } 166 167 update_option('qa_assistant_monitored_plugins', array_values($monitored)); 168 169 wp_send_json_success([ 170 'message' => $monitor ? 'Plugin added to monitoring.' : 'Plugin removed from monitoring.', 171 'slug' => $slug, 172 'is_monitored' => $monitor 173 ]); 48 174 } 49 175 … … 84 210 85 211 if ($result['success']) { 212 $this->log_activity('switch', $plugin_dir, $result['current_branch'], 'success', 'Switched to ' . $result['current_branch']); 86 213 wp_send_json_success([ 87 214 'message' => $result['message'], … … 162 289 163 290 if ($result['success']) { 291 // Store last pulled timestamp (24h expiry) 292 set_transient('qa_assistant_last_pulled_' . md5($path), time(), DAY_IN_SECONDS); 293 294 $this->log_activity('pull', $plugin_dir, $result['branch'], 'success', 'Pulled latest changes'); 295 164 296 wp_send_json_success([ 165 297 'message' => $result['message'], 166 298 'branch' => $result['branch'], 167 299 'output' => $result['output'] ?? '', 168 'plugin_dir' => $plugin_dir 300 'plugin_dir' => $plugin_dir, 301 'lastPulled' => time(), 169 302 ]); 170 303 } else { … … 293 426 } 294 427 } 428 429 /** 430 * Stash changes for a repository 431 */ 432 public function stash_changes() 433 { 434 // Verify nonce for security 435 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 436 wp_send_json_error([ 437 'message' => 'Security check failed.' 438 ]); 439 } 440 441 $plugin_dir = sanitize_text_field(wp_unslash($_POST['plugin_dir'] ?? '')); 442 443 if (empty($plugin_dir)) { 444 wp_send_json_error([ 445 'message' => 'Plugin directory is required.' 446 ]); 447 } 448 449 $path = qa_assistant_get_plugin_path($plugin_dir); 450 451 // Validate plugin directory exists 452 if (!is_dir($path)) { 453 wp_send_json_error([ 454 'message' => 'Plugin directory does not exist.' 455 ]); 456 } 457 458 $result = $this->gitManager->stashChanges($path); 459 460 if ($result['success']) { 461 wp_send_json_success([ 462 'message' => $result['message'], 463 'plugin_dir' => $plugin_dir 464 ]); 465 } else { 466 wp_send_json_error([ 467 'message' => $result['error'] 468 ]); 469 } 470 } 471 472 /** 473 * Commit changes for a repository 474 */ 475 public function commit_changes() 476 { 477 // Verify nonce for security 478 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 479 wp_send_json_error([ 480 'message' => 'Security check failed.' 481 ]); 482 } 483 484 $plugin_dir = sanitize_text_field(wp_unslash($_POST['plugin_dir'] ?? '')); 485 $message = sanitize_text_field(wp_unslash($_POST['commit_message'] ?? '')); 486 487 if (empty($plugin_dir)) { 488 wp_send_json_error([ 489 'message' => 'Plugin directory is required.' 490 ]); 491 } 492 493 if (empty(trim($message))) { 494 wp_send_json_error([ 495 'message' => 'Commit message is required.' 496 ]); 497 } 498 499 $path = qa_assistant_get_plugin_path($plugin_dir); 500 501 // Validate plugin directory exists 502 if (!is_dir($path)) { 503 wp_send_json_error([ 504 'message' => 'Plugin directory does not exist.' 505 ]); 506 } 507 508 $result = $this->gitManager->commitChanges($path, $message); 509 510 if ($result['success']) { 511 wp_send_json_success([ 512 'message' => $result['message'], 513 'plugin_dir' => $plugin_dir 514 ]); 515 } else { 516 wp_send_json_error([ 517 'message' => $result['error'] 518 ]); 519 } 520 } 521 522 /** 523 * Clone a GitHub user repository 524 */ 525 public function clone_repository() 526 { 527 // Verify nonce for security 528 // Note: We use a specific nonce for settings page actions if available, or fall back to the admin nonce 529 $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')); 530 if (!wp_verify_nonce($nonce, 'qa_assistant_clone_repo')) { 531 wp_send_json_error([ 532 'message' => 'Security check failed. Please refresh the page and try again.' 533 ]); 534 } 535 536 // Check permissions 537 if (!current_user_can('manage_options')) { 538 wp_send_json_error([ 539 'message' => 'You do not have permission to perform this action.' 540 ]); 541 } 542 543 $repo_url = sanitize_text_field(wp_unslash($_POST['repo_url'] ?? '')); 544 545 if (empty($repo_url)) { 546 wp_send_json_error([ 547 'message' => 'Repository URL is required.' 548 ]); 549 } 550 551 // Validate URL format (allow HTTP/HTTPS and SSH) 552 if (!filter_var($repo_url, FILTER_VALIDATE_URL) && !preg_match('/^git@[\w\.-]+:[\w\.-]+\/[\w\.-]+\.git$/', $repo_url)) { 553 // Fallback Regex for more loose git url validation if strict check fails 554 if (!preg_match('/^(https?:\/\/|git@).+\.git$/', $repo_url)) { 555 wp_send_json_error([ 556 'message' => 'Invalid formatted Git URL. Please use HTTPS or SSH format ending in .git' 557 ]); 558 } 559 } 560 561 // Extract repository name to better determine target directory 562 $repo_name = ''; 563 564 // Try parsing as URL first 565 $path = wp_parse_url($repo_url, PHP_URL_PATH); 566 if ($path) { 567 $path_parts = pathinfo($path); 568 $repo_name = $path_parts['filename']; 569 } 570 571 // If parse_url failed (common with SCP-like SSH syntax), try regex extraction 572 if (empty($repo_name)) { 573 if (preg_match('/\/([^\/]+)\.git$/', $repo_url, $matches)) { 574 $repo_name = $matches[1]; 575 } 576 } 577 578 if (empty($repo_name)) { 579 wp_send_json_error([ 580 'message' => 'Could not determine repository name from URL.' 581 ]); 582 } 583 584 $target_path = WP_PLUGIN_DIR . '/' . $repo_name; 585 586 // Check if directory already exists before even trying git 587 if (file_exists($target_path)) { 588 wp_send_json_error([ 589 'message' => "Directory '{$repo_name}' already exists in plugins folder. Please remove or rename it first.", 590 'target_exists' => true 591 ]); 592 } 593 594 $result = $this->gitManager->cloneRepository($repo_url, $target_path); 595 596 if ($result['success']) { 597 // Auto-monitor this plugin 598 $monitored = get_option('qa_assistant_monitored_plugins', []); 599 if (!in_array($repo_name, $monitored)) { 600 $monitored[] = $repo_name; 601 update_option('qa_assistant_monitored_plugins', $monitored); 602 } 603 604 wp_send_json_success([ 605 'message' => "Successfully cloned '{$repo_name}' into plugins directory.", 606 'repo_name' => $repo_name, 607 'path' => $target_path 608 ]); 609 } else { 610 wp_send_json_error([ 611 'message' => $result['error'] 612 ]); 613 } 614 } 615 616 /** 617 * Save display settings (Aliases and Monitoring status) 618 */ 619 public function save_display_settings() 620 { 621 // Verify nonce 622 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 623 wp_send_json_error(['message' => 'Security check failed.']); 624 } 625 626 // Retrieve plugins data 627 // Expecting $_POST['plugins'] to be an array of objects: { slug: '...', alias: '...', is_monitored: true/false } 628 // Or a JSON string 629 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Data is sanitized per-field below in the foreach loop. 630 $plugins_input = isset($_POST['plugins']) ? wp_unslash($_POST['plugins']) : []; 631 632 if (is_string($plugins_input)) { 633 $plugins = json_decode($plugins_input, true); 634 } else { 635 $plugins = $plugins_input; 636 } 637 638 if (!is_array($plugins)) { 639 wp_send_json_error(['message' => 'Invalid data format.']); 640 } 641 642 $monitored_slugs = []; 643 $settings_entries = []; 644 645 foreach ($plugins as $plugin) { 646 // sanitize 647 $slug = sanitize_text_field($plugin['slug']); 648 $is_monitored = filter_var($plugin['is_monitored'], FILTER_VALIDATE_BOOLEAN) || $plugin['is_monitored'] === 'true'; 649 $alias = sanitize_text_field($plugin['alias']); 650 651 if ($is_monitored) { 652 $monitored_slugs[] = $slug; 653 // Add to settings entries with alias 654 $settings_entries[$slug] = [ 655 'alias' => $alias 656 ]; 657 } 658 } 659 660 // Update monitored plugins option (Simple list of slugs) 661 update_option('qa_assistant_monitored_plugins', $monitored_slugs); 662 663 // Update settings option (Associative array with aliases) 664 $current_settings = get_option('qa_assistant_settings', []); 665 $current_settings = maybe_unserialize($current_settings); 666 if (!is_array($current_settings)) { 667 $current_settings = []; 668 } 669 670 $current_settings['selected_plugins'] = $settings_entries; 671 update_option('qa_assistant_settings', $current_settings); 672 673 wp_send_json_success([ 674 'message' => 'Display settings saved successfully.', 675 'monitored_count' => count($monitored_slugs) 676 ]); 677 } 678 679 /** 680 * Get monitored repositories for the Git Branches drawer. 681 * Returns each repo's slug, display name, alias, and current branch. 682 */ 683 public function get_repositories() 684 { 685 // Verify nonce 686 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 687 wp_send_json_error(['message' => 'Security check failed.']); 688 } 689 690 if (!current_user_can('manage_options')) { 691 wp_send_json_error(['message' => 'Unauthorized.']); 692 } 693 694 $qa_settings = get_option('qa_assistant_settings', []); 695 $qa_settings = maybe_unserialize($qa_settings); 696 697 if (!is_array($qa_settings) || !isset($qa_settings['selected_plugins'])) { 698 wp_send_json_success(['repositories' => []]); 699 } 700 701 $plugin_dirs = $qa_settings['selected_plugins']; 702 if (!is_array($plugin_dirs)) { 703 wp_send_json_success(['repositories' => []]); 704 } 705 706 $repositories = []; 707 708 foreach ($plugin_dirs as $slug => $settings) { 709 if (!is_array($settings)) { 710 $settings = ['alias' => $settings]; 711 } 712 713 $path = qa_assistant_get_plugin_path($slug); 714 if (!$this->gitManager->isGitRepository($path)) { 715 continue; 716 } 717 718 $currentBranch = $this->gitManager->getCurrentBranch($path); 719 $alias = isset($settings['alias']) && !empty($settings['alias']) ? $settings['alias'] : $slug; 720 721 // Check for uncommitted changes 722 $hasChanges = false; 723 try { 724 $status = $this->gitManager->getRepositoryStatus($path); 725 if (!empty($status['has_changes'])) { 726 $hasChanges = true; 727 } 728 } catch (\Exception $e) { 729 // Silently continue if status check fails 730 } 731 732 // Get last pulled time from transient 733 $lastPulled = get_transient('qa_assistant_last_pulled_' . md5($path)); 734 735 // Try to get a proper display name from plugin header 736 $name = $slug; 737 $main_file = $path . '/' . $slug . '.php'; 738 if (file_exists($main_file)) { 739 $plugin_data = get_plugin_data($main_file, false, false); 740 if (!empty($plugin_data['Name'])) { 741 $name = $plugin_data['Name']; 742 } 743 } 744 745 $repositories[] = [ 746 'slug' => sanitize_text_field($slug), 747 'name' => sanitize_text_field($name), 748 'alias' => sanitize_text_field($alias), 749 'currentBranch' => sanitize_text_field($currentBranch ?: 'unknown'), 750 'hasChanges' => $hasChanges, 751 'lastPulled' => $lastPulled ? intval($lastPulled) : null, 752 ]; 753 } 754 755 wp_send_json_success(['repositories' => $repositories]); 756 } 757 758 /** 759 * Get branches for a single repository (lazy load on repo click). 760 * Returns sorted branch list. 761 */ 762 public function get_branches_for_repo() 763 { 764 // Verify nonce 765 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 766 wp_send_json_error(['message' => 'Security check failed.']); 767 } 768 769 if (!current_user_can('manage_options')) { 770 wp_send_json_error(['message' => 'Unauthorized.']); 771 } 772 773 $plugin_dir = sanitize_text_field(wp_unslash($_POST['plugin_dir'] ?? '')); 774 if (empty($plugin_dir)) { 775 wp_send_json_error(['message' => 'Plugin directory is required.']); 776 } 777 778 $path = qa_assistant_get_plugin_path($plugin_dir); 779 if (!is_dir($path)) { 780 wp_send_json_error(['message' => 'Plugin directory does not exist.']); 781 } 782 783 // Get branches without fetching from remote (fast, no blocking) 784 $branches = $this->gitManager->getBranches($path, false); 785 $currentBranch = $this->gitManager->getCurrentBranch($path) ?: ''; 786 787 // Check for uncommitted changes 788 $hasChanges = false; 789 try { 790 $status = $this->gitManager->getRepositoryStatus($path); 791 if (!empty($status['has_changes'])) { 792 $hasChanges = true; 793 } 794 } catch (\Exception $e) { 795 // Silently continue 796 } 797 798 // Get last pulled time 799 $lastPulled = get_transient('qa_assistant_last_pulled_' . md5($path)); 800 801 // Sort branches: master/main → develop → current → others 802 $branches = $this->sort_branches_for_drawer($branches, $currentBranch); 803 804 wp_send_json_success([ 805 'branches' => array_map('sanitize_text_field', $branches), 806 'currentBranch' => sanitize_text_field($currentBranch), 807 'plugin_dir' => sanitize_text_field($plugin_dir), 808 'hasChanges' => $hasChanges, 809 'lastPulled' => $lastPulled ? intval($lastPulled) : null, 810 ]); 811 } 812 813 /** 814 * Sort branches by priority: master/main → develop → current → others 815 * 816 * @param array $branches 817 * @param string $currentBranch 818 * @return array 819 */ 820 private function sort_branches_for_drawer($branches, $currentBranch) 821 { 822 if (empty($branches)) { 823 return []; 824 } 825 826 $top = []; 827 $develop = []; 828 $current = []; 829 $others = []; 830 831 foreach ($branches as $branch) { 832 if ($branch === 'master' || $branch === 'main') { 833 $top[] = $branch; 834 } elseif ($branch === 'develop' || $branch === 'dev') { 835 $develop[] = $branch; 836 } elseif ($branch === $currentBranch) { 837 $current[] = $branch; 838 } else { 839 $others[] = $branch; 840 } 841 } 842 843 sort($others); 844 845 return array_values(array_unique(array_merge($top, $develop, $current, $others))); 846 } 847 848 /** 849 * Log an activity entry. 850 * 851 * @param string $action Action type: pull, switch, fetch, stash, commit 852 * @param string $repo Repository slug 853 * @param string $branch Branch name 854 * @param string $status success or error 855 * @param string $message Human-readable message 856 */ 857 private function log_activity($action, $repo, $branch, $status, $message) 858 { 859 $logs = get_option('qa_assistant_activity_log', []); 860 if (!is_array($logs)) { 861 $logs = []; 862 } 863 864 array_unshift($logs, [ 865 'action' => sanitize_text_field($action), 866 'repo' => sanitize_text_field($repo), 867 'branch' => sanitize_text_field($branch), 868 'status' => sanitize_text_field($status), 869 'message' => sanitize_text_field($message), 870 'timestamp' => time(), 871 'user' => wp_get_current_user()->display_name, 872 ]); 873 874 // Keep only the last 100 entries 875 $logs = array_slice($logs, 0, 100); 876 update_option('qa_assistant_activity_log', $logs, false); 877 } 878 879 /** 880 * Get system status information for the settings page. 881 */ 882 public function get_system_status() 883 { 884 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 885 wp_send_json_error(['message' => 'Security check failed.']); 886 } 887 888 if (!current_user_can('manage_options')) { 889 wp_send_json_error(['message' => 'Unauthorized.']); 890 } 891 892 // Git version 893 $git_version = 'Not found'; 894 $git_path = 'N/A'; 895 $git_output = []; 896 exec('git --version 2>&1', $git_output); 897 if (!empty($git_output[0])) { 898 $git_version = trim(str_replace('git version', '', $git_output[0])); 899 } 900 $git_path_output = []; 901 exec('which git 2>&1', $git_path_output); 902 if (!empty($git_path_output[0])) { 903 $git_path = trim($git_path_output[0]); 904 } 905 906 // Count monitored repos 907 $qa_settings = get_option('qa_assistant_settings', []); 908 $qa_settings = maybe_unserialize($qa_settings); 909 $monitored_count = 0; 910 if (is_array($qa_settings) && isset($qa_settings['selected_plugins'])) { 911 $monitored_count = count($qa_settings['selected_plugins']); 912 } 913 914 wp_send_json_success([ 915 'git_version' => $git_version, 916 'git_path' => $git_path, 917 'php_version' => phpversion(), 918 'wp_version' => get_bloginfo('version'), 919 'plugin_version' => defined('QA_ASSISTANT_VERSION') ? QA_ASSISTANT_VERSION : 'unknown', 920 'memory_limit' => ini_get('memory_limit'), 921 'memory_usage' => size_format(memory_get_usage(true)), 922 'monitored_repos' => $monitored_count, 923 'os' => PHP_OS, 924 ]); 925 } 926 927 /** 928 * Get activity logs for the settings page. 929 */ 930 public function get_activity_logs() 931 { 932 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 933 wp_send_json_error(['message' => 'Security check failed.']); 934 } 935 936 if (!current_user_can('manage_options')) { 937 wp_send_json_error(['message' => 'Unauthorized.']); 938 } 939 940 $logs = get_option('qa_assistant_activity_log', []); 941 if (!is_array($logs)) { 942 $logs = []; 943 } 944 945 wp_send_json_success(['logs' => $logs]); 946 } 947 948 /** 949 * Clear all activity logs. 950 */ 951 public function clear_activity_logs() 952 { 953 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 954 wp_send_json_error(['message' => 'Security check failed.']); 955 } 956 957 if (!current_user_can('manage_options')) { 958 wp_send_json_error(['message' => 'Unauthorized.']); 959 } 960 961 update_option('qa_assistant_activity_log', [], false); 962 wp_send_json_success(['message' => 'Activity logs cleared.']); 963 } 295 964 } -
qa-assistant/tags/2.0.0/includes/Assets.php
r3370854 r3469660 11 11 * Assets handler class 12 12 */ 13 class Assets { 13 class Assets 14 { 14 15 15 16 /** 16 17 * Class constructor 17 18 */ 18 function __construct() { 19 add_action( 'wp_enqueue_scripts', [ $this, 'register_assets' ] ); 20 add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ] ); 19 function __construct() 20 { 21 add_action('wp_enqueue_scripts', [$this, 'register_assets']); 22 add_action('admin_enqueue_scripts', [$this, 'register_assets']); 23 // Enqueue dashboard assets specifically for the settings page 24 add_action('admin_enqueue_scripts', [$this, 'register_dashboard_assets']); 21 25 // Enqueue admin bar assets on frontend if admin bar is showing 22 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_admin_bar_assets' ] ); 26 add_action('wp_enqueue_scripts', [$this, 'enqueue_admin_bar_assets']); 27 // Enqueue Git Branches drawer React app (admin + frontend) 28 add_action('admin_enqueue_scripts', [$this, 'register_drawer_assets']); 29 add_action('wp_enqueue_scripts', [$this, 'register_drawer_assets']); 23 30 } 24 31 … … 28 35 * @return array 29 36 */ 30 public function get_scripts() { 37 public function get_scripts() 38 { 31 39 return [ 32 40 'qa-assistant-script' => [ 33 'src' => QA_ASSISTANT_ASSETS . '/js/frontend.js',34 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/js/frontend.js'),35 'deps' => [ 'jquery']41 'src' => QA_ASSISTANT_ASSETS . '/js/frontend.js', 42 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/js/frontend.js'), 43 'deps' => ['jquery'] 36 44 ], 37 45 'qa-assistant-admin-script' => [ 38 'src' => QA_ASSISTANT_ASSETS . '/js/admin.js',39 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/js/admin.js'),40 'deps' => [ 'jquery', 'wp-util']46 'src' => QA_ASSISTANT_ASSETS . '/js/admin.js', 47 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/js/admin.js'), 48 'deps' => ['jquery', 'wp-util'] 41 49 ], 42 50 'qa-assistant-select2-script' => [ 43 'src' => QA_ASSISTANT_ASSETS . '/js/select2.min.js',44 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/js/select2.min.js'),45 'deps' => [ 'jquery']51 'src' => QA_ASSISTANT_ASSETS . '/js/select2.min.js', 52 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/js/select2.min.js'), 53 'deps' => ['jquery'] 46 54 ], 47 55 // 'qa-assistant-bootstrap-script' => [ … … 68 76 * @return array 69 77 */ 70 public function get_styles() { 78 public function get_styles() 79 { 71 80 return [ 72 81 'qa-assistant-style' => [ 73 'src' => QA_ASSISTANT_ASSETS . '/css/frontend.css',74 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/css/frontend.css')82 'src' => QA_ASSISTANT_ASSETS . '/css/frontend.css', 83 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/css/frontend.css') 75 84 ], 76 85 'qa-assistant-admin-style' => [ 77 'src' => QA_ASSISTANT_ASSETS . '/css/admin.css',78 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/css/admin.css')86 'src' => QA_ASSISTANT_ASSETS . '/css/admin.css', 87 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/css/admin.css') 79 88 ], 80 89 'qa-assistant-select2-style' => [ 81 'src' => QA_ASSISTANT_ASSETS . '/css/select2.min.css',82 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/css/select2.min.css')90 'src' => QA_ASSISTANT_ASSETS . '/css/select2.min.css', 91 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/css/select2.min.css') 83 92 ], 84 93 // 'qa-assistant-bootstrap-style' => [ … … 94 103 * @return void 95 104 */ 96 public function register_assets() { 105 public function register_assets() 106 { 97 107 $scripts = $this->get_scripts(); 98 $styles = $this->get_styles();108 $styles = $this->get_styles(); 99 109 100 110 // Load admin assets only in admin area 101 111 if (is_admin()) { 102 foreach ( $scripts as $handle => $script) {112 foreach ($scripts as $handle => $script) { 103 113 if (strpos($handle, 'admin') !== false || strpos($handle, 'select2') !== false) { 104 $deps = isset( $script['deps']) ? $script['deps'] : false;105 wp_enqueue_script( $handle, $script['src'], $deps, $script['version'], true);106 } 107 } 108 109 foreach ( $styles as $handle => $style) {114 $deps = isset($script['deps']) ? $script['deps'] : false; 115 wp_enqueue_script($handle, $script['src'], $deps, $script['version'], true); 116 } 117 } 118 119 foreach ($styles as $handle => $style) { 110 120 if (strpos($handle, 'admin') !== false || strpos($handle, 'select2') !== false) { 111 $deps = isset( $style['deps']) ? $style['deps'] : false;112 wp_enqueue_style( $handle, $style['src'], $deps, $style['version']);113 } 114 } 115 116 wp_localize_script( 'qa-assistant-admin-script', 'qaAssistant', [117 'nonce' => wp_create_nonce( 'qa-assistant-admin-nonce'),118 'confirm' => __( 'Are you sure?', 'qa-assistant'),119 'error' => __( 'Something went wrong', 'qa-assistant'),121 $deps = isset($style['deps']) ? $style['deps'] : false; 122 wp_enqueue_style($handle, $style['src'], $deps, $style['version']); 123 } 124 } 125 126 wp_localize_script('qa-assistant-admin-script', 'qaAssistant', [ 127 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 128 'confirm' => __('Are you sure?', 'qa-assistant'), 129 'error' => __('Something went wrong', 'qa-assistant'), 120 130 'ajaxUrl' => admin_url('admin-ajax.php'), 121 ] );131 ]); 122 132 } 123 133 124 134 // Load frontend assets only if needed (adjust condition as necessary) 125 135 if (!is_admin()) { 126 foreach ( $scripts as $handle => $script) {136 foreach ($scripts as $handle => $script) { 127 137 if (strpos($handle, 'frontend') !== false) { 128 $deps = isset( $script['deps']) ? $script['deps'] : false;129 wp_enqueue_script( $handle, $script['src'], $deps, $script['version'], true);130 } 131 } 132 133 foreach ( $styles as $handle => $style) {138 $deps = isset($script['deps']) ? $script['deps'] : false; 139 wp_enqueue_script($handle, $script['src'], $deps, $script['version'], true); 140 } 141 } 142 143 foreach ($styles as $handle => $style) { 134 144 if (strpos($handle, 'frontend') !== false) { 135 $deps = isset( $style['deps']) ? $style['deps'] : false;136 wp_enqueue_style( $handle, $style['src'], $deps, $style['version']);145 $deps = isset($style['deps']) ? $style['deps'] : false; 146 wp_enqueue_style($handle, $style['src'], $deps, $style['version']); 137 147 } 138 148 } … … 143 153 * Enqueue admin bar dropdown assets on frontend if admin bar is showing 144 154 */ 145 public function enqueue_admin_bar_assets() { 146 if ( ! is_admin() && is_admin_bar_showing() ) { 155 public function enqueue_admin_bar_assets() 156 { 157 if (!is_admin() && is_admin_bar_showing()) { 147 158 // Enqueue styles/scripts needed for the admin bar dropdown 148 wp_enqueue_style( 'qa-assistant-admin-style', QA_ASSISTANT_ASSETS . '/css/admin.css', [], filemtime( QA_ASSISTANT_PATH . '/assets/css/admin.css' ));149 wp_enqueue_style( 'qa-assistant-select2-style', QA_ASSISTANT_ASSETS . '/css/select2.min.css', [], filemtime( QA_ASSISTANT_PATH . '/assets/css/select2.min.css' ));150 wp_enqueue_script( 'qa-assistant-select2-script', QA_ASSISTANT_ASSETS . '/js/select2.min.js', [ 'jquery' ], filemtime( QA_ASSISTANT_PATH . '/assets/js/select2.min.js' ), true);151 wp_enqueue_script( 'qa-assistant-admin-script', QA_ASSISTANT_ASSETS . '/js/admin.js', [ 'jquery', 'wp-util' ], filemtime( QA_ASSISTANT_PATH . '/assets/js/admin.js' ), true);159 wp_enqueue_style('qa-assistant-admin-style', QA_ASSISTANT_ASSETS . '/css/admin.css', [], filemtime(QA_ASSISTANT_PATH . '/assets/css/admin.css')); 160 wp_enqueue_style('qa-assistant-select2-style', QA_ASSISTANT_ASSETS . '/css/select2.min.css', [], filemtime(QA_ASSISTANT_PATH . '/assets/css/select2.min.css')); 161 wp_enqueue_script('qa-assistant-select2-script', QA_ASSISTANT_ASSETS . '/js/select2.min.js', ['jquery'], filemtime(QA_ASSISTANT_PATH . '/assets/js/select2.min.js'), true); 162 wp_enqueue_script('qa-assistant-admin-script', QA_ASSISTANT_ASSETS . '/js/admin.js', ['jquery', 'wp-util'], filemtime(QA_ASSISTANT_PATH . '/assets/js/admin.js'), true); 152 163 // Localize script for AJAX and nonce 153 wp_localize_script( 'qa-assistant-admin-script', 'qaAssistant', [154 'nonce' => wp_create_nonce( 'qa-assistant-admin-nonce'),155 'confirm' => __( 'Are you sure?', 'qa-assistant'),156 'error' => __( 'Something went wrong', 'qa-assistant'),164 wp_localize_script('qa-assistant-admin-script', 'qaAssistant', [ 165 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 166 'confirm' => __('Are you sure?', 'qa-assistant'), 167 'error' => __('Something went wrong', 'qa-assistant'), 157 168 'ajaxUrl' => admin_url('admin-ajax.php'), 158 ] ); 159 } 169 ]); 170 } 171 } 172 173 /** 174 * Register React Dashboard assets 175 */ 176 /** 177 * Register React Dashboard assets 178 * 179 * @param string $hook Current admin page hook 180 */ 181 public function register_dashboard_assets($hook) 182 { 183 // Only load on QA Assistant page 184 if ($hook !== 'tools_page_qa-assistant') { 185 return; 186 } 187 188 $build_dir = QA_ASSISTANT_PATH . '/build'; 189 $build_url = QA_ASSISTANT_URL . '/build'; 190 191 if (!file_exists($build_dir . '/index.asset.php')) { 192 return; 193 } 194 195 $asset_file = include $build_dir . '/index.asset.php'; 196 197 wp_enqueue_script( 198 'qa-assistant-dashboard', 199 $build_url . '/index.js', 200 $asset_file['dependencies'], 201 $asset_file['version'], 202 true 203 ); 204 205 wp_enqueue_style( 206 'qa-assistant-dashboard-style', 207 $build_url . '/index.css', 208 [], 209 $asset_file['version'] 210 ); 211 212 // Localize script with server-side data 213 wp_localize_script('qa-assistant-dashboard', 'qaAssistantData', [ 214 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 215 'clone_nonce' => wp_create_nonce('qa_assistant_clone_repo'), 216 'ajaxUrl' => admin_url('admin-ajax.php'), 217 'pluginUrl' => QA_ASSISTANT_PLUGIN_URL, 218 ]); 219 } 220 221 /** 222 * Register Git Branches Drawer React assets. 223 * Loads on both admin and frontend when admin bar is visible. 224 */ 225 public function register_drawer_assets() 226 { 227 // Only load for logged-in users with proper capabilities 228 if (!is_user_logged_in() || !current_user_can('manage_options')) { 229 return; 230 } 231 232 // On frontend, only load if admin bar is showing 233 if (!is_admin() && !is_admin_bar_showing()) { 234 return; 235 } 236 237 $build_dir = QA_ASSISTANT_PATH . '/build/git-drawer'; 238 $build_url = QA_ASSISTANT_URL . '/build/git-drawer'; 239 240 if (!file_exists($build_dir . '/index.asset.php')) { 241 return; 242 } 243 244 $asset_file = include $build_dir . '/index.asset.php'; 245 246 wp_enqueue_script( 247 'qa-git-drawer', 248 $build_url . '/index.js', 249 $asset_file['dependencies'], 250 $asset_file['version'], 251 true 252 ); 253 254 wp_enqueue_style( 255 'qa-git-drawer-style', 256 $build_url . '/index.css', 257 [], 258 $asset_file['version'] 259 ); 260 261 // Pass data to the drawer React app 262 wp_localize_script('qa-git-drawer', 'qaGitDrawer', [ 263 'ajaxUrl' => admin_url('admin-ajax.php'), 264 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 265 'pluginUrl' => QA_ASSISTANT_PLUGIN_URL, 266 ]); 160 267 } 161 268 } -
qa-assistant/tags/2.0.0/includes/GitManager.php
r3370854 r3469660 164 164 $hasChanges = $repo->hasChanges(); 165 165 $branches = $this->getBranches($path, true, $force_refresh); 166 166 167 167 $status = [ 168 168 'valid' => true, … … 467 467 } 468 468 } 469 470 /** 471 * Stash uncommitted changes 472 * 473 * @param string $path Repository path 474 * @return array Operation result 475 */ 476 public function stashChanges($path) 477 { 478 if (!$this->isGitRepository($path)) { 479 return [ 480 'success' => false, 481 'error' => 'Not a Git repository' 482 ]; 483 } 484 485 try { 486 $repo = $this->git->open($path); 487 488 if (!$repo->hasChanges()) { 489 return [ 490 'success' => false, 491 'error' => 'No local changes to stash' 492 ]; 493 } 494 $repo->execute(['stash', 'push', '-u', '-m', 'QA Assistant Auto-Stash before pull']); 495 496 // Invalidate cache 497 delete_transient('qa_assistant_repo_status_' . md5($path)); 498 499 return [ 500 'success' => true, 501 'message' => 'Changes stashed successfully' 502 ]; 503 504 } catch (GitException $e) { 505 return [ 506 'success' => false, 507 'error' => 'Stash operation failed: ' . $e->getMessage() 508 ]; 509 } 510 } 511 512 /** 513 * Commit uncommitted changes 514 * 515 * @param string $path Repository path 516 * @param string $message Commit message 517 * @return array Operation result 518 */ 519 public function commitChanges($path, $message) 520 { 521 if (!$this->isGitRepository($path)) { 522 return [ 523 'success' => false, 524 'error' => 'Not a Git repository' 525 ]; 526 } 527 528 if (empty(trim($message))) { 529 return [ 530 'success' => false, 531 'error' => 'Commit message cannot be empty' 532 ]; 533 } 534 535 try { 536 $repo = $this->git->open($path); 537 538 if (!$repo->hasChanges()) { 539 return [ 540 'success' => false, 541 'error' => 'No local changes to commit' 542 ]; 543 } 544 545 // Add all changes 546 $repo->execute(['add', '.']); 547 548 // Commit 549 $repo->execute(['commit', '-m', $message]); 550 551 // Invalidate cache 552 delete_transient('qa_assistant_repo_status_' . md5($path)); 553 554 return [ 555 'success' => true, 556 'message' => 'Changes committed successfully' 557 ]; 558 559 } catch (GitException $e) { 560 return [ 561 'success' => false, 562 'error' => 'Commit operation failed: ' . $e->getMessage() 563 ]; 564 } 565 } 566 567 /** 568 * Clone a repository 569 * 570 * @param string $url Repository URL 571 * @param string $targetPath Target path to clone into 572 * @return array Operation result 573 */ 574 public function cloneRepository($url, $targetPath) 575 { 576 // Basic validation 577 if (empty($url) || empty($targetPath)) { 578 return [ 579 'success' => false, 580 'error' => 'Repository URL and target path are required' 581 ]; 582 } 583 584 // Check if target directory already exists 585 if (file_exists($targetPath)) { 586 return [ 587 'success' => false, 588 'error' => 'Target directory already exists. Please remove or rename it first.', 589 'target_exists' => true 590 ]; 591 } 592 593 try { 594 // Clone the repository 595 $this->git->cloneRepository($url, $targetPath); 596 597 return [ 598 'success' => true, 599 'message' => 'Repository successfully cloned', 600 'path' => $targetPath 601 ]; 602 } catch (GitException $e) { 603 return [ 604 'success' => false, 605 'error' => 'Clone failed: ' . $e->getMessage() 606 ]; 607 } 608 } 469 609 } -
qa-assistant/tags/2.0.0/qa-assistant.php
r3370854 r3469660 4 4 Plugin URI: https://obayedmamur.com/qa-assistant 5 5 Description: A comprehensive tool for SQA Engineers with GitHub Desktop-like Git branch switching functionality. 6 Version: 1.0.36 Version: 2.0.0 7 7 Author: Obayed Mamur 8 8 Author URI: https://obayedmamur.com … … 10 10 */ 11 11 12 if (! defined('ABSPATH')) {12 if (!defined('ABSPATH')) { 13 13 exit; 14 14 } 15 15 16 16 // Define plugin constants 17 define('QA_ASSISTANT_VERSION', ' 1.0.3');17 define('QA_ASSISTANT_VERSION', '2.0.0'); 18 18 define('QA_ASSISTANT_PLUGIN_FILE', __FILE__); 19 19 define('QA_ASSISTANT_PLUGIN_DIR', plugin_dir_path(__FILE__)); … … 27 27 * @return string The absolute path to the plugin directory 28 28 */ 29 function qa_assistant_get_plugin_path($plugin_dir) { 29 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug. 30 function qa_assistant_get_plugin_path($plugin_dir) 31 { 30 32 // Use WordPress function to get plugins directory 31 33 $plugins_dir = dirname(plugin_dir_path(__FILE__)); … … 68 70 add_action('plugins_loaded', [$this, 'init_plugin']); 69 71 70 add_action('admin_bar_menu', [$this, 'add_git_branch_to_admin_bar'], 100); 72 // Add "Settings" link to plugin action links on the Plugins page 73 add_filter('plugin_action_links_' . QA_ASSISTANT_PLUGIN_BASENAME, [$this, 'plugin_action_links']); 74 75 71 76 } 72 77 … … 80 85 static $instance = false; 81 86 82 if (! $instance) {87 if (!$instance) { 83 88 $instance = new self(); 84 89 } … … 121 126 } 122 127 128 // Initialize Admin Bar 129 if (is_user_logged_in()) { 130 new QaAssistant\Admin\AdminBar(); 131 } 132 123 133 new QaAssistant\API(); 134 } 135 136 /** 137 * Add "Settings" link to plugin action links 138 * 139 * @param array $links Existing plugin action links 140 * @return array Modified plugin action links 141 */ 142 public function plugin_action_links($links) 143 { 144 $settings_link = '<a href="' . admin_url('tools.php?page=qa-assistant') . '">Settings</a>'; 145 array_unshift($links, $settings_link); 146 return $links; 124 147 } 125 148 … … 146 169 return $this->gitManager->getCurrentBranch($path, $force_refresh); 147 170 } 148 149 public function add_git_branch_to_admin_bar($wp_admin_bar)150 {151 // List of plugin directories with their aliases and custom colors152 $qa_assistant_settings = get_option('qa_assistant_settings');153 $qa_assistant_settings = maybe_unserialize($qa_assistant_settings);154 155 if (!is_array($qa_assistant_settings)) {156 return;157 }158 159 $plugin_dirs = $qa_assistant_settings['selected_plugins'];160 $plugin_dirs = array_combine($plugin_dirs, $plugin_dirs);161 162 foreach ($plugin_dirs as $plugin_dir => $settings) {163 $path = qa_assistant_get_plugin_path($plugin_dir);164 $currentBranch = $this->get_git_branch($path);165 if (!$currentBranch) {166 continue;167 }168 169 // Get all branches using GitManager with caching170 $branches = $this->gitManager->getBranches($path, false);171 172 // Use alias or plugin directory name if alias is not provided173 $alias = isset($settings['alias']) ? $settings['alias'] : $plugin_dir;174 175 // Use custom color or generate a random one if not provided176 $color = isset($settings['color']) ? $settings['color'] : '#00fffe';177 178 // Add node to the admin bar for each plugin directory as a Dropdown Sub Menu Item179 if (count($plugin_dirs) > 2) {180 $wp_admin_bar->add_node(array(181 'id' => 'git_branches',182 'title' => '<i class="ab-icon dashicons-share"></i> Git Branches',183 'href' => '',184 ));185 $wp_admin_bar->add_node(array(186 'id' => 'git_branch_' . sanitize_title($plugin_dir),187 'title' => esc_html($alias) . ' (<span style="color: ' . esc_attr($color) . ';">' . esc_html($currentBranch) . '</span>)',188 'href' => '',189 'parent' => 'git_branches',190 'meta' => array('class' => 'qa_assistant_git-branch'),191 ));192 193 // Add pull button for current branch194 $pull_button_id = 'git_pull_' . sanitize_title($plugin_dir);195 $wp_admin_bar->add_node(array(196 'id' => $pull_button_id,197 'title' => 'Pull Latest Changes <svg class="qa-icon qa-pull-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7,10 12,15 17,10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',198 'href' => '#',199 'parent' => 'git_branch_' . sanitize_title($plugin_dir),200 'meta' => array(201 'class' => 'qa-pull-button',202 'onclick' => 'qaAssistantPull("' . esc_js($plugin_dir) . '"); return false;'203 ),204 ));205 206 // Add refresh button to fetch latest branches207 $refresh_button_id = 'git_refresh_' . sanitize_title($plugin_dir);208 $wp_admin_bar->add_node(array(209 'id' => $refresh_button_id,210 'title' => 'Refresh Branches <svg class="qa-icon qa-refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M3 21v-5h5"></path></svg>',211 'href' => '#',212 'parent' => 'git_branch_' . sanitize_title($plugin_dir),213 'meta' => array(214 'class' => 'qa-refresh-button',215 'onclick' => 'qaAssistantRefresh("' . esc_js($plugin_dir) . '"); return false;'216 ),217 ));218 219 // Add search hint for branches if there are many branches220 if (count($branches) > 3) {221 $wp_admin_bar->add_node(array(222 'id' => 'git_branch_search_hint_' . sanitize_title($plugin_dir),223 'title' => '🔍 Type to search branches...<span class="qa-search-cursor">|</span>',224 'href' => '#',225 'parent' => 'git_branch_' . sanitize_title($plugin_dir),226 'meta' => array('class' => 'qa-branch-search-hint'),227 ));228 }229 foreach ($branches as $branchItem) {230 $isCurrentBranch = ($branchItem === $currentBranch);231 $branchClass = 'qa_assistant_git-branch-list-items';232 if ($isCurrentBranch) {233 $branchClass .= ' current-branch';234 }235 236 $wp_admin_bar->add_node(array(237 'id' => 'git_branch_' . sanitize_title($plugin_dir) . '_' . sanitize_title($branchItem),238 'title' => esc_attr($branchItem),239 'href' => '#',240 'parent' => 'git_branch_' . sanitize_title($plugin_dir),241 'data-branch' => esc_attr($branchItem),242 'meta' => array(243 'class' => $branchClass,244 'data-plugin-dir' => esc_attr($plugin_dir),245 'data-branch-name' => esc_attr($branchItem),246 ),247 ));248 }249 } else {250 $wp_admin_bar->add_node(array(251 'id' => 'git_branch_' . sanitize_title($plugin_dir),252 'title' => esc_html($alias) . ' (<span style="color: ' . esc_attr($color) . ';">' . esc_html($currentBranch) . '</span>)',253 'href' => '',254 'meta' => array('class' => 'qa_assistant_git-branch'),255 ));256 257 // Add pull button for current branch258 $pull_button_id = 'git_pull_' . sanitize_title($plugin_dir);259 $wp_admin_bar->add_node(array(260 'id' => $pull_button_id,261 'title' => 'Pull Latest Changes <svg class="qa-icon qa-pull-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7,10 12,15 17,10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',262 'href' => '#',263 'parent' => 'git_branch_' . sanitize_title($plugin_dir),264 'meta' => array(265 'class' => 'qa-pull-button',266 'onclick' => 'qaAssistantPull("' . esc_js($plugin_dir) . '"); return false;'267 ),268 ));269 270 // Add refresh button to fetch latest branches271 $refresh_button_id = 'git_refresh_' . sanitize_title($plugin_dir);272 $wp_admin_bar->add_node(array(273 'id' => $refresh_button_id,274 'title' => 'Refresh Branches <svg class="qa-icon qa-refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M3 21v-5h5"></path></svg>',275 'href' => '#',276 'parent' => 'git_branch_' . sanitize_title($plugin_dir),277 'meta' => array(278 'class' => 'qa-refresh-button',279 'onclick' => 'qaAssistantRefresh("' . esc_js($plugin_dir) . '"); return false;'280 ),281 ));282 283 // Add search hint for branches if there are many branches284 if (count($branches) > 3) {285 $wp_admin_bar->add_node(array(286 'id' => 'git_branch_search_hint_' . sanitize_title($plugin_dir),287 'title' => '🔍 Type to search branches...<span class="qa-search-cursor">|</span>',288 'href' => '#',289 'parent' => 'git_branch_' . sanitize_title($plugin_dir),290 'meta' => array('class' => 'qa-branch-search-hint'),291 ));292 }293 294 foreach ($branches as $branchItem) {295 $isCurrentBranch = ($branchItem === $currentBranch);296 $branchClass = 'qa_assistant_git-branch-list-items';297 if ($isCurrentBranch) {298 $branchClass .= ' current-branch';299 }300 301 $wp_admin_bar->add_node(array(302 'id' => 'git_branch_' . sanitize_title($plugin_dir) . '_' . sanitize_title($branchItem),303 'title' => esc_attr($branchItem),304 'href' => '#',305 'parent' => 'git_branch_' . sanitize_title($plugin_dir),306 'data-branch' => esc_attr($branchItem),307 'meta' => array(308 'class' => $branchClass,309 'data-plugin-dir' => esc_attr($plugin_dir),310 'data-branch-name' => esc_attr($branchItem),311 ),312 ));313 }314 }315 }316 }317 171 } 318 172 … … 322 176 * @return \Qa_Assistant 323 177 */ 178 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug. 324 179 function qa_assistant() 325 180 { … … 329 184 // Call the plugin 330 185 qa_assistant(); 186 187 // Test uncommitted change for git pull modal -
qa-assistant/tags/2.0.0/readme.txt
r3370854 r3469660 3 3 Tags: qa assistant, quality assurance, help, sqa helper tool 4 4 Requires at least: 5.0 5 Tested up to: 6. 86 Requires PHP: 7.47 Stable tag: 1.0.35 Tested up to: 6.9 6 Requires PHP: 8.0 7 Stable tag: 2.0.0 8 8 License: GPLv3 9 9 License URI: https://opensource.org/licenses/GPL-3.0 … … 91 91 == Changelog == 92 92 93 = 2.0.0 - 25/02/2026 = 94 95 **� Major Features & Enhancements:** 96 - Added: Git Branches Drawer feature with new React components and admin bar integration 97 - Added: Modal and backend logic to handle uncommitted changes during Git pull operations (commit or stash) 98 - Added: Custom confirmation modal for plugin removal from the dashboard 99 - Added: Plugin settings link added to the plugin list for easier access 100 101 **🎨 UI/UX Improvements:** 102 - Enhanced: Major UI revamp including a new notification system and animated skeleton loaders 103 - Enhanced: Robust CSS isolation using PostCSS prefixing and inline Tailwind theme 104 - Enhanced: Success button variant and improved Git branch item visuals 105 106 **�🔒 Security & Code Quality:** 107 - Added: PHPCS ignore annotations for correctly-prefixed global functions and classes 108 - Refactored: Refined AJAX URL parsing and plugin data handling 109 110 **⚙️ Compatibility:** 111 - Updated: Minimum PHP requirement from 7.4 to 8.0 to match Composer dependency requirements 112 - Updated: "Tested up to" WordPress version from 6.8 to 6.9 113 - Added: Explicit PHP >= 8.0 constraint in `composer.json` for early validation 114 93 115 = 1.0.3 - Initial Release = 94 116 -
qa-assistant/tags/2.0.0/templates/settings-page.php
r3370854 r3469660 2 2 // Prevent direct access 3 3 if (!defined('ABSPATH')) { 4 exit; // Exit if accessed directly4 exit; 5 5 } 6 6 ?> 7 <div class="wrap"> 8 9 <h1><?php esc_html_e('QA Assistant', 'qa-assistant'); ?></h1> 10 11 <?php 12 // Display admin notices 13 $admin_notice = get_transient('qa_assistant_admin_notice'); 14 if ($admin_notice) { 15 delete_transient('qa_assistant_admin_notice'); 16 $notice_class = 'notice notice-' . esc_attr($admin_notice['type']) . ' is-dismissible'; 17 ?> 18 <div class="<?php echo esc_attr($notice_class); ?>"> 19 <p><?php echo esc_html($admin_notice['message']); ?></p> 20 </div> 21 <?php 22 } 23 ?> 24 25 <div class="qa-assistant-content"> 26 27 <h1>Settings</h1> 28 <div class="qa-assistant-tabs" id="settingsTab" role="tablist"> 29 <div class="qa-assistant-tab-item"> 30 <a class="qa-assistant-tab-link active" id="git-settings-tab" data-tab="git-settings-tab" href="#git-settings-tab" role="tab" aria-controls="git-settings-tab" aria-selected="true">Git Settings</a> 31 </div> 32 </div> 33 34 <div class="qa-assistant-tab-content" id="settingsTabContent"> 35 <div class="qa-assistant-tab-pane active" id="git-settings-tab" role="tabpanel" aria-labelledby="git-settings-tab"> 36 <?php if (! empty($available_plugins)) { ?> 37 38 <form method="post" action="<?php echo esc_url('#'); ?>" class="qa-assistant-form"> 39 <?php wp_nonce_field('qa_assistant_settings_form_action', 'qa_assistant_settings_form_nonce'); ?> 40 41 <h2> 42 <label class="title" for="qa-assistant__plugins-dropdown"> 43 <?php esc_html_e('Git Branch Display', 'qa-assistant'); ?> 44 </label> 45 </h2> 46 47 <p id="qa-assistant__description"> 48 <?php esc_html_e('Select plugins to display Git branch information in the admin bar. You can switch between branches directly from the admin bar with GitHub Desktop-like functionality.', 'qa-assistant'); ?> 49 </p> 50 51 <div class="qa-assistant-feature-info"> 52 <h3>✨ Enhanced Features:</h3> 53 <ul> 54 <li>🔄 <strong>One-click branch switching</strong> - Switch branches directly from the admin bar</li> 55 <li>✅ <strong>Current branch indicator</strong> - See which branch you're currently on</li> 56 <li>⚠️ <strong>Uncommitted changes detection</strong> - Get warnings before switching with unsaved changes</li> 57 <li>🔒 <strong>Force switch option</strong> - Option to discard local changes when switching</li> 58 <li>📢 <strong>Real-time notifications</strong> - Get instant feedback on Git operations</li> 59 <li>🎨 <strong>Visual status indicators</strong> - Color-coded branch status in the admin bar</li> 60 </ul> 61 </div> 62 63 <select class="qa-assistant-select2" id="qa-assistant__plugins-dropdown" name="qa_assistant_plugins[]" aria-describedby="qa-assistant__description" multiple="multiple"> 64 <?php if (1 !== count($available_plugins)) { ?> 65 <option value="" disabled><?php esc_html_e('Select Plugin', 'qa-assistant'); ?></option> 66 <?php } ?> 67 <?php foreach ($available_plugins as $plugin_basename => $available_plugin) { ?> 68 <?php $plugin_dir = explode('/', $plugin_basename)[0]; ?> 69 <option value="<?php echo esc_attr($plugin_dir); ?>" <?php echo in_array($plugin_dir, $selected_plugins) ? 'selected' : ''; ?>> 70 <?php echo esc_html($available_plugin['Name']); ?> 71 </option> 72 <?php } ?> 73 </select> 74 75 <input type="submit" value="<?php esc_attr_e('Save', 'qa-assistant'); ?>" id="qa-assistant__submit" class="qa-assistant-settings-save button button-primary" /> 76 <span id="qa-assistant__spinner" class="spinner" style="float: none;"></span> 77 </form> 78 79 <?php 80 // Show currently selected plugins 81 $current_settings = maybe_unserialize(get_option('qa_assistant_settings', array())); 82 if (!empty($current_settings['selected_plugins'])) { 83 ?> 84 <div class="qa-assistant-selected-plugins"> 85 <h3><?php esc_html_e('Currently Selected Plugins', 'qa-assistant'); ?></h3> 86 <div class="qa-selected-plugins-grid"> 87 <?php 88 foreach ($current_settings['selected_plugins'] as $plugin_dir) { 89 // Find the plugin name from available plugins 90 $plugin_name = $plugin_dir; 91 foreach ($available_plugins as $plugin_basename => $plugin_data) { 92 if (strpos($plugin_basename, $plugin_dir . '/') === 0) { 93 $plugin_name = $plugin_data['Name']; 94 break; 95 } 96 } 97 98 // Check Git status 99 $plugin_path = qa_assistant_get_plugin_path($plugin_dir); 100 $is_git_repo = is_dir($plugin_path . '/.git'); 101 $current_branch = ''; 102 $git_status = 'Not a Git repository'; 103 $status_class = 'no-git'; 104 105 if ($is_git_repo) { 106 $git_head_file = $plugin_path . '/.git/HEAD'; 107 if (file_exists($git_head_file)) { 108 // Use WordPress filesystem API instead of file_get_contents 109 global $wp_filesystem; 110 if (empty($wp_filesystem)) { 111 require_once ABSPATH . '/wp-admin/includes/file.php'; 112 WP_Filesystem(); 113 } 114 $contents = $wp_filesystem->get_contents($git_head_file); 115 if ($contents && strpos($contents, 'ref:') === 0) { 116 $current_branch = trim(str_replace('ref: refs/heads/', '', $contents)); 117 $git_status = 'Branch: ' . $current_branch; 118 $status_class = 'has-git'; 119 } 120 } 121 } 122 ?> 123 <div class="qa-plugin-card <?php echo esc_attr($status_class); ?>" data-plugin-dir="<?php echo esc_attr($plugin_dir); ?>"> 124 <div class="qa-plugin-header"> 125 <h4><?php echo esc_html($plugin_name); ?></h4> 126 <span class="qa-plugin-dir"><?php echo esc_html($plugin_dir); ?></span> 127 </div> 128 <div class="qa-plugin-status"> 129 <span class="qa-git-status <?php echo esc_attr($status_class); ?>"> 130 <?php if ($is_git_repo): ?> 131 <span class="dashicons dashicons-admin-tools"></span> 132 <?php else: ?> 133 <span class="dashicons dashicons-warning"></span> 134 <?php endif; ?> 135 <?php echo esc_html($git_status); ?> 136 </span> 137 </div> 138 </div> 139 <?php 140 } 141 ?> 142 </div> 143 </div> 144 <?php 145 } 146 ?> 147 148 <?php } else { ?> 149 150 <h2><?php esc_html_e('No plugins available.', 'qa-assistant'); ?></h2> 151 152 <?php } ?> 153 </div> 154 </div> 155 156 </div> 157 158 </div> 7 <div id="qa-assistant-dashboard"></div> -
qa-assistant/tags/2.0.0/vendor/composer/autoload_classmap.php
r3370854 r3469660 24 24 'CzProject\\GitPhp\\Runners\\OldGitRunner' => $vendorDir . '/czproject/git-php/src/Runners/OldGitRunner.php', 25 25 'CzProject\\GitPhp\\StaticClassException' => $vendorDir . '/czproject/git-php/src/exceptions.php', 26 'QaAssistant\\API' => $baseDir . '/includes/API.php',27 'QaAssistant\\Admin' => $baseDir . '/includes/Admin.php',28 'QaAssistant\\Admin\\Menu' => $baseDir . '/includes/Admin/Menu.php',29 'QaAssistant\\Admin\\Settings' => $baseDir . '/includes/Admin/Settings.php',30 'QaAssistant\\Ajax' => $baseDir . '/includes/Ajax.php',31 'QaAssistant\\Assets' => $baseDir . '/includes/Assets.php',32 'QaAssistant\\Frontend' => $baseDir . '/includes/Frontend.php',33 'QaAssistant\\Frontend\\Shortcode' => $baseDir . '/includes/Frontend/Shortcode.php',34 'QaAssistant\\GitManager' => $baseDir . '/includes/GitManager.php',35 'QaAssistant\\Installer' => $baseDir . '/includes/Installer.php',36 26 ); -
qa-assistant/tags/2.0.0/vendor/composer/autoload_static.php
r3370854 r3469660 12 12 13 13 public static $prefixLengthsPsr4 = array ( 14 'Q' => 14 'Q' => 15 15 array ( 16 16 'QaAssistant\\' => 12, … … 19 19 20 20 public static $prefixDirsPsr4 = array ( 21 'QaAssistant\\' => 21 'QaAssistant\\' => 22 22 array ( 23 23 0 => __DIR__ . '/../..' . '/includes', … … 43 43 'CzProject\\GitPhp\\Runners\\OldGitRunner' => __DIR__ . '/..' . '/czproject/git-php/src/Runners/OldGitRunner.php', 44 44 'CzProject\\GitPhp\\StaticClassException' => __DIR__ . '/..' . '/czproject/git-php/src/exceptions.php', 45 'QaAssistant\\API' => __DIR__ . '/../..' . '/includes/API.php',46 'QaAssistant\\Admin' => __DIR__ . '/../..' . '/includes/Admin.php',47 'QaAssistant\\Admin\\Menu' => __DIR__ . '/../..' . '/includes/Admin/Menu.php',48 'QaAssistant\\Admin\\Settings' => __DIR__ . '/../..' . '/includes/Admin/Settings.php',49 'QaAssistant\\Ajax' => __DIR__ . '/../..' . '/includes/Ajax.php',50 'QaAssistant\\Assets' => __DIR__ . '/../..' . '/includes/Assets.php',51 'QaAssistant\\Frontend' => __DIR__ . '/../..' . '/includes/Frontend.php',52 'QaAssistant\\Frontend\\Shortcode' => __DIR__ . '/../..' . '/includes/Frontend/Shortcode.php',53 'QaAssistant\\GitManager' => __DIR__ . '/../..' . '/includes/GitManager.php',54 'QaAssistant\\Installer' => __DIR__ . '/../..' . '/includes/Installer.php',55 45 ); 56 46 -
qa-assistant/tags/2.0.0/vendor/composer/installed.json
r3370854 r3469660 61 61 } 62 62 ], 63 "dev": false,63 "dev": true, 64 64 "dev-package-names": [] 65 65 } -
qa-assistant/tags/2.0.0/vendor/composer/installed.php
r3370854 r3469660 4 4 'pretty_version' => 'dev-main', 5 5 'version' => 'dev-main', 6 'reference' => ' 86dd52d03562c58827cfda99707efa65f6b31e7a',6 'reference' => '4b3e48af653456751624d26a957aa04e19a791b1', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', 9 9 'aliases' => array(), 10 'dev' => false,10 'dev' => true, 11 11 ), 12 12 'versions' => array( … … 23 23 'pretty_version' => 'dev-main', 24 24 'version' => 'dev-main', 25 'reference' => ' 86dd52d03562c58827cfda99707efa65f6b31e7a',25 'reference' => '4b3e48af653456751624d26a957aa04e19a791b1', 26 26 'type' => 'wordpress-plugin', 27 27 'install_path' => __DIR__ . '/../../', -
qa-assistant/tags/2.0.0/vendor/composer/platform_check.php
r3370854 r3469660 20 20 } 21 21 } 22 trigger_error( 23 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 E_USER_ERROR 22 throw new \RuntimeException( 23 'Composer detected issues in your platform: ' . implode(' ', $issues) 25 24 ); 26 25 } -
qa-assistant/trunk/assets/css/admin.css
r3370854 r3469660 1 /* Enhanced Git Branch Dropdown Styles */ 2 .qa_assistant_git-branch .ab-sub-wrapper { 3 width: auto; 4 height: 320px !important; 5 overflow-y: auto; 6 border-radius: 12px !important; 7 box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1) !important; 1 /* 2 * QA Assistant Admin Bar Styles 3 * Theme: Tailwind/Shadcn inspired (Slate 50-900) 4 */ 5 6 /* --- Dropdown Container --- */ 7 #wpadminbar .qa_assistant_git-branch-group .ab-sub-wrapper, 8 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper, 9 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper { 10 background: #ffffff !important; 8 11 border: 1px solid #e2e8f0 !important; 9 background: #ffffff !important; 10 backdrop-filter: blur(10px) !important; 11 } 12 13 /* Custom scrollbar for dropdown */ 14 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar { 15 width: 6px; 16 } 17 18 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar-track { 19 background: #f1f5f9; 20 border-radius: 3px; 21 } 22 23 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar-thumb { 24 background: linear-gradient(135deg, #cbd5e1, #94a3b8); 25 border-radius: 3px; 26 transition: background 0.2s ease; 27 } 28 29 .qa_assistant_git-branch .ab-sub-wrapper::-webkit-scrollbar-thumb:hover { 30 background: linear-gradient(135deg, #94a3b8, #64748b); 31 } 32 33 /* Enhanced branch item styling */ 34 .qa_assistant_git-branch-list-items { 35 position: relative; 36 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 37 margin: 2px 4px !important; 12 /* Slate-200 */ 38 13 border-radius: 8px !important; 39 overflow: hidden !important; 40 } 41 42 .qa_assistant_git-branch-list-items:hover > div { 43 cursor: pointer !important; 44 border: 1px solid #e5e7eb !important; 45 background: linear-gradient(135deg, #f9fafb, #f3f4f6) !important; 46 transform: translateX(2px) !important; 47 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08) !important; 48 border-left: 3px solid #3b82f6 !important; 49 } 50 51 .qa_assistant_git-branch-list-items:hover .ab-item { 52 color: #1f2937 !important; 53 font-weight: 600 !important; 54 } 55 56 /* Enhanced hover effect for regular branch items (excluding current branch) */ 57 .qa_assistant_git-branch-list-items:not(.current-branch):hover > div { 58 background: linear-gradient(135deg, #f8fafc, #f1f5f9) !important; 59 border: 1px solid #e2e8f0 !important; 60 border-left: 3px solid #3b82f6 !important; 61 transform: translateX(2px) !important; 62 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1) !important; 63 } 64 65 /* Enhanced current branch indicator */ 66 .qa_assistant_git-branch-list-items.current-branch > div { 67 background: linear-gradient(135deg, #dcfce7, #bbf7d0) !important; 68 border: 1px solid #22c55e !important; 69 border-left: 4px solid #16a34a !important; 70 font-weight: 600 !important; 71 color: #15803d !important; 72 position: relative !important; 73 box-shadow: 0 2px 8px rgba(34, 197, 94, 0.15) !important; 74 } 75 76 .qa_assistant_git-branch-list-items.current-branch .ab-item { 77 color: #15803d !important; 78 font-weight: 600 !important; 79 } 80 81 /* Enhanced hover effect for current branch */ 82 .qa_assistant_git-branch-list-items.current-branch:hover > div { 83 background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important; 84 border: 1px solid #22c55e !important; 85 border-left: 4px solid #16a34a !important; 86 transform: translateX(2px) !important; 87 box-shadow: 0 3px 8px rgba(34, 197, 94, 0.2) !important; 88 } 89 90 .qa_assistant_git-branch-list-items.current-branch:hover .ab-item { 91 color: #14532d !important; 92 } 93 94 .qa_assistant_git-branch-list-items.current-branch > div::before { 95 content: "✓"; 96 color: #16a34a; 97 font-weight: 700; 98 margin-right: 8px; 99 font-size: 14px; 100 filter: drop-shadow(0 1px 2px rgba(22, 163, 74, 0.3)); 101 } 102 103 .qa_assistant_git-branch-list-items.current-branch > div::after { 104 content: "CURRENT"; 105 position: absolute; 106 right: 8px; 107 top: 50%; 108 transform: translateY(-50%); 109 background: #16a34a; 110 color: white; 111 font-size: 9px; 112 font-weight: 700; 113 padding: 2px 6px; 114 border-radius: 4px; 115 letter-spacing: 0.5px; 116 } 117 118 /* Branch switching state */ 119 .qa_assistant_git-branch-list-items.switching-branch { 120 opacity: 0.6; 121 pointer-events: none; 122 } 123 124 .qa_assistant_git-branch-list-items.switching-branch > div { 125 background-color: rgba(255, 193, 7, 0.2)!important; 126 border-left: 3px solid #ffc107!important; 127 } 128 129 .qa_assistant_git-branch-list-items.switching-branch .ab-item { 130 color: #92400e !important; 131 font-weight: 600 !important; 132 } 133 134 /* Enhanced loader with SVG spinner */ 135 .qa-branch-loader { 136 display: inline-block; 137 margin-left: 8px; 138 width: 16px; 139 height: 16px; 140 } 141 142 .qa-spinner { 143 width: 16px; 144 height: 16px; 145 animation: qa-spin 1s linear infinite; 146 } 147 148 .qa-spinner-path { 149 animation: qa-spinner-dash 1.5s ease-in-out infinite; 150 } 151 152 @keyframes qa-spin { 153 0% { transform: rotate(0deg); } 154 100% { transform: rotate(360deg); } 155 } 156 157 @keyframes qa-spinner-dash { 158 0% { 159 stroke-dasharray: 1, 150; 160 stroke-dashoffset: 0; 161 } 162 50% { 163 stroke-dasharray: 90, 150; 164 stroke-dashoffset: -35; 165 } 166 100% { 167 stroke-dasharray: 90, 150; 168 stroke-dashoffset: -124; 169 } 170 } 171 172 /* Enhanced keyboard-based branch search styling */ 173 .qa-branch-search-hint { 174 background: linear-gradient(135deg, #f8fafc, #e2e8f0) !important; 175 border: 1px solid #cbd5e1 !important; 176 border-radius: 6px !important; 177 margin: 4px !important; 178 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; 179 cursor: default !important; 180 pointer-events: none !important; 181 position: relative !important; 182 overflow: hidden !important; 183 } 184 185 .qa-branch-search-hint::before { 186 content: ""; 187 position: absolute; 188 top: 0; 189 left: 0; 190 right: 0; 191 height: 2px; 192 background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4) !important; 193 animation: qa-search-gradient 3s ease-in-out infinite !important; 194 } 195 196 .qa-branch-search-hint .ab-item { 197 padding: 12px 16px !important; 198 font-size: 13px !important; 199 color: #475569 !important; 200 font-weight: 500 !important; 201 font-style: normal !important; 202 } 203 204 /* Remove duplicate search icon - original text already has 🔍 */ 205 206 @keyframes qa-search-gradient { 207 0%, 100% { transform: translateX(-100%); } 208 50% { transform: translateX(100%); } 209 } 210 211 /* Enhanced branch items styling */ 212 .qa_assistant_git-branch .ab-sub-wrapper .qa_assistant_git-branch-list-items .ab-item { 213 padding: 10px 12px !important; 214 font-size: 13px !important; 215 line-height: 1.4 !important; 216 border: none !important; 217 margin: 0 !important; 218 transition: all 0.2s ease !important; 219 color: #374151 !important; 220 font-weight: 500 !important; 221 } 222 223 /* Consistent spacing for all branch dropdown items */ 224 .qa_assistant_git-branch .ab-sub-wrapper .ab-item { 225 padding: 10px 12px !important; 226 font-size: 13px !important; 227 line-height: 1.4 !important; 228 white-space: nowrap !important; 229 overflow: hidden !important; 230 text-overflow: ellipsis !important; 231 color: #374151 !important; 232 font-weight: 500 !important; 233 } 234 235 /* Remove any conflicting margins/padding */ 236 .qa_assistant_git-branch .ab-sub-wrapper li { 237 margin: 0 !important; 238 padding: 0 !important; 239 } 240 241 /* Ensure all branch items have good text color visibility */ 242 .qa_assistant_git-branch .ab-sub-wrapper .ab-item, 243 .qa_assistant_git-branch .ab-sub-wrapper a { 244 color: #374151 !important; 245 text-decoration: none !important; 246 } 247 248 /* Override WordPress admin bar default colors */ 249 .qa_assistant_git-branch .ab-sub-wrapper .ab-item:hover, 250 .qa_assistant_git-branch .ab-sub-wrapper a:hover { 251 color: #1e40af !important; 252 } 253 254 /* Ensure consistent width for dropdown */ 255 .qa_assistant_git-branch .ab-sub-wrapper { 256 min-width: 220px !important; 257 max-width: 300px !important; 258 } 259 260 /* Highlight matching characters during search */ 261 .qa-branch-highlight { 262 background-color: #fff3cd !important; 263 color: #856404 !important; 264 font-weight: bold !important; 265 } 266 267 /* Hidden branches during search */ 268 .qa-branch-hidden { 269 display: none !important; 270 } 271 272 /* Enhanced search active state */ 273 .qa-branch-search-active .qa-branch-search-hint { 274 background: linear-gradient(135deg, #dbeafe, #bfdbfe) !important; 275 border-color: #3b82f6 !important; 276 box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15) !important; 277 } 278 279 .qa-branch-search-active .qa-branch-search-hint::before { 280 background: linear-gradient(90deg, #1d4ed8, #3b82f6, #06b6d4) !important; 281 animation: qa-search-active-gradient 2s ease-in-out infinite !important; 282 } 283 284 .qa-branch-search-active .qa-branch-search-hint .ab-item { 285 color: #1e40af !important; 286 font-weight: 600 !important; 287 } 288 289 /* Remove duplicate active search icon - will keep original 🔍 */ 290 291 @keyframes qa-search-active-gradient { 292 0%, 100% { transform: translateX(-100%) scaleX(1); } 293 50% { transform: translateX(100%) scaleX(1.2); } 294 } 295 296 /* Enhanced blinking cursor animation */ 297 .qa-search-cursor { 298 display: inline-block; 299 margin-left: 4px; 300 animation: qa-cursor-blink 1.2s infinite; 301 font-weight: 600 !important; 14 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; 15 padding: 6px !important; 16 min-width: 260px !important; 17 max-width: 320px !important; 18 } 19 20 /* --- Menu Items General --- */ 21 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper .ab-item, 22 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper a.ab-item, 23 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper .ab-item, 24 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper a.ab-item { 302 25 color: #64748b !important; 303 font-size: 14px !important; 304 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important; 305 } 306 307 @keyframes qa-cursor-blink { 308 0%, 45% { 309 opacity: 1; 310 transform: scaleY(1); 311 } 312 46%, 100% { 313 opacity: 0; 314 transform: scaleY(0.8); 315 } 316 } 317 318 /* Enhanced cursor styling in different states */ 319 .qa-branch-search-hint .qa-search-cursor { 320 color: #64748b !important; 321 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; 322 } 323 324 .qa-branch-search-active .qa-search-cursor { 325 color: #1e40af !important; 326 text-shadow: 0 1px 3px rgba(30, 64, 175, 0.3) !important; 327 animation: qa-cursor-active-blink 1s infinite !important; 328 } 329 330 @keyframes qa-cursor-active-blink { 331 0%, 40% { 332 opacity: 1; 333 transform: scaleY(1) scaleX(1); 334 } 335 41%, 100% { 336 opacity: 0; 337 transform: scaleY(0.9) scaleX(1.1); 338 } 339 } 340 341 /* Modern minimal pull button styling */ 342 .qa-pull-button { 343 background: #ffffff !important; 344 border: 1px solid #e5e7eb !important; 345 border-radius: 6px !important; 346 color: #374151 !important; 347 font-weight: 500 !important; 348 transition: all 0.2s ease !important; 349 margin: 4px !important; 350 position: relative !important; 351 overflow: hidden !important; 352 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; 353 } 354 355 .qa-pull-button::before { 356 content: ""; 357 position: absolute; 358 top: 0; 359 left: -100%; 360 width: 100%; 361 height: 100%; 362 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important; 363 transition: left 0.5s ease !important; 364 } 365 366 .qa-pull-button .ab-item { 367 padding: 10px 14px !important; 368 color: #374151 !important; 26 /* Slate-500 */ 369 27 font-size: 13px !important; 370 28 font-weight: 500 !important; 371 position: relative !important; 372 z-index: 1 !important; 29 padding: 6px 10px !important; 30 border-radius: 6px !important; 31 transition: all 0.15s ease !important; 32 background: transparent !important; 33 height: auto !important; 34 line-height: 1.5 !important; 35 margin-bottom: 2px !important; 373 36 display: flex !important; 374 37 align-items: center !important; … … 376 39 } 377 40 378 .qa-pull-button:hover { 379 background: #f9fafb !important; 380 border-color: #10b981 !important; 381 transform: translateY(-1px) !important; 382 box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15) !important; 383 } 384 385 .qa-pull-button:hover .ab-item { 386 color: #10b981 !important; 387 } 388 389 .qa-pull-button:active { 390 transform: translateY(0px) !important; 391 box-shadow: 0 1px 3px rgba(16, 185, 129, 0.2) !important; 392 } 393 394 /* Modern loading and disabled states */ 395 .qa-pull-button[disabled], 396 .qa-pull-button.qa-pull-loading { 397 background: #f3f4f6 !important; 398 border-color: #d1d5db !important; 399 cursor: not-allowed !important; 400 transform: none !important; 401 box-shadow: none !important; 402 opacity: 0.7 !important; 403 } 404 405 .qa-pull-button.qa-pull-loading .ab-item { 406 color: #6b7280 !important; 407 } 408 409 /* Enhanced pull loader animation */ 410 .qa-pull-loader { 411 display: inline-block; 412 animation: qa-pull-spin 1.2s linear infinite; 413 margin-right: 6px; 414 font-size: 16px !important; 415 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)) !important; 416 } 417 418 @keyframes qa-pull-spin { 419 0% { transform: rotate(0deg); } 420 100% { transform: rotate(360deg); } 421 } 422 423 @keyframes qa-loading-shimmer { 424 0% { left: -100%; } 425 50% { left: 0%; } 426 100% { left: 100%; } 427 } 428 429 /* Enhanced switching state */ 430 .qa_assistant_git-branch-list-items.switching-branch > div { 431 background-color: rgba(255, 193, 7, 0.3) !important; 432 border-left: 3px solid #ffc107 !important; 433 position: relative !important; 434 } 435 436 .qa_assistant_git-branch-list-items.switching-branch > div::after { 437 content: ""; 438 position: absolute; 439 top: 0; 440 left: 0; 441 right: 0; 442 bottom: 0; 443 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); 444 animation: qa-switching-shimmer 1.5s infinite; 445 } 446 447 @keyframes qa-switching-shimmer { 448 0% { transform: translateX(-100%); } 449 100% { transform: translateX(100%); } 450 } 451 452 /* Modern minimal refresh button styling */ 453 .qa-refresh-button { 454 background: #ffffff !important; 455 border: 1px solid #e5e7eb !important; 456 border-radius: 6px !important; 457 color: #374151 !important; 458 font-weight: 500 !important; 459 transition: all 0.2s ease !important; 460 margin: 4px !important; 461 position: relative !important; 462 overflow: hidden !important; 463 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; 464 } 465 466 .qa-refresh-button::before { 467 content: ""; 468 position: absolute; 469 top: 0; 470 left: -100%; 471 width: 100%; 472 height: 100%; 473 background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important; 474 transition: left 0.5s ease !important; 475 } 476 477 .qa-refresh-button .ab-item { 478 padding: 10px 14px !important; 479 color: #374151 !important; 480 font-size: 13px !important; 481 font-weight: 500 !important; 482 position: relative !important; 483 z-index: 1 !important; 41 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper .ab-item:hover, 42 #wpadminbar .qa_assistant_git-branch .ab-sub-wrapper a.ab-item:hover, 43 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper .ab-item:hover, 44 #wpadminbar .qa-admin-bar-root .ab-sub-wrapper a.ab-item:hover { 45 background-color: #f1f5f9 !important; 46 /* Slate-100 */ 47 color: #0f172a !important; 48 /* Slate-900 */ 49 } 50 51 /* --- Key Elements --- */ 52 53 /* Root Icon */ 54 .qa-admin-bar-icon { 55 display: inline-flex !important; 56 align-items: center !important; 57 margin-right: 4px !important; 58 color: #a1a1aa !important; 59 /* Zinc-400 */ 60 margin-top: 2px !important; 61 /* Visual alignment fix */ 62 } 63 64 #wpadminbar:hover .qa-admin-bar-icon { 65 color: #fff !important; 66 } 67 68 /* Parent Item Adjustment */ 69 #wpadminbar .qa-admin-bar-root>.ab-item, 70 #wpadminbar .qa_assistant_git-branch>.ab-item { 484 71 display: flex !important; 485 72 align-items: center !important; 486 gap: 8px !important; 487 } 488 489 .qa-refresh-button:hover { 490 background: #f9fafb !important; 491 border-color: #3b82f6 !important; 492 transform: translateY(-1px) !important; 493 box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15) !important; 494 } 495 496 .qa-refresh-button:hover .ab-item { 497 color: #3b82f6 !important; 498 } 499 500 .qa-refresh-button:active { 501 transform: translateY(0px) !important; 502 box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2) !important; 503 } 504 505 /* Modern refresh loading and disabled states */ 506 .qa-refresh-button[disabled], 507 .qa-refresh-button.qa-refresh-loading { 508 background: #f3f4f6 !important; 509 border-color: #d1d5db !important; 510 cursor: not-allowed !important; 511 transform: none !important; 512 box-shadow: none !important; 513 opacity: 0.7 !important; 514 } 515 516 .qa-refresh-button.qa-refresh-loading .ab-item { 517 color: #6b7280 !important; 518 } 519 520 /* Enhanced refresh loader animation */ 521 .qa-refresh-loader { 522 display: inline-block; 523 animation: qa-refresh-spin 1.2s linear infinite; 524 margin-right: 6px; 525 font-size: 16px !important; 526 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)) !important; 527 } 528 529 @keyframes qa-refresh-spin { 530 0% { transform: rotate(0deg); } 531 100% { transform: rotate(360deg); } 532 } 533 534 /* Modern sleek icon styling */ 535 .qa-icon { 536 width: 16px !important; 537 height: 16px !important; 538 display: inline-block !important; 539 vertical-align: middle !important; 540 margin-left: 8px !important; 541 transition: all 0.2s ease !important; 542 flex-shrink: 0 !important; 543 } 544 545 .qa-pull-icon { 546 stroke: currentColor !important; 547 stroke-width: 2 !important; 548 } 549 550 .qa-refresh-icon { 551 stroke: currentColor !important; 552 stroke-width: 2 !important; 553 } 554 555 /* Icon hover animations */ 556 .qa-pull-button:hover .qa-pull-icon { 557 transform: translateY(1px) !important; 558 stroke: #10b981 !important; 559 } 560 561 .qa-refresh-button:hover .qa-refresh-icon { 562 transform: rotate(90deg) !important; 563 stroke: #3b82f6 !important; 564 } 565 566 /* Loading state icon animations */ 567 .qa-pull-button.qa-pull-loading .qa-pull-icon { 568 animation: qa-pull-bounce 1s ease-in-out infinite !important; 569 } 570 571 .qa-refresh-button.qa-refresh-loading .qa-refresh-icon { 572 animation: qa-refresh-spin 1s linear infinite !important; 573 } 574 575 @keyframes qa-pull-bounce { 576 0%, 100% { transform: translateY(0px); } 577 50% { transform: translateY(-2px); } 578 } 579 580 /* Settings Page Styles */ 581 .qa-assistant-content { 582 max-width: 800px; 583 margin: 2rem 0; 584 padding: 2rem; 585 background: #fff; 586 border-radius: 8px; 587 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 588 } 589 590 .qa-assistant-content h1 { 591 font-size: 2rem; 73 } 74 75 /* Fix arrow alignment on parent items */ 76 #wpadminbar .qa-admin-bar-root>.ab-item::after, 77 #wpadminbar .qa_assistant_git-branch>.ab-item::after { 78 top: 50% !important; 79 transform: translateY(-50%) !important; 80 margin-top: 0 !important; 81 } 82 83 /* Repo Name & Badge */ 84 .qa-repo-name { 85 color: #0f172a !important; 86 /* Slate-900 */ 87 font-weight: 600 !important; 88 margin-right: auto !important; 89 } 90 91 .qa-branch-badge { 92 background: #f1f5f9 !important; 93 /* Slate-100 */ 94 color: #475569 !important; 95 /* Slate-600 */ 96 font-size: 11px !important; 97 padding: 2px 6px !important; 98 border-radius: 4px !important; 99 font-family: monospace !important; 100 border: 1px solid #e2e8f0 !important; 101 } 102 103 /* --- Toolbar (Pull & Refresh) --- */ 104 #wpadminbar .qa-toolbar-container { 105 padding: 8px 8px 8px 8px; 106 border-bottom: 1px solid #e2e8f0; 107 /* Slate-200 */ 108 background-color: #ffffff; 109 /* Slate-50 */ 110 border-radius: 8px 8px 0 0; 111 margin-bottom: 4px; 112 } 113 114 #wpadminbar .qa-branch-toolbar { 115 display: flex; 116 align-items: center; 117 gap: 6px; 118 } 119 120 #wpadminbar .qa-toolbar-btn { 121 flex: 1; 122 display: flex; 123 align-items: center; 124 justify-content: center; 125 gap: 6px; 126 background: #f8fafc; 127 /* Subtle background */ 128 border: 1px solid #e2e8f0; 129 /* Define border */ 130 cursor: pointer; 131 color: #475569; 132 /* Slate-600 */ 133 font-size: 12px; 134 font-weight: 500; 135 padding: 6px 12px; 136 border-radius: 6px; 137 transition: all 0.2s; 138 line-height: 1; 139 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 140 } 141 142 #wpadminbar .qa-toolbar-btn:hover { 143 color: #0f172a; 144 /* Slate-900 */ 145 background-color: #ffffff; 146 border-color: #cbd5e1; 147 box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 148 } 149 150 #wpadminbar .qa-toolbar-btn:active { 151 background-color: #f1f5f9; 152 box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.05); 153 } 154 155 #wpadminbar .qa-toolbar-btn svg { 156 display: block; 157 color: #64748b; 158 } 159 160 #wpadminbar .qa-toolbar-btn:hover svg { 161 color: #3b82f6; 162 /* Blue-500 hover */ 163 } 164 165 /* Loading state for buttons */ 166 #wpadminbar .qa-toolbar-btn.qa-pull-loading, 167 #wpadminbar .qa-toolbar-btn.qa-refresh-loading { 168 opacity: 0.7; 169 cursor: wait; 170 background-color: #f1f5f9; 171 } 172 173 /* --- Search Input --- */ 174 #wpadminbar .qa-search-node { 175 padding: 0 8px 8px 8px; 176 background-color: #ffffff; 177 border-bottom: 1px solid #f1f5f9; 178 /* Slate-100 */ 179 } 180 181 #wpadminbar .qa-search-container { 182 position: relative; 183 display: flex; 184 align-items: center; 185 } 186 187 #wpadminbar .qa-search-icon { 188 position: absolute; 189 left: 10px; 190 color: #94a3b8; 191 /* Slate-400 */ 192 pointer-events: none; 193 width: 14px; 194 height: 14px; 195 } 196 197 #wpadminbar .qa-branch-search-input { 198 width: 100%; 199 padding: 6px 10px 6px 30px; 200 /* Left padding for icon */ 201 font-size: 13px; 202 line-height: 1.5; 203 color: #0f172a; 204 /* Slate-900 */ 205 background-color: #ffffff; 206 border: 1px solid #e2e8f0; 207 /* Slate-200 */ 208 border-radius: 6px; 209 transition: all 0.2s; 210 outline: none; 211 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 212 height: 32px; 213 min-height: 32px; 214 } 215 216 #wpadminbar .qa-branch-search-input:focus { 217 border-color: #3b82f6; 218 /* Blue-500 */ 219 box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); 220 } 221 222 #wpadminbar .qa-branch-search-input::placeholder { 223 color: #94a3b8; 224 /* Slate-400 */ 225 } 226 227 /* --- Scrollable List --- */ 228 #wpadminbar .qa-branch-list-scrollable, 229 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper { 230 max-height: 300px !important; 231 overflow-y: auto !important; 232 overflow-x: hidden; 233 padding-top: 4px; 234 padding-bottom: 4px; 235 border-radius: 0 0 8px 8px; 236 } 237 238 /* --- Branch Items --- */ 239 #wpadminbar .qa_assistant_git-branch-item .ab-item { 240 height: auto !important; 241 padding: 6px 10px !important; 242 line-height: 1.5 !important; 243 border-left: 2px solid transparent !important; 244 /* Prepare for active border */ 245 } 246 247 #wpadminbar .qa-branch-row { 248 display: flex; 249 align-items: center; 250 justify-content: space-between; 251 width: 100%; 252 } 253 254 #wpadminbar .qa-branch-name { 255 font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace; 256 font-size: 12px; 257 color: #475569; 258 /* Slate-600 */ 259 } 260 261 #wpadminbar .qa-branch-row:hover .qa-branch-name { 262 color: #0f172a; 263 /* Slate-900 */ 264 } 265 266 /* Main Branch styling */ 267 #wpadminbar .qa_assistant_git-branch-item.main-branch .qa-branch-name { 592 268 font-weight: 600; 593 color: #23282d; 594 margin-bottom: 1.5rem; 595 line-height: 1.3; 596 } 597 598 .qa-assistant-tabs { 599 border-bottom: 2px solid #e2e4e7; 600 margin-bottom: 2rem; 601 } 602 603 .qa-assistant-tab-link { 604 display: inline-block; 605 padding: 0.75rem 1.5rem; 606 font-size: 1rem; 607 font-weight: 500; 608 color: #646970; 609 text-decoration: none; 610 border-bottom: 2px solid transparent; 611 margin-bottom: -2px; 612 transition: all 0.3s ease; 613 } 614 615 .qa-assistant-tab-link:hover, 616 .qa-assistant-tab-link.active { 617 color: #2271b1; 618 border-bottom-color: #2271b1; 619 } 620 621 .qa-assistant-form { 622 display: flex; 623 flex-direction: column; 624 gap: 1.5rem; 625 } 626 627 .qa-assistant-form h2 { 628 font-size: 1.25rem; 629 font-weight: 500; 630 color: #1d2327; 631 margin: 0; 632 } 633 634 #qa-assistant__description { 635 font-size: 0.9rem; 636 color: #646970; 637 margin: 0.5rem 0; 638 } 639 640 /* Feature info section */ 641 .qa-assistant-feature-info { 642 background: #f8f9fa; 643 border: 1px solid #e2e4e7; 644 border-radius: 6px; 645 padding: 1.5rem; 646 margin: 1.5rem 0; 647 } 648 649 .qa-assistant-feature-info h3 { 650 margin: 0 0 1rem 0; 651 color: #1d2327; 652 font-size: 1.1rem; 653 } 654 655 .qa-assistant-feature-info ul { 656 margin: 0; 657 padding-left: 0; 658 list-style: none; 659 } 660 661 .qa-assistant-feature-info li { 662 margin: 0.75rem 0; 663 padding: 0.5rem 0; 664 border-bottom: 1px solid #e2e4e7; 665 font-size: 0.9rem; 666 line-height: 1.4; 667 } 668 669 .qa-assistant-feature-info li:last-child { 670 border-bottom: none; 671 } 672 673 /* Selected plugins display */ 674 .qa-assistant-selected-plugins { 675 margin-top: 2rem; 676 padding: 1.5rem; 677 background: #fff; 678 border: 1px solid #e2e4e7; 679 border-radius: 6px; 680 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 681 } 682 683 .qa-assistant-selected-plugins h3 { 684 margin: 0 0 1.5rem 0; 685 color: #1d2327; 686 font-size: 1.2rem; 269 color: #334155; 270 } 271 272 /* Current Branch Indicator & Active Row */ 273 #wpadminbar .qa_assistant_git-branch-item.current .ab-item { 274 background-color: #e0f2fe !important; 275 /* Sky-100 (darker than previous slate-50/sky-50) */ 276 border-left-color: #0284c7 !important; 277 /* Sky-600 */ 278 } 279 280 #wpadminbar .qa_assistant_git-branch-item.current .qa-branch-name { 281 color: #0369a1; 282 /* Sky-700 */ 283 font-weight: 700; 284 } 285 286 #wpadminbar .qa-current-indicator { 287 display: inline-flex; 288 align-items: center; 289 padding: 2px 6px; 290 border-radius: 4px; 291 background-color: #e0f2fe; 292 /* Sky-100 */ 293 color: #0369a1; 294 /* Sky-700 */ 295 font-size: 10px; 687 296 font-weight: 600; 688 } 689 690 .qa-selected-plugins-grid { 691 display: grid; 692 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 693 gap: 1rem; 694 } 695 696 .qa-plugin-card { 697 background: #f8f9fa; 698 border: 1px solid #e2e4e7; 699 border-radius: 6px; 700 padding: 1rem; 701 transition: all 0.2s ease; 702 } 703 704 .qa-plugin-card:hover { 705 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 706 transform: translateY(-1px); 707 } 708 709 .qa-plugin-card.has-git { 710 border-left: 4px solid #2ea043; 711 } 712 713 .qa-plugin-card.no-git { 714 border-left: 4px solid #f85149; 715 } 716 717 .qa-plugin-header h4 { 718 margin: 0 0 0.5rem 0; 719 font-size: 1rem; 297 line-height: 1; 298 text-transform: uppercase; 299 letter-spacing: 0.025em; 300 } 301 302 /* Highlight */ 303 .qa-branch-highlight { 304 background-color: #fef08a; 305 /* Yellow-200 */ 306 color: #854d0e; 307 /* Yellow-800 */ 308 padding: 0 2px; 309 border-radius: 2px; 720 310 font-weight: 600; 721 color: #1d2327; 722 } 723 724 .qa-plugin-dir { 725 font-size: 0.85rem; 726 color: #646970; 727 font-family: monospace; 728 background: #e2e4e7; 729 padding: 2px 6px; 730 border-radius: 3px; 731 } 732 733 .qa-plugin-status { 734 margin-top: 0.75rem; 735 } 736 737 .qa-git-status { 738 display: flex; 739 align-items: center; 740 font-size: 0.9rem; 741 font-weight: 500; 742 } 743 744 .qa-git-status .dashicons { 745 margin-right: 0.5rem; 746 font-size: 16px; 747 } 748 749 .qa-git-status.has-git { 750 color: #2ea043; 751 } 752 753 .qa-git-status.no-git { 754 color: #f85149; 755 } 756 757 /* Select2 Enhancements */ 758 .select2-container--default .select2-selection--multiple { 759 border-color: #8c8f94; 760 border-radius: 4px; 761 min-height: 36px; 762 transition: border-color 0.3s ease; 763 width: 100% !important; 764 } 765 766 .select2-container--default.select2-container--focus .select2-selection--multiple { 767 border-color: #2271b1; 768 box-shadow: 0 0 0 1px #2271b1; 769 } 770 771 .select2-container--default .select2-selection--multiple .select2-selection__choice { 772 background-color: #f0f0f1; 773 border: 1px solid #c3c4c7; 774 border-radius: 3px; 775 padding: 4px 8px; 776 margin: 4px; 311 } 312 313 /* Empty State */ 314 .qa-no-branches { 315 padding: 12px !important; 316 text-align: center; 317 color: #94a3b8 !important; 318 font-style: italic; 777 319 font-size: 13px; 778 transition: all 0.2s ease; 779 } 780 781 .select2-container--default .select2-selection--multiple .select2-selection__choice:hover { 782 background-color: #e5e5e5; 783 } 784 785 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { 786 color: #646970; 787 margin-right: 6px; 788 transition: color 0.2s ease; 789 } 790 791 .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { 792 color: #d63638; 793 } 794 795 .select2-dropdown { 796 border-color: #8c8f94; 797 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); 798 animation: select2DropdownFade 0.2s ease-in-out; 799 transform-origin: top; 800 } 801 802 @keyframes select2DropdownFade { 803 from { 804 opacity: 0; 805 transform: translateY(-10px); 320 cursor: default; 321 } 322 323 .qa-no-branches:hover { 324 background-color: transparent !important; 325 } 326 327 /* Scrollbar Styling */ 328 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar { 329 width: 6px; 330 } 331 332 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar-track { 333 background: transparent; 334 } 335 336 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar-thumb { 337 background-color: #cbd5e1; 338 /* Slate-300 */ 339 border-radius: 20px; 340 } 341 342 #wpadminbar .qa-branch-list-scrollable .ab-sub-wrapper::-webkit-scrollbar-thumb:hover { 343 background-color: #94a3b8; 344 /* Slate-400 */ 345 } 346 347 /* Animations */ 348 @keyframes spin { 349 to { 350 transform: rotate(360deg); 806 351 } 807 to { 808 opacity: 1; 809 transform: translateY(0); 810 } 811 } 812 813 .select2-container { 814 width: 100% !important; 815 } 816 817 .select2-search--dropdown { 818 padding: 8px; 819 transition: all 0.2s ease; 820 } 821 822 .select2-search--dropdown .select2-search__field { 823 padding: 6px; 824 border-radius: 4px; 825 transition: all 0.2s ease; 826 } 827 828 .select2-search--dropdown .select2-search__field:focus { 829 border-color: #2271b1; 830 box-shadow: 0 0 0 1px #2271b1; 831 outline: none; 832 } 833 834 .select2-container--default .select2-results__option--highlighted[aria-selected] { 835 background-color: #2271b1; 836 } 837 838 /* Save Button and Spinner */ 839 .qa-assistant-form .button-primary { 840 align-self: flex-start; 841 margin-top: 1rem; 842 padding: 0.5rem 1.5rem; 843 height: auto; 844 transition: all 0.2s ease; 845 } 846 847 .qa-assistant-form .button-primary:hover { 848 background-color: #135e96; 849 } 850 851 #qa-assistant__spinner { 852 margin-left: 1rem; 853 margin-top: 1.25rem; 854 } 855 856 /* Modern Enhanced Notification System */ 352 } 353 354 .qa-spin { 355 animation: spin 1s linear infinite; 356 } 357 358 /* --- Notification System --- */ 857 359 .qa-notification { 858 360 position: fixed; 859 top: 80px; 860 right: 20px; 361 bottom: 24px; 362 right: 24px; 363 background: #ffffff; 364 border: 1px solid #e2e8f0; 365 border-radius: 8px; 366 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 367 width: 320px; 861 368 z-index: 999999; 862 min-width: 320px; 863 max-width: 420px; 864 background: white; 865 border-radius: 12px; 866 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1); 369 transform: translateX(120%); 370 transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); 867 371 overflow: hidden; 868 transform: translateX(100%);869 opacity: 0;870 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);871 372 } 872 373 873 374 .qa-notification-show { 874 375 transform: translateX(0); 875 opacity: 1;876 376 } 877 377 878 378 .qa-notification-hide { 879 transform: translateX(100%); 880 opacity: 0; 379 transform: translateX(120%); 881 380 } 882 381 … … 884 383 display: flex; 885 384 align-items: flex-start; 886 padding: 16px 20px;385 padding: 16px; 887 386 gap: 12px; 888 387 } … … 892 391 width: 24px; 893 392 height: 24px; 894 border-radius: 50%;895 display: flex;896 align-items: center;897 justify-content: center;898 393 margin-top: 2px; 899 }900 901 .qa-notification-svg {902 width: 16px;903 height: 16px;904 }905 906 .qa-notification-success .qa-notification-icon {907 background: #dcfce7;908 color: #16a34a;909 }910 911 .qa-notification-error .qa-notification-icon {912 background: #fef2f2;913 color: #dc2626;914 }915 916 .qa-notification-info .qa-notification-icon {917 background: #dbeafe;918 color: #2563eb;919 }920 921 .qa-notification-warning .qa-notification-icon {922 background: #fef3c7;923 color: #d97706;924 394 } 925 395 … … 932 402 font-size: 14px; 933 403 font-weight: 600; 934 color: #111827; 935 margin-bottom: 2px; 936 line-height: 1.4; 404 color: #0f172a; 405 margin-bottom: 4px; 937 406 } 938 407 939 408 .qa-notification-message { 940 409 font-size: 13px; 941 color: #6b7280; 942 line-height: 1.4; 943 word-wrap: break-word; 410 color: #475569; 411 line-height: 1.5; 944 412 } 945 413 946 414 .qa-notification-close { 947 415 flex-shrink: 0; 416 width: 20px; 417 height: 20px; 418 padding: 2px; 419 margin: -2px -2px 0 0; 420 background: transparent; 421 border: none; 422 color: #94a3b8; 423 cursor: pointer; 424 border-radius: 4px; 425 transition: all 0.2s; 426 display: flex; 427 align-items: center; 428 justify-content: center; 429 } 430 431 .qa-notification-close:hover { 432 color: #0f172a; 433 background: #f1f5f9; 434 } 435 436 .qa-notification-svg { 437 width: 100%; 438 height: 100%; 439 } 440 441 .qa-notification-success .qa-notification-icon { 442 color: #10b981; 443 } 444 445 .qa-notification-error .qa-notification-icon { 446 color: #ef4444; 447 } 448 449 .qa-notification-warning .qa-notification-icon { 450 color: #f59e0b; 451 } 452 453 .qa-notification-info .qa-notification-icon { 454 color: #3b82f6; 455 } 456 457 .qa-notification-progress { 458 height: 3px; 459 background: #e2e8f0; 460 width: 100%; 461 transform-origin: left; 462 } 463 464 .qa-notification-progress-animate { 465 animation: qa-progress 5s linear forwards; 466 } 467 468 @keyframes qa-progress { 469 0% { 470 transform: scaleX(1); 471 } 472 473 100% { 474 transform: scaleX(0); 475 } 476 } 477 478 .qa-notification-success .qa-notification-progress-animate { 479 background: #10b981; 480 } 481 482 .qa-notification-error .qa-notification-progress-animate { 483 background: #ef4444; 484 } 485 486 .qa-notification-warning .qa-notification-progress-animate { 487 background: #f59e0b; 488 } 489 490 .qa-notification-info .qa-notification-progress-animate { 491 background: #3b82f6; 492 } 493 494 /* --- Action Modal --- */ 495 .qa-action-modal-overlay { 496 position: fixed; 497 top: 0; 498 left: 0; 499 width: 100vw; 500 height: 100vh; 501 background: rgba(15, 23, 42, 0.6); 502 backdrop-filter: blur(4px); 503 display: flex; 504 align-items: center; 505 justify-content: center; 506 z-index: 999999; 507 animation: qaModalFadeIn 0.2s ease-out; 508 } 509 510 .qa-action-modal-content { 511 background: #ffffff; 512 border-radius: 8px; 513 width: 90%; 514 max-width: 450px; 515 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 516 overflow: hidden; 517 display: flex; 518 flex-direction: column; 519 animation: qaModalSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); 520 } 521 522 .qa-action-modal-header { 523 padding: 16px 24px; 524 border-bottom: 1px solid #e2e8f0; 525 display: flex; 526 align-items: center; 527 justify-content: space-between; 528 } 529 530 .qa-action-modal-title { 531 margin: 0; 532 font-size: 16px; 533 font-weight: 600; 534 color: #0f172a; 535 } 536 537 .qa-action-modal-close { 948 538 background: none; 949 539 border: none; 540 color: #64748b; 541 font-size: 24px; 542 line-height: 1; 950 543 cursor: pointer; 951 padding: 4px; 544 padding: 0; 545 display: flex; 546 align-items: center; 547 justify-content: center; 548 transition: color 0.2s; 549 } 550 551 .qa-action-modal-close:hover { 552 color: #0f172a; 553 } 554 555 .qa-action-modal-body { 556 padding: 24px; 557 color: #334155; 558 font-size: 14px; 559 line-height: 1.5; 560 } 561 562 .qa-action-modal-body p { 563 margin-top: 0; 564 margin-bottom: 16px; 565 } 566 567 .qa-commit-section { 568 display: flex; 569 flex-direction: column; 570 gap: 6px; 571 margin-top: 16px; 572 padding-top: 16px; 573 border-top: 1px dashed #cbd5e1; 574 } 575 576 .qa-commit-section label { 577 font-weight: 500; 578 color: #0f172a; 579 font-size: 13px; 580 } 581 582 .qa-commit-input { 583 padding: 8px 12px; 584 border: 1px solid #cbd5e1; 952 585 border-radius: 6px; 953 color: #9ca3af; 954 transition: all 0.2s ease; 955 width: 24px; 956 height: 24px; 957 display: flex; 586 font-size: 14px; 587 width: 100%; 588 box-sizing: border-box; 589 transition: border-color 0.2s, box-shadow 0.2s; 590 } 591 592 .qa-commit-input:focus { 593 outline: none; 594 border-color: #3b82f6; 595 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); 596 } 597 598 .qa-action-modal-footer { 599 padding: 16px 24px; 600 background: #f8fafc; 601 border-top: 1px solid #e2e8f0; 602 display: flex; 603 align-items: center; 604 justify-content: flex-end; 605 gap: 12px; 606 } 607 608 .qa-btn { 609 padding: 8px 16px; 610 border-radius: 6px; 611 font-size: 14px; 612 font-weight: 500; 613 cursor: pointer; 614 border: none; 615 transition: all 0.2s; 616 display: inline-flex; 958 617 align-items: center; 959 618 justify-content: center; 960 619 } 961 620 962 .qa-notification-close:hover { 963 background: #f3f4f6; 964 color: #374151; 965 } 966 967 .qa-notification-close svg { 968 width: 14px; 969 height: 14px; 970 } 971 972 .qa-notification-progress { 973 height: 3px; 974 background: #f3f4f6; 975 position: relative; 976 overflow: hidden; 977 } 978 979 .qa-notification-progress::before { 980 content: ''; 981 position: absolute; 982 top: 0; 983 left: 0; 984 height: 100%; 985 width: 0; 986 transition: width 5s linear; 987 } 988 989 .qa-notification-progress-animate::before { 990 width: 100%; 991 } 992 993 .qa-notification-success .qa-notification-progress::before { 994 background: #16a34a; 995 } 996 997 .qa-notification-error .qa-notification-progress::before { 998 background: #dc2626; 999 } 1000 1001 .qa-notification-info .qa-notification-progress::before { 621 .qa-btn:disabled { 622 opacity: 0.6; 623 cursor: not-allowed; 624 } 625 626 .qa-btn-secondary { 627 background: #ffffff; 628 border: 1px solid #cbd5e1; 629 color: #334155; 630 } 631 632 .qa-btn-secondary:hover:not(:disabled) { 633 background: #f1f5f9; 634 color: #0f172a; 635 } 636 637 .qa-btn-primary { 638 background: #3b82f6; 639 color: #ffffff; 640 } 641 642 .qa-btn-primary:hover:not(:disabled) { 1002 643 background: #2563eb; 1003 644 } 1004 645 1005 .qa-notification-warning .qa-notification-progress::before { 646 .qa-btn-warning { 647 background: #f59e0b; 648 color: #ffffff; 649 } 650 651 .qa-btn-warning:hover:not(:disabled) { 1006 652 background: #d97706; 1007 653 } 1008 654 1009 /* Git Branch Status Icons */ 1010 .qa-git-status-icon { 1011 margin-right: 4px; 1012 font-size: 12px; 1013 } 1014 1015 .qa-git-status-clean::before { 1016 content: "✓"; 1017 color: #2ea043; 1018 } 1019 1020 .qa-git-status-dirty::before { 1021 content: "●"; 1022 color: #fb8500; 1023 } 1024 1025 .qa-git-status-ahead::before { 1026 content: "↑"; 1027 color: #0969da; 1028 } 1029 1030 .qa-git-status-behind::before { 1031 content: "↓"; 1032 color: #cf222e; 1033 } 1034 1035 /* Git Repository Validation Styles */ 1036 .qa-plugin-card.no-git { 1037 border-left: 4px solid #f59e0b; 1038 background-color: #fef3c7; 1039 } 1040 1041 .qa-plugin-card.has-git { 1042 border-left: 4px solid #10b981; 1043 background-color: #d1fae5; 1044 } 1045 1046 .qa-git-status.no-git { 1047 color: #d97706; 1048 font-weight: 600; 1049 } 1050 1051 .qa-git-status.has-git { 1052 color: #059669; 1053 font-weight: 600; 1054 } 1055 1056 .qa-git-status .dashicons { 1057 font-size: 16px; 1058 width: 16px; 1059 height: 16px; 1060 margin-right: 4px; 1061 vertical-align: text-top; 1062 } 1063 1064 /* Enhanced plugin card styling */ 1065 .qa-plugin-card { 1066 padding: 16px; 1067 margin-bottom: 12px; 1068 border-radius: 8px; 1069 border: 1px solid #e5e7eb; 1070 transition: all 0.2s ease; 1071 } 1072 1073 .qa-plugin-card:hover { 1074 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 1075 } 1076 1077 .qa-plugin-header h4 { 1078 margin: 0 0 4px 0; 1079 font-size: 16px; 1080 font-weight: 600; 1081 color: #1f2937; 1082 } 1083 1084 .qa-plugin-dir { 1085 font-size: 12px; 1086 color: #6b7280; 1087 font-family: monospace; 1088 background: #f3f4f6; 1089 padding: 2px 6px; 1090 border-radius: 4px; 1091 } 1092 1093 .qa-plugin-status { 1094 margin-top: 8px; 1095 } 655 @keyframes qaModalFadeIn { 656 from { 657 opacity: 0; 658 } 659 660 to { 661 opacity: 1; 662 } 663 } 664 665 @keyframes qaModalSlideUp { 666 from { 667 opacity: 0; 668 transform: translateY(20px) scale(0.95); 669 } 670 671 to { 672 opacity: 1; 673 transform: translateY(0) scale(1); 674 } 675 } -
qa-assistant/trunk/assets/js/admin.js
r3370854 r3469660 1 ;(function($) { 2 3 $(document).ready(function() { 4 $('.qa-assistant-select2').select2(); 1 ; (function ($) { 2 3 $(document).ready(function () { 4 $('.qa-assistant-select2').select2({ 5 width: '100%' 6 }); 7 8 // Tab Switching Logic 9 $('.qa-assistant-tab-link').on('click', function (e) { 10 e.preventDefault(); 11 12 // Remove active class from all links and panes 13 $('.qa-assistant-tab-link').removeClass('active').attr('aria-selected', 'false'); 14 $('.qa-assistant-tab-pane').removeClass('active'); 15 16 // Add active class to clicked link 17 $(this).addClass('active').attr('aria-selected', 'true'); 18 19 // Show corresponding pane 20 let target = $(this).attr('href'); 21 $(target).addClass('active'); 22 }); 23 24 // Clone Repository Form Handling 25 $('#qa-assistant-clone-form').on('submit', function (e) { 26 e.preventDefault(); 27 28 let $form = $(this); 29 let $btn = $('#qa-clone-btn'); 30 let $spinner = $('#qa-clone-spinner'); 31 let $status = $('#qa-clone-status'); 32 let repoUrl = $('#qa-repo-url').val().trim(); 33 // Get the nonce specifically for cloning 34 let cloneNonce = $('#qa_assistant_clone_nonce').val() || qaAssistant.nonce; 35 36 if (!repoUrl) { 37 showNotification('Please enter a repository URL', 'error'); 38 return; 39 } 40 41 // Reset status 42 $status.hide().removeClass('notice-success notice-error notice-warning').html(''); 43 44 // Show loading state 45 $btn.prop('disabled', true); 46 $spinner.addClass('is-active'); 47 48 $.ajax({ 49 url: qaAssistant.ajaxUrl, 50 method: "POST", 51 data: { 52 action: "qa_assistant_clone_repo", 53 nonce: cloneNonce, 54 repo_url: repoUrl 55 }, 56 success: function (response) { 57 if (response.success) { 58 $status.addClass('notice-success') 59 .html(`<p><strong>Success!</strong> ${response.data.message}<br><strong>Next Step:</strong> Please add the plugin "${response.data.repo_name}" to the 'Git Branch Display' list below and save settings to enable admin bar features.</p>`) 60 .show(); 61 62 showNotification('Repository cloned successfully', 'success'); 63 64 // Clear input 65 $('#qa-repo-url').val(''); 66 67 // Reload after a delay to show the new plugin in the list 68 setTimeout(() => { 69 location.reload(); 70 }, 3000); // Increased delay so user can read message 71 } else { 72 let msg = response.data.message || 'Unknown error occurred'; 73 $status.addClass('notice-error') 74 .html(`<p><strong>Error:</strong> ${msg}</p>`) 75 .show(); 76 77 if (response.data.target_exists) { 78 showNotification('Target directory already exists', 'warning'); 79 } else { 80 showNotification(msg, 'error'); 81 } 82 } 83 }, 84 error: function (xhr, status, error) { 85 let errorMsg = 'Network error occurred. Please try again.'; 86 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { 87 errorMsg = xhr.responseJSON.data.message; 88 } 89 90 $status.addClass('notice-error') 91 .html(`<p><strong>Error:</strong> ${errorMsg}</p>`) 92 .show(); 93 94 showNotification(errorMsg, 'error'); 95 }, 96 complete: function () { 97 $btn.prop('disabled', false); 98 $spinner.removeClass('is-active'); 99 } 100 }); 101 }); 5 102 6 103 // Add Git repository validation for plugin selection 7 104 initializeGitValidation(); 8 105 9 // Keyboard-based branch search functionality 10 let searchBuffer = ''; 11 12 // Handle keypress events on branch dropdowns 13 $(document).on('keydown', function(e) { 14 // Only activate when a branch dropdown is open 15 let openDropdown = $('.qa_assistant_git-branch .ab-sub-wrapper:visible'); 16 if (openDropdown.length === 0) return; 17 18 // Handle alphanumeric keys for search 19 if (e.key.length === 1 && e.key.match(/[a-zA-Z0-9\-_]/)) { 20 e.preventDefault(); 21 22 // Add character to search buffer 23 searchBuffer += e.key.toLowerCase(); 24 25 // Perform search 26 performBranchSearch(openDropdown, searchBuffer); 27 } 28 29 // Handle Escape to clear search 30 if (e.key === 'Escape') { 31 searchBuffer = ''; 32 clearBranchSearch(openDropdown); 33 } 34 35 // Handle Backspace to remove last character 36 if (e.key === 'Backspace' && searchBuffer.length > 0) { 37 e.preventDefault(); 38 searchBuffer = searchBuffer.slice(0, -1); 39 if (searchBuffer.length > 0) { 40 performBranchSearch(openDropdown, searchBuffer); 106 // --- NEW SEARCH LOGIC --- 107 108 // Prevent dropdown from closing when clicking inside search or toolbar 109 $(document).on('click', '.qa-branch-search-container, .qa-toolbar-container, .qa-branch-search-input', function (e) { 110 e.stopPropagation(); 111 }); 112 113 // Focus search input when dropdown opens 114 $('.qa_assistant_git-branch').hover(function () { 115 let $input = $(this).find('.qa-branch-search-input'); 116 if ($input.length) { 117 setTimeout(() => $input.focus(), 100); 118 } 119 }); 120 121 // Handle Input Event (Real-time filtering) 122 $(document.body).on('input', '.qa-branch-search-input', function (e) { 123 let searchTerm = $(this).val().toLowerCase().trim(); 124 // console.log('Search input triggered:', searchTerm); 125 126 let $input = $(this); 127 // DOM Structure: 128 // div.ab-sub-wrapper 129 // > ul.ab-submenu (contains Search LI) 130 // > ul.qa-branch-list-scrollable (contains Branch LIs) 131 132 // 1. Find the UL containing the search input 133 let $searchUL = $input.closest('ul.ab-submenu'); 134 135 // 2. Find the sibling UL that contains the branches 136 let $groupContainer = $searchUL.siblings('.qa-branch-list-scrollable'); 137 138 // Fallback: If not found, try to find it within the same .ab-sub-wrapper parent 139 if ($groupContainer.length === 0) { 140 $groupContainer = $input.closest('.ab-sub-wrapper').find('.qa-branch-list-scrollable'); 141 } 142 143 // console.log('Group Container found:', $groupContainer.length); 144 145 let hasMatches = false; 146 147 // Loop through branch items 148 $groupContainer.find('.qa_assistant_git-branch-item').each(function () { 149 let $item = $(this); 150 let branchName = $item.find('.qa-branch-name').text(); // Use text() finding the name span directly is safer 151 152 // If data attribute exists use it, otherwise text 153 if ($item.data('branch-name')) { 154 branchName = $item.data('branch-name'); 155 } 156 157 if (!branchName) return; 158 159 branchName = branchName.toString().toLowerCase(); 160 161 if (branchName.includes(searchTerm)) { 162 $item.show(); 163 hasMatches = true; 164 165 // Highlight logic 166 let originalName = $item.data('branch-name') || $item.find('.qa-branch-name').text(); 167 let highlighted = highlightSearchTerm(originalName, searchTerm); 168 $item.find('.qa-branch-name').html(highlighted); 41 169 } else { 42 clearBranchSearch(openDropdown); 43 } 44 } 45 }); 46 47 function performBranchSearch(dropdown, searchTerm) { 48 let branchContainer = dropdown.closest('.qa_assistant_git-branch'); 49 let searchHint = branchContainer.find('.qa-branch-search-hint'); 50 let hasMatches = false; 51 52 // Update search hint with blinking cursor 53 if (searchHint.length > 0) { 54 searchHint.find('.ab-item').html(`🔍 Searching: "${searchTerm}<span class="qa-search-cursor">|</span>"`); 55 branchContainer.addClass('qa-branch-search-active'); 56 } 57 58 // Filter and highlight branches 59 branchContainer.find('.qa_assistant_git-branch-list-items').each(function() { 60 let $item = $(this); 61 let branchName = $item.find('.ab-item').text().toLowerCase(); 62 63 if (branchName.includes(searchTerm)) { 64 $item.removeClass('qa-branch-hidden').show(); 65 66 // Highlight matching text 67 let originalText = $item.find('.ab-item').text(); 68 let highlightedText = highlightSearchTerm(originalText, searchTerm); 69 $item.find('.ab-item').html(highlightedText); 70 71 hasMatches = true; 170 $item.hide(); 171 } 172 }); 173 174 // Empty state handling 175 let $noResultsMsg = $groupContainer.find('.qa-no-branches-dynamic'); 176 let $serverNoResults = $groupContainer.find('.qa-no-branches'); 177 178 if (!hasMatches) { 179 if ($noResultsMsg.length === 0) { 180 // Check if a server-side one exists 181 if ($serverNoResults.length > 0) { 182 $serverNoResults.show(); 183 } else { 184 // Create dynamic one 185 $groupContainer.append('<li class="qa-no-branches-dynamic ab-item" style="padding: 12px; text-align: center; color: #94a3b8; font-style: italic; list-style: none;">No branches found</li>'); 186 } 72 187 } else { 73 $item.addClass('qa-branch-hidden').hide(); 74 } 75 }); 76 77 // Show "no matches" if needed 78 if (!hasMatches && searchHint.length > 0) { 79 searchHint.find('.ab-item').html(`🔍 No matches for "${searchTerm}<span class="qa-search-cursor">|</span>"`); 80 } 81 } 82 83 function clearBranchSearch(dropdown) { 84 let branchContainer = dropdown.closest('.qa_assistant_git-branch'); 85 let searchHint = branchContainer.find('.qa-branch-search-hint'); 86 87 // Reset search hint 88 if (searchHint.length > 0) { 89 searchHint.find('.ab-item').html('🔍 Type to search branches...<span class="qa-search-cursor">|</span>'); 90 branchContainer.removeClass('qa-branch-search-active'); 91 } 92 93 // Show all branches and remove highlighting 94 branchContainer.find('.qa_assistant_git-branch-list-items').each(function() { 95 let $item = $(this); 96 $item.removeClass('qa-branch-hidden').show(); 97 98 // Remove highlighting 99 let originalText = $item.find('.ab-item').text(); 100 $item.find('.ab-item').text(originalText); 101 }); 102 } 188 $noResultsMsg.show(); 189 } 190 } else { 191 // Hide dynamic message 192 if ($noResultsMsg.length > 0) { 193 $noResultsMsg.hide(); 194 } 195 // Also hide server-side one if it exists 196 if ($serverNoResults.length > 0) { 197 $serverNoResults.hide(); 198 } 199 } 200 }); 103 201 104 202 function highlightSearchTerm(text, searchTerm) { 105 203 if (!searchTerm) return text; 106 107 let regex = new RegExp(`(${searchTerm})`, 'gi'); 204 // Escape special regex chars in search term 205 let safeTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 206 let regex = new RegExp(`(${safeTerm})`, 'gi'); 108 207 return text.replace(regex, '<span class="qa-branch-highlight">$1</span>'); 109 208 } 110 209 111 // Clear search when dropdown closes 112 $(document).on('click', function(e) { 113 if (!$(e.target).closest('.qa_assistant_git-branch').length) { 114 searchBuffer = ''; 115 $('.qa_assistant_git-branch').each(function() { 116 clearBranchSearch($(this).find('.ab-sub-wrapper')); 117 }); 118 } 119 }); 210 // --- END NEW SEARCH LOGIC --- 120 211 121 212 // Enhanced branch switching with immediate feedback 122 $(document).on('click', '.qa_assistant_git-branch- list-items', function(e) {213 $(document).on('click', '.qa_assistant_git-branch-item', function (e) { 123 214 e.preventDefault(); 124 215 … … 161 252 162 253 switchBranch(pluginDir, branchName, false) 163 .done(function (response) {254 .done(function (response) { 164 255 if (response.success) { 165 256 showNotification(`Successfully switched to branch: ${response.data.current_branch}`, 'success'); … … 176 267 } 177 268 }) 178 .fail(function (xhr, status, error) {269 .fail(function (xhr, status, error) { 179 270 showNotification('Network error occurred. Please try again.', 'error'); 180 271 console.error('Branch switch failed:', error); 181 272 }) 182 .always(function () {273 .always(function () { 183 274 // Remove loader and re-enable item 184 275 loader.remove(); … … 195 286 if (confirm(`You have uncommitted changes. Do you want to discard them and switch to ${branchName}?`)) { 196 287 switchBranch(pluginDir, branchName, true) 197 .done(function (response) {288 .done(function (response) { 198 289 if (response.success) { 199 290 showNotification(`Force switched to branch: ${response.data.current_branch}`, 'success'); … … 229 320 230 321 // Remove current-branch class from all items 231 branchContainer.find('.qa_assistant_git-branch- list-items').removeClass('current-branch');322 branchContainer.find('.qa_assistant_git-branch-item').removeClass('current-branch'); 232 323 233 324 // Add current-branch class to the new current branch … … 288 379 289 380 // Manual close 290 notification.find('.qa-notification-close').on('click', function () {381 notification.find('.qa-notification-close').on('click', function () { 291 382 hideNotification(notification); 292 383 }); 384 385 return notification; 293 386 } 294 387 … … 311 404 312 405 // Global function for pull operations (called via onclick) 313 window.qaAssistantPull = function (pluginDir) {314 // Show i mmediate feedback315 showNotification('Pulling latest changes...', 'info');406 window.qaAssistantPull = function (pluginDir) { 407 // Show initial feedback that we can track and hide if needed 408 let pullingNotification = showNotification('Pulling latest changes...', 'info'); 316 409 317 410 // Find the button that was clicked and add loading state 318 let $button = $('.qa-pull-button').filter(function () {411 let $button = $('.qa-pull-button').filter(function () { 319 412 return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir); 320 413 }); … … 326 419 327 420 pullBranch(pluginDir) 328 .done(function (response) {329 if (response .success) {421 .done(function (response) { 422 if (response && response.success) { 330 423 showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success'); 331 424 // Reload page to show updated state 332 425 setTimeout(() => location.reload(), 1500); 333 426 } else { 334 if (response.data.has_changes) { 335 showNotification('You have uncommitted changes. Please commit or stash them before pulling.', 'warning'); 427 if (response && response.data && response.data.has_changes) { 428 hideNotification(pullingNotification); // Hide the info toast 429 showUncommittedChangesModal(pluginDir); 336 430 } else { 337 showNotification(response.data.message || 'Failed to pull changes', 'error'); 431 let errMsg = (response && response.data && response.data.message) ? response.data.message : 'Failed to pull changes'; 432 showNotification(errMsg, 'error'); 338 433 } 339 434 } 340 435 }) 341 .fail(function (xhr, status, error) {436 .fail(function (xhr, status, error) { 342 437 showNotification('Network error occurred during pull. Please try again.', 'error'); 343 438 console.error('Pull failed:', error); 344 439 }) 345 .always(function () {440 .always(function () { 346 441 // Restore button 347 442 $button.find('.ab-item').html(originalText); … … 351 446 // Fallback if button not found 352 447 pullBranch(pluginDir) 353 .done(function (response) {354 if (response .success) {448 .done(function (response) { 449 if (response && response.success) { 355 450 showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success'); 356 451 setTimeout(() => location.reload(), 1500); 357 452 } else { 358 showNotification(response.data.message || 'Failed to pull changes', 'error'); 453 if (response && response.data && response.data.has_changes) { 454 hideNotification(pullingNotification); // Hide the info toast 455 showUncommittedChangesModal(pluginDir); 456 } else { 457 let errMsg = (response && response.data && response.data.message) ? response.data.message : 'Failed to pull changes'; 458 showNotification(errMsg, 'error'); 459 } 359 460 } 360 461 }) 361 .fail(function (xhr, status, error) {462 .fail(function (xhr, status, error) { 362 463 showNotification('Network error occurred during pull. Please try again.', 'error'); 363 464 console.error('Pull failed:', error); … … 394 495 395 496 // Global function for refreshing branches (called via onclick) 396 window.qaAssistantRefresh = function (pluginDir) {497 window.qaAssistantRefresh = function (pluginDir) { 397 498 // Show immediate feedback 398 499 showNotification('Fetching latest branches from remote...', 'info'); 399 500 400 501 // Find the button that was clicked and add loading state 401 let $button = $('.qa-refresh-button').filter(function () {502 let $button = $('.qa-refresh-button').filter(function () { 402 503 return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir); 403 504 }); … … 409 510 410 511 refreshBranches(pluginDir) 411 .done(function (response) {512 .done(function (response) { 412 513 if (response.success) { 413 514 let message = response.data.fetch_success … … 423 524 } 424 525 }) 425 .fail(function (xhr, status, error) {526 .fail(function (xhr, status, error) { 426 527 showNotification('Network error occurred during refresh. Please try again.', 'error'); 427 528 console.error('Refresh failed:', error); 428 529 }) 429 .always(function () {530 .always(function () { 430 531 // Restore button 431 532 $button.find('.ab-item').html(originalText); … … 435 536 // Fallback if button not found 436 537 refreshBranches(pluginDir) 437 .done(function (response) {538 .done(function (response) { 438 539 if (response.success) { 439 540 let message = response.data.fetch_success … … 446 547 } 447 548 }) 448 .fail(function (xhr, status, error) {549 .fail(function (xhr, status, error) { 449 550 showNotification('Network error occurred during refresh. Please try again.', 'error'); 450 551 console.error('Refresh failed:', error); … … 471 572 } 472 573 574 // --- Uncommitted Changes Modal Logic --- 575 function showUncommittedChangesModal(pluginDir) { 576 $('.qa-action-modal').remove(); 577 578 let modalHtml = $(` 579 <div class="qa-action-modal-overlay qa-action-modal" id="qaUncommittedModal"> 580 <div class="qa-action-modal-content"> 581 <div class="qa-action-modal-header"> 582 <h3 class="qa-action-modal-title">Uncommitted Changes Found</h3> 583 <button class="qa-action-modal-close" aria-label="Close modal">×</button> 584 </div> 585 <div class="qa-action-modal-body"> 586 <p>You have uncommitted local changes that prevent pulling. How would you like to proceed?</p> 587 588 <!-- Commit Section --> 589 <div class="qa-commit-section"> 590 <label for="qaCommitMessage">Commit Message:</label> 591 <input type="text" id="qaCommitMessage" class="qa-commit-input" placeholder="e.g. Fixed minor bug"> 592 </div> 593 </div> 594 <div class="qa-action-modal-footer"> 595 <button class="qa-btn qa-btn-secondary qa-close-modal">Cancel</button> 596 <button class="qa-btn qa-btn-warning qa-stash-btn">Stash & Pull</button> 597 <button class="qa-btn qa-btn-primary qa-commit-btn">Commit & Pull</button> 598 </div> 599 </div> 600 </div> 601 `); 602 603 $('body').append(modalHtml); 604 let $modal = modalHtml; 605 606 // Close actions 607 $modal.find('.qa-action-modal-close, .qa-close-modal').on('click', function () { 608 $modal.remove(); 609 }); 610 611 // Stash action 612 $modal.find('.qa-stash-btn').on('click', function () { 613 let $btn = $(this); 614 $btn.prop('disabled', true).text('Stashing...'); 615 $modal.find('.qa-btn').prop('disabled', true); 616 617 stashChanges(pluginDir).done(function (response) { 618 if (response.success) { 619 $modal.remove(); 620 // Proceed with pull 621 showNotification('Pulling latest changes...', 'info'); 622 window.qaAssistantPull(pluginDir); 623 } else { 624 showNotification(response.data.message || 'Failed to stash changes.', 'error'); 625 $modal.find('.qa-btn').prop('disabled', false); 626 $btn.text('Stash & Pull'); 627 } 628 }).fail(function () { 629 showNotification('Network error occurred during stash.', 'error'); 630 $modal.find('.qa-btn').prop('disabled', false); 631 $btn.text('Stash & Pull'); 632 }); 633 }); 634 635 // Commit action 636 $modal.find('.qa-commit-btn').on('click', function () { 637 let message = $modal.find('#qaCommitMessage').val().trim(); 638 if (!message) { 639 showNotification('Please enter a commit message.', 'warning'); 640 $modal.find('#qaCommitMessage').focus(); 641 return; 642 } 643 644 let $btn = $(this); 645 $btn.prop('disabled', true).text('Committing...'); 646 $modal.find('.qa-btn').prop('disabled', true); 647 648 commitChanges(pluginDir, message).done(function (response) { 649 if (response.success) { 650 $modal.remove(); 651 // Proceed with pull 652 showNotification('Pulling latest changes...', 'info'); 653 window.qaAssistantPull(pluginDir); 654 } else { 655 showNotification(response.data.message || 'Failed to commit changes.', 'error'); 656 $modal.find('.qa-btn').prop('disabled', false); 657 $btn.text('Commit & Pull'); 658 } 659 }).fail(function () { 660 showNotification('Network error occurred during commit.', 'error'); 661 $modal.find('.qa-btn').prop('disabled', false); 662 $btn.text('Commit & Pull'); 663 }); 664 }); 665 } 666 667 function stashChanges(pluginDir) { 668 return $.ajax({ 669 url: qaAssistant.ajaxUrl, 670 method: "POST", 671 data: { 672 action: "qa_assistant_stash_changes", 673 nonce: qaAssistant.nonce, 674 plugin_dir: pluginDir 675 } 676 }); 677 } 678 679 function commitChanges(pluginDir, message) { 680 return $.ajax({ 681 url: qaAssistant.ajaxUrl, 682 method: "POST", 683 data: { 684 action: "qa_assistant_commit_changes", 685 nonce: qaAssistant.nonce, 686 plugin_dir: pluginDir, 687 commit_message: message 688 } 689 }); 690 } 691 473 692 }); 474 693 … … 478 697 function initializeGitValidation() { 479 698 // Add change event listener to plugin selection dropdown 480 $('.qa-assistant-select2').on('change', function () {699 $('.qa-assistant-select2').on('change', function () { 481 700 validateSelectedPlugins(); 482 701 }); 483 702 484 703 // Add form submission validation 485 $('.qa-assistant-form').on('submit', function (e) {704 $('.qa-assistant-form').on('submit', function (e) { 486 705 if (!validateSelectedPlugins()) { 487 706 e.preventDefault(); … … 499 718 500 719 // Check each selected plugin 501 selectedValues.forEach(function (pluginDir) {720 selectedValues.forEach(function (pluginDir) { 502 721 let pluginCard = $(`.qa-plugin-card[data-plugin-dir="${pluginDir}"]`); 503 722 if (pluginCard.length > 0) { -
qa-assistant/trunk/composer.json
r3370854 r3469660 12 12 "minimum-stability": "stable", 13 13 "require": { 14 "php": ">=8.0", 14 15 "czproject/git-php": "^4.0" 15 16 }, … … 18 19 "QaAssistant\\": "includes/" 19 20 }, 20 "files": [ "includes/functions.php" ] 21 "files": [ 22 "includes/functions.php" 23 ] 21 24 } 22 25 } -
qa-assistant/trunk/includes/Admin/Menu.php
r3370854 r3469660 11 11 * The Menu handler class 12 12 */ 13 class Menu { 13 class Menu 14 { 14 15 15 16 /** 16 17 * Initialize the class 17 18 */ 18 function __construct( ) { 19 add_action( 'admin_menu', [ $this, 'admin_menu' ] ); 19 function __construct() 20 { 21 add_action('admin_menu', [$this, 'admin_menu']); 20 22 } 21 23 … … 25 27 * @return void 26 28 */ 27 public function admin_menu() { 29 public function admin_menu() 30 { 28 31 $parent_slug = 'qa-assistant'; 29 $capability = apply_filters('qa -assistant/menu/capability', 'manage_options');32 $capability = apply_filters('qa_assistant_menu_capability', 'manage_options'); 30 33 31 34 // $hook = add_menu_page(__('Options Table', 'nhrrob-options-table-manager'), __('Options Table', 'nhrrob-options-table-manager'), $capability, $parent_slug, [$this, 'settings_page'], 'dashicons-admin-post'); 32 35 // add_submenu_page( $parent_slug, __( 'Settings', 'nhrrob-options-table-manager' ), __( 'Settings', 'nhrrob-options-table-manager' ), $capability, 'nhrotm-options-table-manager-settings', [ $this, 'settings_page' ] ); 33 $hook = add_submenu_page( 'tools.php', __( 'QA Assistant', 'qa-assistant' ), __( 'QA Assistant', 'qa-assistant' ), $capability, $parent_slug, [ $this, 'settings_page' ]);36 $hook = add_submenu_page('tools.php', __('QA Assistant', 'qa-assistant'), __('QA Assistant', 'qa-assistant'), $capability, $parent_slug, [$this, 'settings_page']); 34 37 35 38 add_action('admin_head-' . $hook, [$this, 'enqueue_assets']); … … 41 44 * @return void 42 45 */ 43 public function settings_page() { 46 public function settings_page() 47 { 44 48 // Check user capabilities 45 49 if (!current_user_can('manage_options')) { … … 49 53 $settings = new Settings(); 50 54 51 wp_enqueue_style( 'qa-assistant-select2-style');52 wp_enqueue_script( 'qa-assistant-select2-script');53 wp_enqueue_style( 'qa-assistant-bootstrap-style');54 wp_enqueue_script( 'qa-assistant-bootstrap-script');55 wp_enqueue_script( 'qa-assistant-popper-js-script');56 wp_enqueue_script( 'qa-assistant-jquery-slim-script');55 wp_enqueue_style('qa-assistant-select2-style'); 56 wp_enqueue_script('qa-assistant-select2-script'); 57 wp_enqueue_style('qa-assistant-bootstrap-style'); 58 wp_enqueue_script('qa-assistant-bootstrap-script'); 59 wp_enqueue_script('qa-assistant-popper-js-script'); 60 wp_enqueue_script('qa-assistant-jquery-slim-script'); 57 61 58 62 $available_plugins = $settings->get_available_plugins(); … … 64 68 } 65 69 66 // Get currently selected plugins for the dropdown67 $current_settings = maybe_unserialize(get_option('qa_assistant_settings', array()));68 $selected_plugins = isset($current_settings['selected_plugins']) ? $current_settings['selected_plugins'] : array();70 // Get currently selected plugins for the dropdown 71 $current_settings = maybe_unserialize(get_option('qa_assistant_settings', array())); 72 $selected_plugins = isset($current_settings['selected_plugins']) ? $current_settings['selected_plugins'] : array(); 69 73 70 74 // Save settings data 71 if ( isset( $_POST['qa_assistant_settings_form_nonce'] ) && wp_verify_nonce( sanitize_text_field(wp_unslash($_POST['qa_assistant_settings_form_nonce'])), 'qa_assistant_settings_form_action' )) {75 if (isset($_POST['qa_assistant_settings_form_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['qa_assistant_settings_form_nonce'])), 'qa_assistant_settings_form_action')) { 72 76 // get posted data and sanitize 73 77 $selected_plugins = []; … … 141 145 } 142 146 143 require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php';147 require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php'; 144 148 } 145 149 … … 149 153 * @return void 150 154 */ 151 public function enqueue_assets() { 152 wp_enqueue_style( 'qa-assistant-admin-style' ); 153 wp_enqueue_script( 'qa-assistant-admin-script' ); 155 public function enqueue_assets() 156 { 157 wp_enqueue_style('qa-assistant-admin-style'); 158 wp_enqueue_script('qa-assistant-admin-script'); 154 159 } 155 160 } -
qa-assistant/trunk/includes/Ajax.php
r3370854 r3469660 42 42 add_action('wp_ajax_qa_assistant_refresh_branches', [$this, 'refresh_branches']); 43 43 44 // Stash and commit 45 add_action('wp_ajax_qa_assistant_stash_changes', [$this, 'stash_changes']); 46 add_action('wp_ajax_qa_assistant_commit_changes', [$this, 'commit_changes']); 47 44 48 // Legacy support 45 49 add_action('wp_ajax_qa_assistant_get_branch_data', [$this, 'get_branch_data']); 46 50 51 // Clone repository 52 add_action('wp_ajax_qa_assistant_clone_repo', [$this, 'clone_repository']); 53 54 // Toggle monitor status 55 add_action('wp_ajax_qa_assistant_toggle_monitor', [$this, 'toggle_monitor_plugin']); 56 57 // Get installed git plugins 58 add_action('wp_ajax_qa_assistant_get_plugins', [$this, 'get_git_plugins']); 59 60 // Save display settings (aliases and monitoring) 61 add_action('wp_ajax_qa_assistant_save_display_settings', [$this, 'save_display_settings']); 62 63 // Git drawer endpoints 64 add_action('wp_ajax_qa_assistant_get_repositories', [$this, 'get_repositories']); 65 add_action('wp_ajax_qa_assistant_get_branches', [$this, 'get_branches_for_repo']); 66 67 // Settings page endpoints 68 add_action('wp_ajax_qa_assistant_get_system_status', [$this, 'get_system_status']); 69 add_action('wp_ajax_qa_assistant_get_activity_logs', [$this, 'get_activity_logs']); 70 add_action('wp_ajax_qa_assistant_clear_activity_logs', [$this, 'clear_activity_logs']); 71 47 72 $this->gitManager = new GitManager(); 73 } 74 75 /** 76 * Get list of plugins that are git repositories 77 */ 78 public function get_git_plugins() 79 { 80 // Verify nonce usually, but for initial fetch we might just check permissions 81 // or use the common admin nonce 82 if (!current_user_can('manage_options')) { 83 wp_send_json_error(['message' => 'Unauthorized']); 84 } 85 86 $plugins = []; 87 $plugin_dirs = array_filter(glob(WP_PLUGIN_DIR . '/*'), 'is_dir'); 88 $monitored_plugins = get_option('qa_assistant_monitored_plugins', []); 89 90 // Retrieve settings for aliases 91 $settings_option = get_option('qa_assistant_settings', []); 92 $settings_option = maybe_unserialize($settings_option); 93 $selected_plugins_settings = isset($settings_option['selected_plugins']) ? $settings_option['selected_plugins'] : []; 94 95 foreach ($plugin_dirs as $dir) { 96 $slug = basename($dir); 97 // Check if it's a git repo 98 if ($this->gitManager->isGitRepository($dir)) { 99 $status = $this->gitManager->getRepositoryStatus($dir); 100 $branch = $this->gitManager->getCurrentBranch($dir); 101 102 // Get Plugin Name from main file if possible 103 $name = $slug; 104 $main_file = $dir . '/' . $slug . '.php'; 105 if (file_exists($main_file)) { 106 $plugin_data = get_plugin_data($main_file, false, false); 107 if (!empty($plugin_data['Name'])) { 108 $name = $plugin_data['Name']; 109 } 110 } 111 112 // Determine Alias 113 $alias = ''; 114 // Check new associated array format 115 if (isset($selected_plugins_settings[$slug]) && is_array($selected_plugins_settings[$slug])) { 116 $alias = isset($selected_plugins_settings[$slug]['alias']) ? $selected_plugins_settings[$slug]['alias'] : ''; 117 } elseif (isset($selected_plugins_settings[$slug]) && !is_array($selected_plugins_settings[$slug])) { 118 // Check legacy simple key-value (if any, though legacy was simple array of values) 119 // If it was simple array [0 => 'slug'], keys are integers. 120 // If it was assoc [slug => slug], value is string. 121 // We assume new format mostly, but 'alias' defaults to empty string. 122 } 123 124 $plugins[] = [ 125 'id' => $slug, // Use slug as ID 126 'name' => $name, 127 'slug' => $slug, 128 'currentBranch' => $branch, 129 'status' => $status['has_changes'] ? 'modified' : 'stable', 130 'path' => $dir, 131 'is_monitored' => in_array($slug, $monitored_plugins), 132 'alias' => $alias 133 ]; 134 } 135 } 136 137 wp_send_json_success(['plugins' => $plugins]); 138 } 139 140 /** 141 * Toggle monitor status for a plugin 142 */ 143 public function toggle_monitor_plugin() 144 { 145 // Verify nonce 146 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 147 wp_send_json_error(['message' => 'Security check failed.']); 148 } 149 150 $slug = sanitize_text_field(wp_unslash($_POST['slug'] ?? '')); 151 $monitor = filter_var(wp_unslash($_POST['monitor'] ?? false), FILTER_VALIDATE_BOOLEAN); 152 153 if (empty($slug)) { 154 wp_send_json_error(['message' => 'Plugin slug is required.']); 155 } 156 157 $monitored = get_option('qa_assistant_monitored_plugins', []); 158 159 if ($monitor) { 160 if (!in_array($slug, $monitored)) { 161 $monitored[] = $slug; 162 } 163 } else { 164 $monitored = array_diff($monitored, [$slug]); 165 } 166 167 update_option('qa_assistant_monitored_plugins', array_values($monitored)); 168 169 wp_send_json_success([ 170 'message' => $monitor ? 'Plugin added to monitoring.' : 'Plugin removed from monitoring.', 171 'slug' => $slug, 172 'is_monitored' => $monitor 173 ]); 48 174 } 49 175 … … 84 210 85 211 if ($result['success']) { 212 $this->log_activity('switch', $plugin_dir, $result['current_branch'], 'success', 'Switched to ' . $result['current_branch']); 86 213 wp_send_json_success([ 87 214 'message' => $result['message'], … … 162 289 163 290 if ($result['success']) { 291 // Store last pulled timestamp (24h expiry) 292 set_transient('qa_assistant_last_pulled_' . md5($path), time(), DAY_IN_SECONDS); 293 294 $this->log_activity('pull', $plugin_dir, $result['branch'], 'success', 'Pulled latest changes'); 295 164 296 wp_send_json_success([ 165 297 'message' => $result['message'], 166 298 'branch' => $result['branch'], 167 299 'output' => $result['output'] ?? '', 168 'plugin_dir' => $plugin_dir 300 'plugin_dir' => $plugin_dir, 301 'lastPulled' => time(), 169 302 ]); 170 303 } else { … … 293 426 } 294 427 } 428 429 /** 430 * Stash changes for a repository 431 */ 432 public function stash_changes() 433 { 434 // Verify nonce for security 435 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 436 wp_send_json_error([ 437 'message' => 'Security check failed.' 438 ]); 439 } 440 441 $plugin_dir = sanitize_text_field(wp_unslash($_POST['plugin_dir'] ?? '')); 442 443 if (empty($plugin_dir)) { 444 wp_send_json_error([ 445 'message' => 'Plugin directory is required.' 446 ]); 447 } 448 449 $path = qa_assistant_get_plugin_path($plugin_dir); 450 451 // Validate plugin directory exists 452 if (!is_dir($path)) { 453 wp_send_json_error([ 454 'message' => 'Plugin directory does not exist.' 455 ]); 456 } 457 458 $result = $this->gitManager->stashChanges($path); 459 460 if ($result['success']) { 461 wp_send_json_success([ 462 'message' => $result['message'], 463 'plugin_dir' => $plugin_dir 464 ]); 465 } else { 466 wp_send_json_error([ 467 'message' => $result['error'] 468 ]); 469 } 470 } 471 472 /** 473 * Commit changes for a repository 474 */ 475 public function commit_changes() 476 { 477 // Verify nonce for security 478 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 479 wp_send_json_error([ 480 'message' => 'Security check failed.' 481 ]); 482 } 483 484 $plugin_dir = sanitize_text_field(wp_unslash($_POST['plugin_dir'] ?? '')); 485 $message = sanitize_text_field(wp_unslash($_POST['commit_message'] ?? '')); 486 487 if (empty($plugin_dir)) { 488 wp_send_json_error([ 489 'message' => 'Plugin directory is required.' 490 ]); 491 } 492 493 if (empty(trim($message))) { 494 wp_send_json_error([ 495 'message' => 'Commit message is required.' 496 ]); 497 } 498 499 $path = qa_assistant_get_plugin_path($plugin_dir); 500 501 // Validate plugin directory exists 502 if (!is_dir($path)) { 503 wp_send_json_error([ 504 'message' => 'Plugin directory does not exist.' 505 ]); 506 } 507 508 $result = $this->gitManager->commitChanges($path, $message); 509 510 if ($result['success']) { 511 wp_send_json_success([ 512 'message' => $result['message'], 513 'plugin_dir' => $plugin_dir 514 ]); 515 } else { 516 wp_send_json_error([ 517 'message' => $result['error'] 518 ]); 519 } 520 } 521 522 /** 523 * Clone a GitHub user repository 524 */ 525 public function clone_repository() 526 { 527 // Verify nonce for security 528 // Note: We use a specific nonce for settings page actions if available, or fall back to the admin nonce 529 $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')); 530 if (!wp_verify_nonce($nonce, 'qa_assistant_clone_repo')) { 531 wp_send_json_error([ 532 'message' => 'Security check failed. Please refresh the page and try again.' 533 ]); 534 } 535 536 // Check permissions 537 if (!current_user_can('manage_options')) { 538 wp_send_json_error([ 539 'message' => 'You do not have permission to perform this action.' 540 ]); 541 } 542 543 $repo_url = sanitize_text_field(wp_unslash($_POST['repo_url'] ?? '')); 544 545 if (empty($repo_url)) { 546 wp_send_json_error([ 547 'message' => 'Repository URL is required.' 548 ]); 549 } 550 551 // Validate URL format (allow HTTP/HTTPS and SSH) 552 if (!filter_var($repo_url, FILTER_VALIDATE_URL) && !preg_match('/^git@[\w\.-]+:[\w\.-]+\/[\w\.-]+\.git$/', $repo_url)) { 553 // Fallback Regex for more loose git url validation if strict check fails 554 if (!preg_match('/^(https?:\/\/|git@).+\.git$/', $repo_url)) { 555 wp_send_json_error([ 556 'message' => 'Invalid formatted Git URL. Please use HTTPS or SSH format ending in .git' 557 ]); 558 } 559 } 560 561 // Extract repository name to better determine target directory 562 $repo_name = ''; 563 564 // Try parsing as URL first 565 $path = wp_parse_url($repo_url, PHP_URL_PATH); 566 if ($path) { 567 $path_parts = pathinfo($path); 568 $repo_name = $path_parts['filename']; 569 } 570 571 // If parse_url failed (common with SCP-like SSH syntax), try regex extraction 572 if (empty($repo_name)) { 573 if (preg_match('/\/([^\/]+)\.git$/', $repo_url, $matches)) { 574 $repo_name = $matches[1]; 575 } 576 } 577 578 if (empty($repo_name)) { 579 wp_send_json_error([ 580 'message' => 'Could not determine repository name from URL.' 581 ]); 582 } 583 584 $target_path = WP_PLUGIN_DIR . '/' . $repo_name; 585 586 // Check if directory already exists before even trying git 587 if (file_exists($target_path)) { 588 wp_send_json_error([ 589 'message' => "Directory '{$repo_name}' already exists in plugins folder. Please remove or rename it first.", 590 'target_exists' => true 591 ]); 592 } 593 594 $result = $this->gitManager->cloneRepository($repo_url, $target_path); 595 596 if ($result['success']) { 597 // Auto-monitor this plugin 598 $monitored = get_option('qa_assistant_monitored_plugins', []); 599 if (!in_array($repo_name, $monitored)) { 600 $monitored[] = $repo_name; 601 update_option('qa_assistant_monitored_plugins', $monitored); 602 } 603 604 wp_send_json_success([ 605 'message' => "Successfully cloned '{$repo_name}' into plugins directory.", 606 'repo_name' => $repo_name, 607 'path' => $target_path 608 ]); 609 } else { 610 wp_send_json_error([ 611 'message' => $result['error'] 612 ]); 613 } 614 } 615 616 /** 617 * Save display settings (Aliases and Monitoring status) 618 */ 619 public function save_display_settings() 620 { 621 // Verify nonce 622 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 623 wp_send_json_error(['message' => 'Security check failed.']); 624 } 625 626 // Retrieve plugins data 627 // Expecting $_POST['plugins'] to be an array of objects: { slug: '...', alias: '...', is_monitored: true/false } 628 // Or a JSON string 629 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Data is sanitized per-field below in the foreach loop. 630 $plugins_input = isset($_POST['plugins']) ? wp_unslash($_POST['plugins']) : []; 631 632 if (is_string($plugins_input)) { 633 $plugins = json_decode($plugins_input, true); 634 } else { 635 $plugins = $plugins_input; 636 } 637 638 if (!is_array($plugins)) { 639 wp_send_json_error(['message' => 'Invalid data format.']); 640 } 641 642 $monitored_slugs = []; 643 $settings_entries = []; 644 645 foreach ($plugins as $plugin) { 646 // sanitize 647 $slug = sanitize_text_field($plugin['slug']); 648 $is_monitored = filter_var($plugin['is_monitored'], FILTER_VALIDATE_BOOLEAN) || $plugin['is_monitored'] === 'true'; 649 $alias = sanitize_text_field($plugin['alias']); 650 651 if ($is_monitored) { 652 $monitored_slugs[] = $slug; 653 // Add to settings entries with alias 654 $settings_entries[$slug] = [ 655 'alias' => $alias 656 ]; 657 } 658 } 659 660 // Update monitored plugins option (Simple list of slugs) 661 update_option('qa_assistant_monitored_plugins', $monitored_slugs); 662 663 // Update settings option (Associative array with aliases) 664 $current_settings = get_option('qa_assistant_settings', []); 665 $current_settings = maybe_unserialize($current_settings); 666 if (!is_array($current_settings)) { 667 $current_settings = []; 668 } 669 670 $current_settings['selected_plugins'] = $settings_entries; 671 update_option('qa_assistant_settings', $current_settings); 672 673 wp_send_json_success([ 674 'message' => 'Display settings saved successfully.', 675 'monitored_count' => count($monitored_slugs) 676 ]); 677 } 678 679 /** 680 * Get monitored repositories for the Git Branches drawer. 681 * Returns each repo's slug, display name, alias, and current branch. 682 */ 683 public function get_repositories() 684 { 685 // Verify nonce 686 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 687 wp_send_json_error(['message' => 'Security check failed.']); 688 } 689 690 if (!current_user_can('manage_options')) { 691 wp_send_json_error(['message' => 'Unauthorized.']); 692 } 693 694 $qa_settings = get_option('qa_assistant_settings', []); 695 $qa_settings = maybe_unserialize($qa_settings); 696 697 if (!is_array($qa_settings) || !isset($qa_settings['selected_plugins'])) { 698 wp_send_json_success(['repositories' => []]); 699 } 700 701 $plugin_dirs = $qa_settings['selected_plugins']; 702 if (!is_array($plugin_dirs)) { 703 wp_send_json_success(['repositories' => []]); 704 } 705 706 $repositories = []; 707 708 foreach ($plugin_dirs as $slug => $settings) { 709 if (!is_array($settings)) { 710 $settings = ['alias' => $settings]; 711 } 712 713 $path = qa_assistant_get_plugin_path($slug); 714 if (!$this->gitManager->isGitRepository($path)) { 715 continue; 716 } 717 718 $currentBranch = $this->gitManager->getCurrentBranch($path); 719 $alias = isset($settings['alias']) && !empty($settings['alias']) ? $settings['alias'] : $slug; 720 721 // Check for uncommitted changes 722 $hasChanges = false; 723 try { 724 $status = $this->gitManager->getRepositoryStatus($path); 725 if (!empty($status['has_changes'])) { 726 $hasChanges = true; 727 } 728 } catch (\Exception $e) { 729 // Silently continue if status check fails 730 } 731 732 // Get last pulled time from transient 733 $lastPulled = get_transient('qa_assistant_last_pulled_' . md5($path)); 734 735 // Try to get a proper display name from plugin header 736 $name = $slug; 737 $main_file = $path . '/' . $slug . '.php'; 738 if (file_exists($main_file)) { 739 $plugin_data = get_plugin_data($main_file, false, false); 740 if (!empty($plugin_data['Name'])) { 741 $name = $plugin_data['Name']; 742 } 743 } 744 745 $repositories[] = [ 746 'slug' => sanitize_text_field($slug), 747 'name' => sanitize_text_field($name), 748 'alias' => sanitize_text_field($alias), 749 'currentBranch' => sanitize_text_field($currentBranch ?: 'unknown'), 750 'hasChanges' => $hasChanges, 751 'lastPulled' => $lastPulled ? intval($lastPulled) : null, 752 ]; 753 } 754 755 wp_send_json_success(['repositories' => $repositories]); 756 } 757 758 /** 759 * Get branches for a single repository (lazy load on repo click). 760 * Returns sorted branch list. 761 */ 762 public function get_branches_for_repo() 763 { 764 // Verify nonce 765 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 766 wp_send_json_error(['message' => 'Security check failed.']); 767 } 768 769 if (!current_user_can('manage_options')) { 770 wp_send_json_error(['message' => 'Unauthorized.']); 771 } 772 773 $plugin_dir = sanitize_text_field(wp_unslash($_POST['plugin_dir'] ?? '')); 774 if (empty($plugin_dir)) { 775 wp_send_json_error(['message' => 'Plugin directory is required.']); 776 } 777 778 $path = qa_assistant_get_plugin_path($plugin_dir); 779 if (!is_dir($path)) { 780 wp_send_json_error(['message' => 'Plugin directory does not exist.']); 781 } 782 783 // Get branches without fetching from remote (fast, no blocking) 784 $branches = $this->gitManager->getBranches($path, false); 785 $currentBranch = $this->gitManager->getCurrentBranch($path) ?: ''; 786 787 // Check for uncommitted changes 788 $hasChanges = false; 789 try { 790 $status = $this->gitManager->getRepositoryStatus($path); 791 if (!empty($status['has_changes'])) { 792 $hasChanges = true; 793 } 794 } catch (\Exception $e) { 795 // Silently continue 796 } 797 798 // Get last pulled time 799 $lastPulled = get_transient('qa_assistant_last_pulled_' . md5($path)); 800 801 // Sort branches: master/main → develop → current → others 802 $branches = $this->sort_branches_for_drawer($branches, $currentBranch); 803 804 wp_send_json_success([ 805 'branches' => array_map('sanitize_text_field', $branches), 806 'currentBranch' => sanitize_text_field($currentBranch), 807 'plugin_dir' => sanitize_text_field($plugin_dir), 808 'hasChanges' => $hasChanges, 809 'lastPulled' => $lastPulled ? intval($lastPulled) : null, 810 ]); 811 } 812 813 /** 814 * Sort branches by priority: master/main → develop → current → others 815 * 816 * @param array $branches 817 * @param string $currentBranch 818 * @return array 819 */ 820 private function sort_branches_for_drawer($branches, $currentBranch) 821 { 822 if (empty($branches)) { 823 return []; 824 } 825 826 $top = []; 827 $develop = []; 828 $current = []; 829 $others = []; 830 831 foreach ($branches as $branch) { 832 if ($branch === 'master' || $branch === 'main') { 833 $top[] = $branch; 834 } elseif ($branch === 'develop' || $branch === 'dev') { 835 $develop[] = $branch; 836 } elseif ($branch === $currentBranch) { 837 $current[] = $branch; 838 } else { 839 $others[] = $branch; 840 } 841 } 842 843 sort($others); 844 845 return array_values(array_unique(array_merge($top, $develop, $current, $others))); 846 } 847 848 /** 849 * Log an activity entry. 850 * 851 * @param string $action Action type: pull, switch, fetch, stash, commit 852 * @param string $repo Repository slug 853 * @param string $branch Branch name 854 * @param string $status success or error 855 * @param string $message Human-readable message 856 */ 857 private function log_activity($action, $repo, $branch, $status, $message) 858 { 859 $logs = get_option('qa_assistant_activity_log', []); 860 if (!is_array($logs)) { 861 $logs = []; 862 } 863 864 array_unshift($logs, [ 865 'action' => sanitize_text_field($action), 866 'repo' => sanitize_text_field($repo), 867 'branch' => sanitize_text_field($branch), 868 'status' => sanitize_text_field($status), 869 'message' => sanitize_text_field($message), 870 'timestamp' => time(), 871 'user' => wp_get_current_user()->display_name, 872 ]); 873 874 // Keep only the last 100 entries 875 $logs = array_slice($logs, 0, 100); 876 update_option('qa_assistant_activity_log', $logs, false); 877 } 878 879 /** 880 * Get system status information for the settings page. 881 */ 882 public function get_system_status() 883 { 884 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 885 wp_send_json_error(['message' => 'Security check failed.']); 886 } 887 888 if (!current_user_can('manage_options')) { 889 wp_send_json_error(['message' => 'Unauthorized.']); 890 } 891 892 // Git version 893 $git_version = 'Not found'; 894 $git_path = 'N/A'; 895 $git_output = []; 896 exec('git --version 2>&1', $git_output); 897 if (!empty($git_output[0])) { 898 $git_version = trim(str_replace('git version', '', $git_output[0])); 899 } 900 $git_path_output = []; 901 exec('which git 2>&1', $git_path_output); 902 if (!empty($git_path_output[0])) { 903 $git_path = trim($git_path_output[0]); 904 } 905 906 // Count monitored repos 907 $qa_settings = get_option('qa_assistant_settings', []); 908 $qa_settings = maybe_unserialize($qa_settings); 909 $monitored_count = 0; 910 if (is_array($qa_settings) && isset($qa_settings['selected_plugins'])) { 911 $monitored_count = count($qa_settings['selected_plugins']); 912 } 913 914 wp_send_json_success([ 915 'git_version' => $git_version, 916 'git_path' => $git_path, 917 'php_version' => phpversion(), 918 'wp_version' => get_bloginfo('version'), 919 'plugin_version' => defined('QA_ASSISTANT_VERSION') ? QA_ASSISTANT_VERSION : 'unknown', 920 'memory_limit' => ini_get('memory_limit'), 921 'memory_usage' => size_format(memory_get_usage(true)), 922 'monitored_repos' => $monitored_count, 923 'os' => PHP_OS, 924 ]); 925 } 926 927 /** 928 * Get activity logs for the settings page. 929 */ 930 public function get_activity_logs() 931 { 932 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 933 wp_send_json_error(['message' => 'Security check failed.']); 934 } 935 936 if (!current_user_can('manage_options')) { 937 wp_send_json_error(['message' => 'Unauthorized.']); 938 } 939 940 $logs = get_option('qa_assistant_activity_log', []); 941 if (!is_array($logs)) { 942 $logs = []; 943 } 944 945 wp_send_json_success(['logs' => $logs]); 946 } 947 948 /** 949 * Clear all activity logs. 950 */ 951 public function clear_activity_logs() 952 { 953 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'qa-assistant-admin-nonce')) { 954 wp_send_json_error(['message' => 'Security check failed.']); 955 } 956 957 if (!current_user_can('manage_options')) { 958 wp_send_json_error(['message' => 'Unauthorized.']); 959 } 960 961 update_option('qa_assistant_activity_log', [], false); 962 wp_send_json_success(['message' => 'Activity logs cleared.']); 963 } 295 964 } -
qa-assistant/trunk/includes/Assets.php
r3370854 r3469660 11 11 * Assets handler class 12 12 */ 13 class Assets { 13 class Assets 14 { 14 15 15 16 /** 16 17 * Class constructor 17 18 */ 18 function __construct() { 19 add_action( 'wp_enqueue_scripts', [ $this, 'register_assets' ] ); 20 add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ] ); 19 function __construct() 20 { 21 add_action('wp_enqueue_scripts', [$this, 'register_assets']); 22 add_action('admin_enqueue_scripts', [$this, 'register_assets']); 23 // Enqueue dashboard assets specifically for the settings page 24 add_action('admin_enqueue_scripts', [$this, 'register_dashboard_assets']); 21 25 // Enqueue admin bar assets on frontend if admin bar is showing 22 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_admin_bar_assets' ] ); 26 add_action('wp_enqueue_scripts', [$this, 'enqueue_admin_bar_assets']); 27 // Enqueue Git Branches drawer React app (admin + frontend) 28 add_action('admin_enqueue_scripts', [$this, 'register_drawer_assets']); 29 add_action('wp_enqueue_scripts', [$this, 'register_drawer_assets']); 23 30 } 24 31 … … 28 35 * @return array 29 36 */ 30 public function get_scripts() { 37 public function get_scripts() 38 { 31 39 return [ 32 40 'qa-assistant-script' => [ 33 'src' => QA_ASSISTANT_ASSETS . '/js/frontend.js',34 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/js/frontend.js'),35 'deps' => [ 'jquery']41 'src' => QA_ASSISTANT_ASSETS . '/js/frontend.js', 42 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/js/frontend.js'), 43 'deps' => ['jquery'] 36 44 ], 37 45 'qa-assistant-admin-script' => [ 38 'src' => QA_ASSISTANT_ASSETS . '/js/admin.js',39 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/js/admin.js'),40 'deps' => [ 'jquery', 'wp-util']46 'src' => QA_ASSISTANT_ASSETS . '/js/admin.js', 47 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/js/admin.js'), 48 'deps' => ['jquery', 'wp-util'] 41 49 ], 42 50 'qa-assistant-select2-script' => [ 43 'src' => QA_ASSISTANT_ASSETS . '/js/select2.min.js',44 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/js/select2.min.js'),45 'deps' => [ 'jquery']51 'src' => QA_ASSISTANT_ASSETS . '/js/select2.min.js', 52 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/js/select2.min.js'), 53 'deps' => ['jquery'] 46 54 ], 47 55 // 'qa-assistant-bootstrap-script' => [ … … 68 76 * @return array 69 77 */ 70 public function get_styles() { 78 public function get_styles() 79 { 71 80 return [ 72 81 'qa-assistant-style' => [ 73 'src' => QA_ASSISTANT_ASSETS . '/css/frontend.css',74 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/css/frontend.css')82 'src' => QA_ASSISTANT_ASSETS . '/css/frontend.css', 83 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/css/frontend.css') 75 84 ], 76 85 'qa-assistant-admin-style' => [ 77 'src' => QA_ASSISTANT_ASSETS . '/css/admin.css',78 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/css/admin.css')86 'src' => QA_ASSISTANT_ASSETS . '/css/admin.css', 87 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/css/admin.css') 79 88 ], 80 89 'qa-assistant-select2-style' => [ 81 'src' => QA_ASSISTANT_ASSETS . '/css/select2.min.css',82 'version' => filemtime( QA_ASSISTANT_PATH . '/assets/css/select2.min.css')90 'src' => QA_ASSISTANT_ASSETS . '/css/select2.min.css', 91 'version' => filemtime(QA_ASSISTANT_PATH . '/assets/css/select2.min.css') 83 92 ], 84 93 // 'qa-assistant-bootstrap-style' => [ … … 94 103 * @return void 95 104 */ 96 public function register_assets() { 105 public function register_assets() 106 { 97 107 $scripts = $this->get_scripts(); 98 $styles = $this->get_styles();108 $styles = $this->get_styles(); 99 109 100 110 // Load admin assets only in admin area 101 111 if (is_admin()) { 102 foreach ( $scripts as $handle => $script) {112 foreach ($scripts as $handle => $script) { 103 113 if (strpos($handle, 'admin') !== false || strpos($handle, 'select2') !== false) { 104 $deps = isset( $script['deps']) ? $script['deps'] : false;105 wp_enqueue_script( $handle, $script['src'], $deps, $script['version'], true);106 } 107 } 108 109 foreach ( $styles as $handle => $style) {114 $deps = isset($script['deps']) ? $script['deps'] : false; 115 wp_enqueue_script($handle, $script['src'], $deps, $script['version'], true); 116 } 117 } 118 119 foreach ($styles as $handle => $style) { 110 120 if (strpos($handle, 'admin') !== false || strpos($handle, 'select2') !== false) { 111 $deps = isset( $style['deps']) ? $style['deps'] : false;112 wp_enqueue_style( $handle, $style['src'], $deps, $style['version']);113 } 114 } 115 116 wp_localize_script( 'qa-assistant-admin-script', 'qaAssistant', [117 'nonce' => wp_create_nonce( 'qa-assistant-admin-nonce'),118 'confirm' => __( 'Are you sure?', 'qa-assistant'),119 'error' => __( 'Something went wrong', 'qa-assistant'),121 $deps = isset($style['deps']) ? $style['deps'] : false; 122 wp_enqueue_style($handle, $style['src'], $deps, $style['version']); 123 } 124 } 125 126 wp_localize_script('qa-assistant-admin-script', 'qaAssistant', [ 127 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 128 'confirm' => __('Are you sure?', 'qa-assistant'), 129 'error' => __('Something went wrong', 'qa-assistant'), 120 130 'ajaxUrl' => admin_url('admin-ajax.php'), 121 ] );131 ]); 122 132 } 123 133 124 134 // Load frontend assets only if needed (adjust condition as necessary) 125 135 if (!is_admin()) { 126 foreach ( $scripts as $handle => $script) {136 foreach ($scripts as $handle => $script) { 127 137 if (strpos($handle, 'frontend') !== false) { 128 $deps = isset( $script['deps']) ? $script['deps'] : false;129 wp_enqueue_script( $handle, $script['src'], $deps, $script['version'], true);130 } 131 } 132 133 foreach ( $styles as $handle => $style) {138 $deps = isset($script['deps']) ? $script['deps'] : false; 139 wp_enqueue_script($handle, $script['src'], $deps, $script['version'], true); 140 } 141 } 142 143 foreach ($styles as $handle => $style) { 134 144 if (strpos($handle, 'frontend') !== false) { 135 $deps = isset( $style['deps']) ? $style['deps'] : false;136 wp_enqueue_style( $handle, $style['src'], $deps, $style['version']);145 $deps = isset($style['deps']) ? $style['deps'] : false; 146 wp_enqueue_style($handle, $style['src'], $deps, $style['version']); 137 147 } 138 148 } … … 143 153 * Enqueue admin bar dropdown assets on frontend if admin bar is showing 144 154 */ 145 public function enqueue_admin_bar_assets() { 146 if ( ! is_admin() && is_admin_bar_showing() ) { 155 public function enqueue_admin_bar_assets() 156 { 157 if (!is_admin() && is_admin_bar_showing()) { 147 158 // Enqueue styles/scripts needed for the admin bar dropdown 148 wp_enqueue_style( 'qa-assistant-admin-style', QA_ASSISTANT_ASSETS . '/css/admin.css', [], filemtime( QA_ASSISTANT_PATH . '/assets/css/admin.css' ));149 wp_enqueue_style( 'qa-assistant-select2-style', QA_ASSISTANT_ASSETS . '/css/select2.min.css', [], filemtime( QA_ASSISTANT_PATH . '/assets/css/select2.min.css' ));150 wp_enqueue_script( 'qa-assistant-select2-script', QA_ASSISTANT_ASSETS . '/js/select2.min.js', [ 'jquery' ], filemtime( QA_ASSISTANT_PATH . '/assets/js/select2.min.js' ), true);151 wp_enqueue_script( 'qa-assistant-admin-script', QA_ASSISTANT_ASSETS . '/js/admin.js', [ 'jquery', 'wp-util' ], filemtime( QA_ASSISTANT_PATH . '/assets/js/admin.js' ), true);159 wp_enqueue_style('qa-assistant-admin-style', QA_ASSISTANT_ASSETS . '/css/admin.css', [], filemtime(QA_ASSISTANT_PATH . '/assets/css/admin.css')); 160 wp_enqueue_style('qa-assistant-select2-style', QA_ASSISTANT_ASSETS . '/css/select2.min.css', [], filemtime(QA_ASSISTANT_PATH . '/assets/css/select2.min.css')); 161 wp_enqueue_script('qa-assistant-select2-script', QA_ASSISTANT_ASSETS . '/js/select2.min.js', ['jquery'], filemtime(QA_ASSISTANT_PATH . '/assets/js/select2.min.js'), true); 162 wp_enqueue_script('qa-assistant-admin-script', QA_ASSISTANT_ASSETS . '/js/admin.js', ['jquery', 'wp-util'], filemtime(QA_ASSISTANT_PATH . '/assets/js/admin.js'), true); 152 163 // Localize script for AJAX and nonce 153 wp_localize_script( 'qa-assistant-admin-script', 'qaAssistant', [154 'nonce' => wp_create_nonce( 'qa-assistant-admin-nonce'),155 'confirm' => __( 'Are you sure?', 'qa-assistant'),156 'error' => __( 'Something went wrong', 'qa-assistant'),164 wp_localize_script('qa-assistant-admin-script', 'qaAssistant', [ 165 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 166 'confirm' => __('Are you sure?', 'qa-assistant'), 167 'error' => __('Something went wrong', 'qa-assistant'), 157 168 'ajaxUrl' => admin_url('admin-ajax.php'), 158 ] ); 159 } 169 ]); 170 } 171 } 172 173 /** 174 * Register React Dashboard assets 175 */ 176 /** 177 * Register React Dashboard assets 178 * 179 * @param string $hook Current admin page hook 180 */ 181 public function register_dashboard_assets($hook) 182 { 183 // Only load on QA Assistant page 184 if ($hook !== 'tools_page_qa-assistant') { 185 return; 186 } 187 188 $build_dir = QA_ASSISTANT_PATH . '/build'; 189 $build_url = QA_ASSISTANT_URL . '/build'; 190 191 if (!file_exists($build_dir . '/index.asset.php')) { 192 return; 193 } 194 195 $asset_file = include $build_dir . '/index.asset.php'; 196 197 wp_enqueue_script( 198 'qa-assistant-dashboard', 199 $build_url . '/index.js', 200 $asset_file['dependencies'], 201 $asset_file['version'], 202 true 203 ); 204 205 wp_enqueue_style( 206 'qa-assistant-dashboard-style', 207 $build_url . '/index.css', 208 [], 209 $asset_file['version'] 210 ); 211 212 // Localize script with server-side data 213 wp_localize_script('qa-assistant-dashboard', 'qaAssistantData', [ 214 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 215 'clone_nonce' => wp_create_nonce('qa_assistant_clone_repo'), 216 'ajaxUrl' => admin_url('admin-ajax.php'), 217 'pluginUrl' => QA_ASSISTANT_PLUGIN_URL, 218 ]); 219 } 220 221 /** 222 * Register Git Branches Drawer React assets. 223 * Loads on both admin and frontend when admin bar is visible. 224 */ 225 public function register_drawer_assets() 226 { 227 // Only load for logged-in users with proper capabilities 228 if (!is_user_logged_in() || !current_user_can('manage_options')) { 229 return; 230 } 231 232 // On frontend, only load if admin bar is showing 233 if (!is_admin() && !is_admin_bar_showing()) { 234 return; 235 } 236 237 $build_dir = QA_ASSISTANT_PATH . '/build/git-drawer'; 238 $build_url = QA_ASSISTANT_URL . '/build/git-drawer'; 239 240 if (!file_exists($build_dir . '/index.asset.php')) { 241 return; 242 } 243 244 $asset_file = include $build_dir . '/index.asset.php'; 245 246 wp_enqueue_script( 247 'qa-git-drawer', 248 $build_url . '/index.js', 249 $asset_file['dependencies'], 250 $asset_file['version'], 251 true 252 ); 253 254 wp_enqueue_style( 255 'qa-git-drawer-style', 256 $build_url . '/index.css', 257 [], 258 $asset_file['version'] 259 ); 260 261 // Pass data to the drawer React app 262 wp_localize_script('qa-git-drawer', 'qaGitDrawer', [ 263 'ajaxUrl' => admin_url('admin-ajax.php'), 264 'nonce' => wp_create_nonce('qa-assistant-admin-nonce'), 265 'pluginUrl' => QA_ASSISTANT_PLUGIN_URL, 266 ]); 160 267 } 161 268 } -
qa-assistant/trunk/includes/GitManager.php
r3370854 r3469660 164 164 $hasChanges = $repo->hasChanges(); 165 165 $branches = $this->getBranches($path, true, $force_refresh); 166 166 167 167 $status = [ 168 168 'valid' => true, … … 467 467 } 468 468 } 469 470 /** 471 * Stash uncommitted changes 472 * 473 * @param string $path Repository path 474 * @return array Operation result 475 */ 476 public function stashChanges($path) 477 { 478 if (!$this->isGitRepository($path)) { 479 return [ 480 'success' => false, 481 'error' => 'Not a Git repository' 482 ]; 483 } 484 485 try { 486 $repo = $this->git->open($path); 487 488 if (!$repo->hasChanges()) { 489 return [ 490 'success' => false, 491 'error' => 'No local changes to stash' 492 ]; 493 } 494 $repo->execute(['stash', 'push', '-u', '-m', 'QA Assistant Auto-Stash before pull']); 495 496 // Invalidate cache 497 delete_transient('qa_assistant_repo_status_' . md5($path)); 498 499 return [ 500 'success' => true, 501 'message' => 'Changes stashed successfully' 502 ]; 503 504 } catch (GitException $e) { 505 return [ 506 'success' => false, 507 'error' => 'Stash operation failed: ' . $e->getMessage() 508 ]; 509 } 510 } 511 512 /** 513 * Commit uncommitted changes 514 * 515 * @param string $path Repository path 516 * @param string $message Commit message 517 * @return array Operation result 518 */ 519 public function commitChanges($path, $message) 520 { 521 if (!$this->isGitRepository($path)) { 522 return [ 523 'success' => false, 524 'error' => 'Not a Git repository' 525 ]; 526 } 527 528 if (empty(trim($message))) { 529 return [ 530 'success' => false, 531 'error' => 'Commit message cannot be empty' 532 ]; 533 } 534 535 try { 536 $repo = $this->git->open($path); 537 538 if (!$repo->hasChanges()) { 539 return [ 540 'success' => false, 541 'error' => 'No local changes to commit' 542 ]; 543 } 544 545 // Add all changes 546 $repo->execute(['add', '.']); 547 548 // Commit 549 $repo->execute(['commit', '-m', $message]); 550 551 // Invalidate cache 552 delete_transient('qa_assistant_repo_status_' . md5($path)); 553 554 return [ 555 'success' => true, 556 'message' => 'Changes committed successfully' 557 ]; 558 559 } catch (GitException $e) { 560 return [ 561 'success' => false, 562 'error' => 'Commit operation failed: ' . $e->getMessage() 563 ]; 564 } 565 } 566 567 /** 568 * Clone a repository 569 * 570 * @param string $url Repository URL 571 * @param string $targetPath Target path to clone into 572 * @return array Operation result 573 */ 574 public function cloneRepository($url, $targetPath) 575 { 576 // Basic validation 577 if (empty($url) || empty($targetPath)) { 578 return [ 579 'success' => false, 580 'error' => 'Repository URL and target path are required' 581 ]; 582 } 583 584 // Check if target directory already exists 585 if (file_exists($targetPath)) { 586 return [ 587 'success' => false, 588 'error' => 'Target directory already exists. Please remove or rename it first.', 589 'target_exists' => true 590 ]; 591 } 592 593 try { 594 // Clone the repository 595 $this->git->cloneRepository($url, $targetPath); 596 597 return [ 598 'success' => true, 599 'message' => 'Repository successfully cloned', 600 'path' => $targetPath 601 ]; 602 } catch (GitException $e) { 603 return [ 604 'success' => false, 605 'error' => 'Clone failed: ' . $e->getMessage() 606 ]; 607 } 608 } 469 609 } -
qa-assistant/trunk/qa-assistant.php
r3370854 r3469660 4 4 Plugin URI: https://obayedmamur.com/qa-assistant 5 5 Description: A comprehensive tool for SQA Engineers with GitHub Desktop-like Git branch switching functionality. 6 Version: 1.0.36 Version: 2.0.0 7 7 Author: Obayed Mamur 8 8 Author URI: https://obayedmamur.com … … 10 10 */ 11 11 12 if (! defined('ABSPATH')) {12 if (!defined('ABSPATH')) { 13 13 exit; 14 14 } 15 15 16 16 // Define plugin constants 17 define('QA_ASSISTANT_VERSION', ' 1.0.3');17 define('QA_ASSISTANT_VERSION', '2.0.0'); 18 18 define('QA_ASSISTANT_PLUGIN_FILE', __FILE__); 19 19 define('QA_ASSISTANT_PLUGIN_DIR', plugin_dir_path(__FILE__)); … … 27 27 * @return string The absolute path to the plugin directory 28 28 */ 29 function qa_assistant_get_plugin_path($plugin_dir) { 29 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug. 30 function qa_assistant_get_plugin_path($plugin_dir) 31 { 30 32 // Use WordPress function to get plugins directory 31 33 $plugins_dir = dirname(plugin_dir_path(__FILE__)); … … 68 70 add_action('plugins_loaded', [$this, 'init_plugin']); 69 71 70 add_action('admin_bar_menu', [$this, 'add_git_branch_to_admin_bar'], 100); 72 // Add "Settings" link to plugin action links on the Plugins page 73 add_filter('plugin_action_links_' . QA_ASSISTANT_PLUGIN_BASENAME, [$this, 'plugin_action_links']); 74 75 71 76 } 72 77 … … 80 85 static $instance = false; 81 86 82 if (! $instance) {87 if (!$instance) { 83 88 $instance = new self(); 84 89 } … … 121 126 } 122 127 128 // Initialize Admin Bar 129 if (is_user_logged_in()) { 130 new QaAssistant\Admin\AdminBar(); 131 } 132 123 133 new QaAssistant\API(); 134 } 135 136 /** 137 * Add "Settings" link to plugin action links 138 * 139 * @param array $links Existing plugin action links 140 * @return array Modified plugin action links 141 */ 142 public function plugin_action_links($links) 143 { 144 $settings_link = '<a href="' . admin_url('tools.php?page=qa-assistant') . '">Settings</a>'; 145 array_unshift($links, $settings_link); 146 return $links; 124 147 } 125 148 … … 146 169 return $this->gitManager->getCurrentBranch($path, $force_refresh); 147 170 } 148 149 public function add_git_branch_to_admin_bar($wp_admin_bar)150 {151 // List of plugin directories with their aliases and custom colors152 $qa_assistant_settings = get_option('qa_assistant_settings');153 $qa_assistant_settings = maybe_unserialize($qa_assistant_settings);154 155 if (!is_array($qa_assistant_settings)) {156 return;157 }158 159 $plugin_dirs = $qa_assistant_settings['selected_plugins'];160 $plugin_dirs = array_combine($plugin_dirs, $plugin_dirs);161 162 foreach ($plugin_dirs as $plugin_dir => $settings) {163 $path = qa_assistant_get_plugin_path($plugin_dir);164 $currentBranch = $this->get_git_branch($path);165 if (!$currentBranch) {166 continue;167 }168 169 // Get all branches using GitManager with caching170 $branches = $this->gitManager->getBranches($path, false);171 172 // Use alias or plugin directory name if alias is not provided173 $alias = isset($settings['alias']) ? $settings['alias'] : $plugin_dir;174 175 // Use custom color or generate a random one if not provided176 $color = isset($settings['color']) ? $settings['color'] : '#00fffe';177 178 // Add node to the admin bar for each plugin directory as a Dropdown Sub Menu Item179 if (count($plugin_dirs) > 2) {180 $wp_admin_bar->add_node(array(181 'id' => 'git_branches',182 'title' => '<i class="ab-icon dashicons-share"></i> Git Branches',183 'href' => '',184 ));185 $wp_admin_bar->add_node(array(186 'id' => 'git_branch_' . sanitize_title($plugin_dir),187 'title' => esc_html($alias) . ' (<span style="color: ' . esc_attr($color) . ';">' . esc_html($currentBranch) . '</span>)',188 'href' => '',189 'parent' => 'git_branches',190 'meta' => array('class' => 'qa_assistant_git-branch'),191 ));192 193 // Add pull button for current branch194 $pull_button_id = 'git_pull_' . sanitize_title($plugin_dir);195 $wp_admin_bar->add_node(array(196 'id' => $pull_button_id,197 'title' => 'Pull Latest Changes <svg class="qa-icon qa-pull-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7,10 12,15 17,10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',198 'href' => '#',199 'parent' => 'git_branch_' . sanitize_title($plugin_dir),200 'meta' => array(201 'class' => 'qa-pull-button',202 'onclick' => 'qaAssistantPull("' . esc_js($plugin_dir) . '"); return false;'203 ),204 ));205 206 // Add refresh button to fetch latest branches207 $refresh_button_id = 'git_refresh_' . sanitize_title($plugin_dir);208 $wp_admin_bar->add_node(array(209 'id' => $refresh_button_id,210 'title' => 'Refresh Branches <svg class="qa-icon qa-refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M3 21v-5h5"></path></svg>',211 'href' => '#',212 'parent' => 'git_branch_' . sanitize_title($plugin_dir),213 'meta' => array(214 'class' => 'qa-refresh-button',215 'onclick' => 'qaAssistantRefresh("' . esc_js($plugin_dir) . '"); return false;'216 ),217 ));218 219 // Add search hint for branches if there are many branches220 if (count($branches) > 3) {221 $wp_admin_bar->add_node(array(222 'id' => 'git_branch_search_hint_' . sanitize_title($plugin_dir),223 'title' => '🔍 Type to search branches...<span class="qa-search-cursor">|</span>',224 'href' => '#',225 'parent' => 'git_branch_' . sanitize_title($plugin_dir),226 'meta' => array('class' => 'qa-branch-search-hint'),227 ));228 }229 foreach ($branches as $branchItem) {230 $isCurrentBranch = ($branchItem === $currentBranch);231 $branchClass = 'qa_assistant_git-branch-list-items';232 if ($isCurrentBranch) {233 $branchClass .= ' current-branch';234 }235 236 $wp_admin_bar->add_node(array(237 'id' => 'git_branch_' . sanitize_title($plugin_dir) . '_' . sanitize_title($branchItem),238 'title' => esc_attr($branchItem),239 'href' => '#',240 'parent' => 'git_branch_' . sanitize_title($plugin_dir),241 'data-branch' => esc_attr($branchItem),242 'meta' => array(243 'class' => $branchClass,244 'data-plugin-dir' => esc_attr($plugin_dir),245 'data-branch-name' => esc_attr($branchItem),246 ),247 ));248 }249 } else {250 $wp_admin_bar->add_node(array(251 'id' => 'git_branch_' . sanitize_title($plugin_dir),252 'title' => esc_html($alias) . ' (<span style="color: ' . esc_attr($color) . ';">' . esc_html($currentBranch) . '</span>)',253 'href' => '',254 'meta' => array('class' => 'qa_assistant_git-branch'),255 ));256 257 // Add pull button for current branch258 $pull_button_id = 'git_pull_' . sanitize_title($plugin_dir);259 $wp_admin_bar->add_node(array(260 'id' => $pull_button_id,261 'title' => 'Pull Latest Changes <svg class="qa-icon qa-pull-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7,10 12,15 17,10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',262 'href' => '#',263 'parent' => 'git_branch_' . sanitize_title($plugin_dir),264 'meta' => array(265 'class' => 'qa-pull-button',266 'onclick' => 'qaAssistantPull("' . esc_js($plugin_dir) . '"); return false;'267 ),268 ));269 270 // Add refresh button to fetch latest branches271 $refresh_button_id = 'git_refresh_' . sanitize_title($plugin_dir);272 $wp_admin_bar->add_node(array(273 'id' => $refresh_button_id,274 'title' => 'Refresh Branches <svg class="qa-icon qa-refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M3 21v-5h5"></path></svg>',275 'href' => '#',276 'parent' => 'git_branch_' . sanitize_title($plugin_dir),277 'meta' => array(278 'class' => 'qa-refresh-button',279 'onclick' => 'qaAssistantRefresh("' . esc_js($plugin_dir) . '"); return false;'280 ),281 ));282 283 // Add search hint for branches if there are many branches284 if (count($branches) > 3) {285 $wp_admin_bar->add_node(array(286 'id' => 'git_branch_search_hint_' . sanitize_title($plugin_dir),287 'title' => '🔍 Type to search branches...<span class="qa-search-cursor">|</span>',288 'href' => '#',289 'parent' => 'git_branch_' . sanitize_title($plugin_dir),290 'meta' => array('class' => 'qa-branch-search-hint'),291 ));292 }293 294 foreach ($branches as $branchItem) {295 $isCurrentBranch = ($branchItem === $currentBranch);296 $branchClass = 'qa_assistant_git-branch-list-items';297 if ($isCurrentBranch) {298 $branchClass .= ' current-branch';299 }300 301 $wp_admin_bar->add_node(array(302 'id' => 'git_branch_' . sanitize_title($plugin_dir) . '_' . sanitize_title($branchItem),303 'title' => esc_attr($branchItem),304 'href' => '#',305 'parent' => 'git_branch_' . sanitize_title($plugin_dir),306 'data-branch' => esc_attr($branchItem),307 'meta' => array(308 'class' => $branchClass,309 'data-plugin-dir' => esc_attr($plugin_dir),310 'data-branch-name' => esc_attr($branchItem),311 ),312 ));313 }314 }315 }316 }317 171 } 318 172 … … 322 176 * @return \Qa_Assistant 323 177 */ 178 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug. 324 179 function qa_assistant() 325 180 { … … 329 184 // Call the plugin 330 185 qa_assistant(); 186 187 // Test uncommitted change for git pull modal -
qa-assistant/trunk/readme.txt
r3370854 r3469660 3 3 Tags: qa assistant, quality assurance, help, sqa helper tool 4 4 Requires at least: 5.0 5 Tested up to: 6. 86 Requires PHP: 7.47 Stable tag: 1.0.35 Tested up to: 6.9 6 Requires PHP: 8.0 7 Stable tag: 2.0.0 8 8 License: GPLv3 9 9 License URI: https://opensource.org/licenses/GPL-3.0 … … 91 91 == Changelog == 92 92 93 = 2.0.0 - 25/02/2026 = 94 95 **� Major Features & Enhancements:** 96 - Added: Git Branches Drawer feature with new React components and admin bar integration 97 - Added: Modal and backend logic to handle uncommitted changes during Git pull operations (commit or stash) 98 - Added: Custom confirmation modal for plugin removal from the dashboard 99 - Added: Plugin settings link added to the plugin list for easier access 100 101 **🎨 UI/UX Improvements:** 102 - Enhanced: Major UI revamp including a new notification system and animated skeleton loaders 103 - Enhanced: Robust CSS isolation using PostCSS prefixing and inline Tailwind theme 104 - Enhanced: Success button variant and improved Git branch item visuals 105 106 **�🔒 Security & Code Quality:** 107 - Added: PHPCS ignore annotations for correctly-prefixed global functions and classes 108 - Refactored: Refined AJAX URL parsing and plugin data handling 109 110 **⚙️ Compatibility:** 111 - Updated: Minimum PHP requirement from 7.4 to 8.0 to match Composer dependency requirements 112 - Updated: "Tested up to" WordPress version from 6.8 to 6.9 113 - Added: Explicit PHP >= 8.0 constraint in `composer.json` for early validation 114 93 115 = 1.0.3 - Initial Release = 94 116 -
qa-assistant/trunk/templates/settings-page.php
r3370854 r3469660 2 2 // Prevent direct access 3 3 if (!defined('ABSPATH')) { 4 exit; // Exit if accessed directly4 exit; 5 5 } 6 6 ?> 7 <div class="wrap"> 8 9 <h1><?php esc_html_e('QA Assistant', 'qa-assistant'); ?></h1> 10 11 <?php 12 // Display admin notices 13 $admin_notice = get_transient('qa_assistant_admin_notice'); 14 if ($admin_notice) { 15 delete_transient('qa_assistant_admin_notice'); 16 $notice_class = 'notice notice-' . esc_attr($admin_notice['type']) . ' is-dismissible'; 17 ?> 18 <div class="<?php echo esc_attr($notice_class); ?>"> 19 <p><?php echo esc_html($admin_notice['message']); ?></p> 20 </div> 21 <?php 22 } 23 ?> 24 25 <div class="qa-assistant-content"> 26 27 <h1>Settings</h1> 28 <div class="qa-assistant-tabs" id="settingsTab" role="tablist"> 29 <div class="qa-assistant-tab-item"> 30 <a class="qa-assistant-tab-link active" id="git-settings-tab" data-tab="git-settings-tab" href="#git-settings-tab" role="tab" aria-controls="git-settings-tab" aria-selected="true">Git Settings</a> 31 </div> 32 </div> 33 34 <div class="qa-assistant-tab-content" id="settingsTabContent"> 35 <div class="qa-assistant-tab-pane active" id="git-settings-tab" role="tabpanel" aria-labelledby="git-settings-tab"> 36 <?php if (! empty($available_plugins)) { ?> 37 38 <form method="post" action="<?php echo esc_url('#'); ?>" class="qa-assistant-form"> 39 <?php wp_nonce_field('qa_assistant_settings_form_action', 'qa_assistant_settings_form_nonce'); ?> 40 41 <h2> 42 <label class="title" for="qa-assistant__plugins-dropdown"> 43 <?php esc_html_e('Git Branch Display', 'qa-assistant'); ?> 44 </label> 45 </h2> 46 47 <p id="qa-assistant__description"> 48 <?php esc_html_e('Select plugins to display Git branch information in the admin bar. You can switch between branches directly from the admin bar with GitHub Desktop-like functionality.', 'qa-assistant'); ?> 49 </p> 50 51 <div class="qa-assistant-feature-info"> 52 <h3>✨ Enhanced Features:</h3> 53 <ul> 54 <li>🔄 <strong>One-click branch switching</strong> - Switch branches directly from the admin bar</li> 55 <li>✅ <strong>Current branch indicator</strong> - See which branch you're currently on</li> 56 <li>⚠️ <strong>Uncommitted changes detection</strong> - Get warnings before switching with unsaved changes</li> 57 <li>🔒 <strong>Force switch option</strong> - Option to discard local changes when switching</li> 58 <li>📢 <strong>Real-time notifications</strong> - Get instant feedback on Git operations</li> 59 <li>🎨 <strong>Visual status indicators</strong> - Color-coded branch status in the admin bar</li> 60 </ul> 61 </div> 62 63 <select class="qa-assistant-select2" id="qa-assistant__plugins-dropdown" name="qa_assistant_plugins[]" aria-describedby="qa-assistant__description" multiple="multiple"> 64 <?php if (1 !== count($available_plugins)) { ?> 65 <option value="" disabled><?php esc_html_e('Select Plugin', 'qa-assistant'); ?></option> 66 <?php } ?> 67 <?php foreach ($available_plugins as $plugin_basename => $available_plugin) { ?> 68 <?php $plugin_dir = explode('/', $plugin_basename)[0]; ?> 69 <option value="<?php echo esc_attr($plugin_dir); ?>" <?php echo in_array($plugin_dir, $selected_plugins) ? 'selected' : ''; ?>> 70 <?php echo esc_html($available_plugin['Name']); ?> 71 </option> 72 <?php } ?> 73 </select> 74 75 <input type="submit" value="<?php esc_attr_e('Save', 'qa-assistant'); ?>" id="qa-assistant__submit" class="qa-assistant-settings-save button button-primary" /> 76 <span id="qa-assistant__spinner" class="spinner" style="float: none;"></span> 77 </form> 78 79 <?php 80 // Show currently selected plugins 81 $current_settings = maybe_unserialize(get_option('qa_assistant_settings', array())); 82 if (!empty($current_settings['selected_plugins'])) { 83 ?> 84 <div class="qa-assistant-selected-plugins"> 85 <h3><?php esc_html_e('Currently Selected Plugins', 'qa-assistant'); ?></h3> 86 <div class="qa-selected-plugins-grid"> 87 <?php 88 foreach ($current_settings['selected_plugins'] as $plugin_dir) { 89 // Find the plugin name from available plugins 90 $plugin_name = $plugin_dir; 91 foreach ($available_plugins as $plugin_basename => $plugin_data) { 92 if (strpos($plugin_basename, $plugin_dir . '/') === 0) { 93 $plugin_name = $plugin_data['Name']; 94 break; 95 } 96 } 97 98 // Check Git status 99 $plugin_path = qa_assistant_get_plugin_path($plugin_dir); 100 $is_git_repo = is_dir($plugin_path . '/.git'); 101 $current_branch = ''; 102 $git_status = 'Not a Git repository'; 103 $status_class = 'no-git'; 104 105 if ($is_git_repo) { 106 $git_head_file = $plugin_path . '/.git/HEAD'; 107 if (file_exists($git_head_file)) { 108 // Use WordPress filesystem API instead of file_get_contents 109 global $wp_filesystem; 110 if (empty($wp_filesystem)) { 111 require_once ABSPATH . '/wp-admin/includes/file.php'; 112 WP_Filesystem(); 113 } 114 $contents = $wp_filesystem->get_contents($git_head_file); 115 if ($contents && strpos($contents, 'ref:') === 0) { 116 $current_branch = trim(str_replace('ref: refs/heads/', '', $contents)); 117 $git_status = 'Branch: ' . $current_branch; 118 $status_class = 'has-git'; 119 } 120 } 121 } 122 ?> 123 <div class="qa-plugin-card <?php echo esc_attr($status_class); ?>" data-plugin-dir="<?php echo esc_attr($plugin_dir); ?>"> 124 <div class="qa-plugin-header"> 125 <h4><?php echo esc_html($plugin_name); ?></h4> 126 <span class="qa-plugin-dir"><?php echo esc_html($plugin_dir); ?></span> 127 </div> 128 <div class="qa-plugin-status"> 129 <span class="qa-git-status <?php echo esc_attr($status_class); ?>"> 130 <?php if ($is_git_repo): ?> 131 <span class="dashicons dashicons-admin-tools"></span> 132 <?php else: ?> 133 <span class="dashicons dashicons-warning"></span> 134 <?php endif; ?> 135 <?php echo esc_html($git_status); ?> 136 </span> 137 </div> 138 </div> 139 <?php 140 } 141 ?> 142 </div> 143 </div> 144 <?php 145 } 146 ?> 147 148 <?php } else { ?> 149 150 <h2><?php esc_html_e('No plugins available.', 'qa-assistant'); ?></h2> 151 152 <?php } ?> 153 </div> 154 </div> 155 156 </div> 157 158 </div> 7 <div id="qa-assistant-dashboard"></div> -
qa-assistant/trunk/vendor/composer/autoload_classmap.php
r3370854 r3469660 24 24 'CzProject\\GitPhp\\Runners\\OldGitRunner' => $vendorDir . '/czproject/git-php/src/Runners/OldGitRunner.php', 25 25 'CzProject\\GitPhp\\StaticClassException' => $vendorDir . '/czproject/git-php/src/exceptions.php', 26 'QaAssistant\\API' => $baseDir . '/includes/API.php',27 'QaAssistant\\Admin' => $baseDir . '/includes/Admin.php',28 'QaAssistant\\Admin\\Menu' => $baseDir . '/includes/Admin/Menu.php',29 'QaAssistant\\Admin\\Settings' => $baseDir . '/includes/Admin/Settings.php',30 'QaAssistant\\Ajax' => $baseDir . '/includes/Ajax.php',31 'QaAssistant\\Assets' => $baseDir . '/includes/Assets.php',32 'QaAssistant\\Frontend' => $baseDir . '/includes/Frontend.php',33 'QaAssistant\\Frontend\\Shortcode' => $baseDir . '/includes/Frontend/Shortcode.php',34 'QaAssistant\\GitManager' => $baseDir . '/includes/GitManager.php',35 'QaAssistant\\Installer' => $baseDir . '/includes/Installer.php',36 26 ); -
qa-assistant/trunk/vendor/composer/autoload_static.php
r3370854 r3469660 12 12 13 13 public static $prefixLengthsPsr4 = array ( 14 'Q' => 14 'Q' => 15 15 array ( 16 16 'QaAssistant\\' => 12, … … 19 19 20 20 public static $prefixDirsPsr4 = array ( 21 'QaAssistant\\' => 21 'QaAssistant\\' => 22 22 array ( 23 23 0 => __DIR__ . '/../..' . '/includes', … … 43 43 'CzProject\\GitPhp\\Runners\\OldGitRunner' => __DIR__ . '/..' . '/czproject/git-php/src/Runners/OldGitRunner.php', 44 44 'CzProject\\GitPhp\\StaticClassException' => __DIR__ . '/..' . '/czproject/git-php/src/exceptions.php', 45 'QaAssistant\\API' => __DIR__ . '/../..' . '/includes/API.php',46 'QaAssistant\\Admin' => __DIR__ . '/../..' . '/includes/Admin.php',47 'QaAssistant\\Admin\\Menu' => __DIR__ . '/../..' . '/includes/Admin/Menu.php',48 'QaAssistant\\Admin\\Settings' => __DIR__ . '/../..' . '/includes/Admin/Settings.php',49 'QaAssistant\\Ajax' => __DIR__ . '/../..' . '/includes/Ajax.php',50 'QaAssistant\\Assets' => __DIR__ . '/../..' . '/includes/Assets.php',51 'QaAssistant\\Frontend' => __DIR__ . '/../..' . '/includes/Frontend.php',52 'QaAssistant\\Frontend\\Shortcode' => __DIR__ . '/../..' . '/includes/Frontend/Shortcode.php',53 'QaAssistant\\GitManager' => __DIR__ . '/../..' . '/includes/GitManager.php',54 'QaAssistant\\Installer' => __DIR__ . '/../..' . '/includes/Installer.php',55 45 ); 56 46 -
qa-assistant/trunk/vendor/composer/installed.json
r3370854 r3469660 61 61 } 62 62 ], 63 "dev": false,63 "dev": true, 64 64 "dev-package-names": [] 65 65 } -
qa-assistant/trunk/vendor/composer/installed.php
r3370854 r3469660 4 4 'pretty_version' => 'dev-main', 5 5 'version' => 'dev-main', 6 'reference' => ' 86dd52d03562c58827cfda99707efa65f6b31e7a',6 'reference' => '4b3e48af653456751624d26a957aa04e19a791b1', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', 9 9 'aliases' => array(), 10 'dev' => false,10 'dev' => true, 11 11 ), 12 12 'versions' => array( … … 23 23 'pretty_version' => 'dev-main', 24 24 'version' => 'dev-main', 25 'reference' => ' 86dd52d03562c58827cfda99707efa65f6b31e7a',25 'reference' => '4b3e48af653456751624d26a957aa04e19a791b1', 26 26 'type' => 'wordpress-plugin', 27 27 'install_path' => __DIR__ . '/../../', -
qa-assistant/trunk/vendor/composer/platform_check.php
r3370854 r3469660 20 20 } 21 21 } 22 trigger_error( 23 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 E_USER_ERROR 22 throw new \RuntimeException( 23 'Composer detected issues in your platform: ' . implode(' ', $issues) 25 24 ); 26 25 }
Note: See TracChangeset
for help on using the changeset viewer.