Plugin Directory

Changeset 3469660


Ignore:
Timestamp:
02/25/2026 05:57:48 PM (4 weeks ago)
Author:
obayedmamur
Message:

Update to version 2.0.0 from GitHub

Location:
qa-assistant
Files:
52 added
30 edited
1 copied

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;
    811    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 */
    3813    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 {
    30225    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 */
    36927    font-size: 13px !important;
    37028    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;
    37336    display: flex !important;
    37437    align-items: center !important;
     
    37639}
    37740
    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 {
    48471    display: flex !important;
    48572    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 {
    592268    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;
    687296    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;
    720310    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;
    777319    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);
    806351    }
    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 --- */
    857359.qa-notification {
    858360    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;
    861368    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);
    867371    overflow: hidden;
    868     transform: translateX(100%);
    869     opacity: 0;
    870     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    871372}
    872373
    873374.qa-notification-show {
    874375    transform: translateX(0);
    875     opacity: 1;
    876376}
    877377
    878378.qa-notification-hide {
    879     transform: translateX(100%);
    880     opacity: 0;
     379    transform: translateX(120%);
    881380}
    882381
     
    884383    display: flex;
    885384    align-items: flex-start;
    886     padding: 16px 20px;
     385    padding: 16px;
    887386    gap: 12px;
    888387}
     
    892391    width: 24px;
    893392    height: 24px;
    894     border-radius: 50%;
    895     display: flex;
    896     align-items: center;
    897     justify-content: center;
    898393    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;
    924394}
    925395
     
    932402    font-size: 14px;
    933403    font-weight: 600;
    934     color: #111827;
    935     margin-bottom: 2px;
    936     line-height: 1.4;
     404    color: #0f172a;
     405    margin-bottom: 4px;
    937406}
    938407
    939408.qa-notification-message {
    940409    font-size: 13px;
    941     color: #6b7280;
    942     line-height: 1.4;
    943     word-wrap: break-word;
     410    color: #475569;
     411    line-height: 1.5;
    944412}
    945413
    946414.qa-notification-close {
    947415    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 {
    948538    background: none;
    949539    border: none;
     540    color: #64748b;
     541    font-size: 24px;
     542    line-height: 1;
    950543    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;
    952585    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;
    958617    align-items: center;
    959618    justify-content: center;
    960619}
    961620
    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) {
    1002643    background: #2563eb;
    1003644}
    1004645
    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) {
    1006652    background: #d97706;
    1007653}
    1008654
    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        });
    5102
    6103        // Add Git repository validation for plugin selection
    7104        initializeGitValidation();
    8105
    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);
    41169                } 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                    }
    72187                } 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        });
    103201
    104202        function highlightSearchTerm(text, searchTerm) {
    105203            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');
    108207            return text.replace(regex, '<span class="qa-branch-highlight">$1</span>');
    109208        }
    110209
    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 ---
    120211
    121212        // 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) {
    123214            e.preventDefault();
    124215
     
    161252
    162253            switchBranch(pluginDir, branchName, false)
    163                 .done(function(response) {
     254                .done(function (response) {
    164255                    if (response.success) {
    165256                        showNotification(`Successfully switched to branch: ${response.data.current_branch}`, 'success');
     
    176267                    }
    177268                })
    178                 .fail(function(xhr, status, error) {
     269                .fail(function (xhr, status, error) {
    179270                    showNotification('Network error occurred. Please try again.', 'error');
    180271                    console.error('Branch switch failed:', error);
    181272                })
    182                 .always(function() {
     273                .always(function () {
    183274                    // Remove loader and re-enable item
    184275                    loader.remove();
     
    195286                if (confirm(`You have uncommitted changes. Do you want to discard them and switch to ${branchName}?`)) {
    196287                    switchBranch(pluginDir, branchName, true)
    197                         .done(function(response) {
     288                        .done(function (response) {
    198289                            if (response.success) {
    199290                                showNotification(`Force switched to branch: ${response.data.current_branch}`, 'success');
     
    229320
    230321            // 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');
    232323
    233324            // Add current-branch class to the new current branch
     
    288379
    289380            // Manual close
    290             notification.find('.qa-notification-close').on('click', function() {
     381            notification.find('.qa-notification-close').on('click', function () {
    291382                hideNotification(notification);
    292383            });
     384
     385            return notification;
    293386        }
    294387
     
    311404
    312405        // Global function for pull operations (called via onclick)
    313         window.qaAssistantPull = function(pluginDir) {
    314             // Show immediate feedback
    315             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');
    316409
    317410            // 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 () {
    319412                return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir);
    320413            });
     
    326419
    327420                pullBranch(pluginDir)
    328                     .done(function(response) {
    329                         if (response.success) {
     421                    .done(function (response) {
     422                        if (response && response.success) {
    330423                            showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success');
    331424                            // Reload page to show updated state
    332425                            setTimeout(() => location.reload(), 1500);
    333426                        } 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);
    336430                            } 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');
    338433                            }
    339434                        }
    340435                    })
    341                     .fail(function(xhr, status, error) {
     436                    .fail(function (xhr, status, error) {
    342437                        showNotification('Network error occurred during pull. Please try again.', 'error');
    343438                        console.error('Pull failed:', error);
    344439                    })
    345                     .always(function() {
     440                    .always(function () {
    346441                        // Restore button
    347442                        $button.find('.ab-item').html(originalText);
     
    351446                // Fallback if button not found
    352447                pullBranch(pluginDir)
    353                     .done(function(response) {
    354                         if (response.success) {
     448                    .done(function (response) {
     449                        if (response && response.success) {
    355450                            showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success');
    356451                            setTimeout(() => location.reload(), 1500);
    357452                        } 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                            }
    359460                        }
    360461                    })
    361                     .fail(function(xhr, status, error) {
     462                    .fail(function (xhr, status, error) {
    362463                        showNotification('Network error occurred during pull. Please try again.', 'error');
    363464                        console.error('Pull failed:', error);
     
    394495
    395496        // Global function for refreshing branches (called via onclick)
    396         window.qaAssistantRefresh = function(pluginDir) {
     497        window.qaAssistantRefresh = function (pluginDir) {
    397498            // Show immediate feedback
    398499            showNotification('Fetching latest branches from remote...', 'info');
    399500
    400501            // 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 () {
    402503                return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir);
    403504            });
     
    409510
    410511                refreshBranches(pluginDir)
    411                     .done(function(response) {
     512                    .done(function (response) {
    412513                        if (response.success) {
    413514                            let message = response.data.fetch_success
     
    423524                        }
    424525                    })
    425                     .fail(function(xhr, status, error) {
     526                    .fail(function (xhr, status, error) {
    426527                        showNotification('Network error occurred during refresh. Please try again.', 'error');
    427528                        console.error('Refresh failed:', error);
    428529                    })
    429                     .always(function() {
     530                    .always(function () {
    430531                        // Restore button
    431532                        $button.find('.ab-item').html(originalText);
     
    435536                // Fallback if button not found
    436537                refreshBranches(pluginDir)
    437                     .done(function(response) {
     538                    .done(function (response) {
    438539                        if (response.success) {
    439540                            let message = response.data.fetch_success
     
    446547                        }
    447548                    })
    448                     .fail(function(xhr, status, error) {
     549                    .fail(function (xhr, status, error) {
    449550                        showNotification('Network error occurred during refresh. Please try again.', 'error');
    450551                        console.error('Refresh failed:', error);
     
    471572        }
    472573
     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">&times;</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
    473692    });
    474693
     
    478697    function initializeGitValidation() {
    479698        // Add change event listener to plugin selection dropdown
    480         $('.qa-assistant-select2').on('change', function() {
     699        $('.qa-assistant-select2').on('change', function () {
    481700            validateSelectedPlugins();
    482701        });
    483702
    484703        // Add form submission validation
    485         $('.qa-assistant-form').on('submit', function(e) {
     704        $('.qa-assistant-form').on('submit', function (e) {
    486705            if (!validateSelectedPlugins()) {
    487706                e.preventDefault();
     
    499718
    500719        // Check each selected plugin
    501         selectedValues.forEach(function(pluginDir) {
     720        selectedValues.forEach(function (pluginDir) {
    502721            let pluginCard = $(`.qa-plugin-card[data-plugin-dir="${pluginDir}"]`);
    503722            if (pluginCard.length > 0) {
  • qa-assistant/tags/2.0.0/composer.json

    r3370854 r3469660  
    1212    "minimum-stability": "stable",
    1313    "require": {
     14        "php": ">=8.0",
    1415        "czproject/git-php": "^4.0"
    1516    },
     
    1819            "QaAssistant\\": "includes/"
    1920        },
    20         "files": [ "includes/functions.php" ]
     21        "files": [
     22            "includes/functions.php"
     23        ]
    2124    }
    2225}
  • qa-assistant/tags/2.0.0/includes/Admin/Menu.php

    r3370854 r3469660  
    1111 * The Menu handler class
    1212 */
    13 class Menu {
     13class Menu
     14{
    1415
    1516    /**
    1617     * Initialize the class
    1718     */
    18     function __construct( ) {
    19         add_action( 'admin_menu', [ $this, 'admin_menu' ] );
     19    function __construct()
     20    {
     21        add_action('admin_menu', [$this, 'admin_menu']);
    2022    }
    2123
     
    2527     * @return void
    2628     */
    27     public function admin_menu() {
     29    public function admin_menu()
     30    {
    2831        $parent_slug = 'qa-assistant';
    29         $capability = apply_filters('qa-assistant/menu/capability', 'manage_options');
     32        $capability = apply_filters('qa_assistant_menu_capability', 'manage_options');
    3033
    3134        // $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');
    3235        // 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']);
    3437
    3538        add_action('admin_head-' . $hook, [$this, 'enqueue_assets']);
     
    4144     * @return void
    4245     */
    43     public function settings_page() {
     46    public function settings_page()
     47    {
    4448        // Check user capabilities
    4549        if (!current_user_can('manage_options')) {
     
    4953        $settings = new Settings();
    5054
    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');
    5761
    5862        $available_plugins = $settings->get_available_plugins();
     
    6468        }
    6569
    66         // Get currently selected plugins for the dropdown
    67         $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();
    6973
    7074        // 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')) {
    7276            // get posted data and sanitize
    7377            $selected_plugins = [];
     
    141145        }
    142146
    143         require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php';
     147        require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php';
    144148    }
    145149
     
    149153     * @return void
    150154     */
    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');
    154159    }
    155160}
  • qa-assistant/tags/2.0.0/includes/Ajax.php

    r3370854 r3469660  
    4242        add_action('wp_ajax_qa_assistant_refresh_branches', [$this, 'refresh_branches']);
    4343
     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
    4448        // Legacy support
    4549        add_action('wp_ajax_qa_assistant_get_branch_data', [$this, 'get_branch_data']);
    4650
     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
    4772        $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        ]);
    48174    }
    49175
     
    84210
    85211        if ($result['success']) {
     212            $this->log_activity('switch', $plugin_dir, $result['current_branch'], 'success', 'Switched to ' . $result['current_branch']);
    86213            wp_send_json_success([
    87214                'message' => $result['message'],
     
    162289
    163290        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
    164296            wp_send_json_success([
    165297                'message' => $result['message'],
    166298                'branch' => $result['branch'],
    167299                'output' => $result['output'] ?? '',
    168                 'plugin_dir' => $plugin_dir
     300                'plugin_dir' => $plugin_dir,
     301                'lastPulled' => time(),
    169302            ]);
    170303        } else {
     
    293426        }
    294427    }
     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    }
    295964}
  • qa-assistant/tags/2.0.0/includes/Assets.php

    r3370854 r3469660  
    1111 * Assets handler class
    1212 */
    13 class Assets {
     13class Assets
     14{
    1415
    1516    /**
    1617     * Class constructor
    1718     */
    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']);
    2125        // 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']);
    2330    }
    2431
     
    2835     * @return array
    2936     */
    30     public function get_scripts() {
     37    public function get_scripts()
     38    {
    3139        return [
    3240            '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']
    3644            ],
    3745            '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']
    4149            ],
    4250            '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']
    4654            ],
    4755            // 'qa-assistant-bootstrap-script' => [
     
    6876     * @return array
    6977     */
    70     public function get_styles() {
     78    public function get_styles()
     79    {
    7180        return [
    7281            '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')
    7584            ],
    7685            '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')
    7988            ],
    8089            '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')
    8392            ],
    8493            // 'qa-assistant-bootstrap-style' => [
     
    94103     * @return void
    95104     */
    96     public function register_assets() {
     105    public function register_assets()
     106    {
    97107        $scripts = $this->get_scripts();
    98         $styles  = $this->get_styles();
     108        $styles = $this->get_styles();
    99109
    100110        // Load admin assets only in admin area
    101111        if (is_admin()) {
    102             foreach ( $scripts as $handle => $script ) {
     112            foreach ($scripts as $handle => $script) {
    103113                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) {
    110120                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'),
    120130                'ajaxUrl' => admin_url('admin-ajax.php'),
    121             ] );
     131            ]);
    122132        }
    123133
    124134        // Load frontend assets only if needed (adjust condition as necessary)
    125135        if (!is_admin()) {
    126             foreach ( $scripts as $handle => $script ) {
     136            foreach ($scripts as $handle => $script) {
    127137                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) {
    134144                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']);
    137147                }
    138148            }
     
    143153     * Enqueue admin bar dropdown assets on frontend if admin bar is showing
    144154     */
    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()) {
    147158            // 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);
    152163            // 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'),
    157168                '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        ]);
    160267    }
    161268}
  • qa-assistant/tags/2.0.0/includes/GitManager.php

    r3370854 r3469660  
    164164            $hasChanges = $repo->hasChanges();
    165165            $branches = $this->getBranches($path, true, $force_refresh);
    166            
     166
    167167            $status = [
    168168                'valid' => true,
     
    467467        }
    468468    }
     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    }
    469609}
  • qa-assistant/tags/2.0.0/qa-assistant.php

    r3370854 r3469660  
    44Plugin URI: https://obayedmamur.com/qa-assistant
    55Description: A comprehensive tool for SQA Engineers with GitHub Desktop-like Git branch switching functionality.
    6 Version: 1.0.3
     6Version: 2.0.0
    77Author: Obayed Mamur
    88Author URI: https://obayedmamur.com
     
    1010*/
    1111
    12 if (! defined('ABSPATH')) {
     12if (!defined('ABSPATH')) {
    1313    exit;
    1414}
    1515
    1616// Define plugin constants
    17 define('QA_ASSISTANT_VERSION', '1.0.3');
     17define('QA_ASSISTANT_VERSION', '2.0.0');
    1818define('QA_ASSISTANT_PLUGIN_FILE', __FILE__);
    1919define('QA_ASSISTANT_PLUGIN_DIR', plugin_dir_path(__FILE__));
     
    2727 * @return string The absolute path to the plugin directory
    2828 */
    29 function qa_assistant_get_plugin_path($plugin_dir) {
     29// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug.
     30function qa_assistant_get_plugin_path($plugin_dir)
     31{
    3032    // Use WordPress function to get plugins directory
    3133    $plugins_dir = dirname(plugin_dir_path(__FILE__));
     
    6870        add_action('plugins_loaded', [$this, 'init_plugin']);
    6971
    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
    7176    }
    7277
     
    8085        static $instance = false;
    8186
    82         if (! $instance) {
     87        if (!$instance) {
    8388            $instance = new self();
    8489        }
     
    121126        }
    122127
     128        // Initialize Admin Bar
     129        if (is_user_logged_in()) {
     130            new QaAssistant\Admin\AdminBar();
     131        }
     132
    123133        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;
    124147    }
    125148
     
    146169        return $this->gitManager->getCurrentBranch($path, $force_refresh);
    147170    }
    148 
    149     public function add_git_branch_to_admin_bar($wp_admin_bar)
    150     {
    151         // List of plugin directories with their aliases and custom colors
    152         $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 caching
    170             $branches = $this->gitManager->getBranches($path, false);
    171 
    172             // Use alias or plugin directory name if alias is not provided
    173             $alias = isset($settings['alias']) ? $settings['alias'] : $plugin_dir;
    174 
    175             // Use custom color or generate a random one if not provided
    176             $color = isset($settings['color']) ? $settings['color'] : '#00fffe';
    177 
    178             // Add node to the admin bar for each plugin directory as a Dropdown Sub Menu Item
    179             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 branch
    194                 $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 branches
    207                 $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 branches
    220                 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 branch
    258                 $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 branches
    271                 $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 branches
    284                 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     }
    317171}
    318172
     
    322176 * @return \Qa_Assistant
    323177 */
     178// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug.
    324179function qa_assistant()
    325180{
     
    329184// Call the plugin
    330185qa_assistant();
     186
     187// Test uncommitted change for git pull modal
  • qa-assistant/tags/2.0.0/readme.txt

    r3370854 r3469660  
    33Tags: qa assistant, quality assurance, help, sqa helper tool
    44Requires at least: 5.0
    5 Tested up to: 6.8
    6 Requires PHP: 7.4
    7 Stable tag: 1.0.3
     5Tested up to: 6.9
     6Requires PHP: 8.0
     7Stable tag: 2.0.0
    88License: GPLv3
    99License URI: https://opensource.org/licenses/GPL-3.0
     
    9191== Changelog ==
    9292
     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
    93115= 1.0.3 - Initial Release =
    94116
  • qa-assistant/tags/2.0.0/templates/settings-page.php

    r3370854 r3469660  
    22// Prevent direct access
    33if (!defined('ABSPATH')) {
    4     exit; // Exit if accessed directly
     4    exit;
    55}
    66?>
    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  
    2424    'CzProject\\GitPhp\\Runners\\OldGitRunner' => $vendorDir . '/czproject/git-php/src/Runners/OldGitRunner.php',
    2525    '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',
    3626);
  • qa-assistant/tags/2.0.0/vendor/composer/autoload_static.php

    r3370854 r3469660  
    1212
    1313    public static $prefixLengthsPsr4 = array (
    14         'Q' => 
     14        'Q' =>
    1515        array (
    1616            'QaAssistant\\' => 12,
     
    1919
    2020    public static $prefixDirsPsr4 = array (
    21         'QaAssistant\\' => 
     21        'QaAssistant\\' =>
    2222        array (
    2323            0 => __DIR__ . '/../..' . '/includes',
     
    4343        'CzProject\\GitPhp\\Runners\\OldGitRunner' => __DIR__ . '/..' . '/czproject/git-php/src/Runners/OldGitRunner.php',
    4444        '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',
    5545    );
    5646
  • qa-assistant/tags/2.0.0/vendor/composer/installed.json

    r3370854 r3469660  
    6161        }
    6262    ],
    63     "dev": false,
     63    "dev": true,
    6464    "dev-package-names": []
    6565}
  • qa-assistant/tags/2.0.0/vendor/composer/installed.php

    r3370854 r3469660  
    44        'pretty_version' => 'dev-main',
    55        'version' => 'dev-main',
    6         'reference' => '86dd52d03562c58827cfda99707efa65f6b31e7a',
     6        'reference' => '4b3e48af653456751624d26a957aa04e19a791b1',
    77        'type' => 'wordpress-plugin',
    88        'install_path' => __DIR__ . '/../../',
    99        'aliases' => array(),
    10         'dev' => false,
     10        'dev' => true,
    1111    ),
    1212    'versions' => array(
     
    2323            'pretty_version' => 'dev-main',
    2424            'version' => 'dev-main',
    25             'reference' => '86dd52d03562c58827cfda99707efa65f6b31e7a',
     25            'reference' => '4b3e48af653456751624d26a957aa04e19a791b1',
    2626            'type' => 'wordpress-plugin',
    2727            'install_path' => __DIR__ . '/../../',
  • qa-assistant/tags/2.0.0/vendor/composer/platform_check.php

    r3370854 r3469660  
    2020        }
    2121    }
    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)
    2524    );
    2625}
  • 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;
    811    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 */
    3813    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 {
    30225    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 */
    36927    font-size: 13px !important;
    37028    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;
    37336    display: flex !important;
    37437    align-items: center !important;
     
    37639}
    37740
    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 {
    48471    display: flex !important;
    48572    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 {
    592268    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;
    687296    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;
    720310    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;
    777319    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);
    806351    }
    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 --- */
    857359.qa-notification {
    858360    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;
    861368    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);
    867371    overflow: hidden;
    868     transform: translateX(100%);
    869     opacity: 0;
    870     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    871372}
    872373
    873374.qa-notification-show {
    874375    transform: translateX(0);
    875     opacity: 1;
    876376}
    877377
    878378.qa-notification-hide {
    879     transform: translateX(100%);
    880     opacity: 0;
     379    transform: translateX(120%);
    881380}
    882381
     
    884383    display: flex;
    885384    align-items: flex-start;
    886     padding: 16px 20px;
     385    padding: 16px;
    887386    gap: 12px;
    888387}
     
    892391    width: 24px;
    893392    height: 24px;
    894     border-radius: 50%;
    895     display: flex;
    896     align-items: center;
    897     justify-content: center;
    898393    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;
    924394}
    925395
     
    932402    font-size: 14px;
    933403    font-weight: 600;
    934     color: #111827;
    935     margin-bottom: 2px;
    936     line-height: 1.4;
     404    color: #0f172a;
     405    margin-bottom: 4px;
    937406}
    938407
    939408.qa-notification-message {
    940409    font-size: 13px;
    941     color: #6b7280;
    942     line-height: 1.4;
    943     word-wrap: break-word;
     410    color: #475569;
     411    line-height: 1.5;
    944412}
    945413
    946414.qa-notification-close {
    947415    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 {
    948538    background: none;
    949539    border: none;
     540    color: #64748b;
     541    font-size: 24px;
     542    line-height: 1;
    950543    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;
    952585    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;
    958617    align-items: center;
    959618    justify-content: center;
    960619}
    961620
    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) {
    1002643    background: #2563eb;
    1003644}
    1004645
    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) {
    1006652    background: #d97706;
    1007653}
    1008654
    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        });
    5102
    6103        // Add Git repository validation for plugin selection
    7104        initializeGitValidation();
    8105
    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);
    41169                } 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                    }
    72187                } 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        });
    103201
    104202        function highlightSearchTerm(text, searchTerm) {
    105203            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');
    108207            return text.replace(regex, '<span class="qa-branch-highlight">$1</span>');
    109208        }
    110209
    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 ---
    120211
    121212        // 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) {
    123214            e.preventDefault();
    124215
     
    161252
    162253            switchBranch(pluginDir, branchName, false)
    163                 .done(function(response) {
     254                .done(function (response) {
    164255                    if (response.success) {
    165256                        showNotification(`Successfully switched to branch: ${response.data.current_branch}`, 'success');
     
    176267                    }
    177268                })
    178                 .fail(function(xhr, status, error) {
     269                .fail(function (xhr, status, error) {
    179270                    showNotification('Network error occurred. Please try again.', 'error');
    180271                    console.error('Branch switch failed:', error);
    181272                })
    182                 .always(function() {
     273                .always(function () {
    183274                    // Remove loader and re-enable item
    184275                    loader.remove();
     
    195286                if (confirm(`You have uncommitted changes. Do you want to discard them and switch to ${branchName}?`)) {
    196287                    switchBranch(pluginDir, branchName, true)
    197                         .done(function(response) {
     288                        .done(function (response) {
    198289                            if (response.success) {
    199290                                showNotification(`Force switched to branch: ${response.data.current_branch}`, 'success');
     
    229320
    230321            // 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');
    232323
    233324            // Add current-branch class to the new current branch
     
    288379
    289380            // Manual close
    290             notification.find('.qa-notification-close').on('click', function() {
     381            notification.find('.qa-notification-close').on('click', function () {
    291382                hideNotification(notification);
    292383            });
     384
     385            return notification;
    293386        }
    294387
     
    311404
    312405        // Global function for pull operations (called via onclick)
    313         window.qaAssistantPull = function(pluginDir) {
    314             // Show immediate feedback
    315             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');
    316409
    317410            // 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 () {
    319412                return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir);
    320413            });
     
    326419
    327420                pullBranch(pluginDir)
    328                     .done(function(response) {
    329                         if (response.success) {
     421                    .done(function (response) {
     422                        if (response && response.success) {
    330423                            showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success');
    331424                            // Reload page to show updated state
    332425                            setTimeout(() => location.reload(), 1500);
    333426                        } 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);
    336430                            } 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');
    338433                            }
    339434                        }
    340435                    })
    341                     .fail(function(xhr, status, error) {
     436                    .fail(function (xhr, status, error) {
    342437                        showNotification('Network error occurred during pull. Please try again.', 'error');
    343438                        console.error('Pull failed:', error);
    344439                    })
    345                     .always(function() {
     440                    .always(function () {
    346441                        // Restore button
    347442                        $button.find('.ab-item').html(originalText);
     
    351446                // Fallback if button not found
    352447                pullBranch(pluginDir)
    353                     .done(function(response) {
    354                         if (response.success) {
     448                    .done(function (response) {
     449                        if (response && response.success) {
    355450                            showNotification(`Successfully pulled changes for branch: ${response.data.branch}`, 'success');
    356451                            setTimeout(() => location.reload(), 1500);
    357452                        } 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                            }
    359460                        }
    360461                    })
    361                     .fail(function(xhr, status, error) {
     462                    .fail(function (xhr, status, error) {
    362463                        showNotification('Network error occurred during pull. Please try again.', 'error');
    363464                        console.error('Pull failed:', error);
     
    394495
    395496        // Global function for refreshing branches (called via onclick)
    396         window.qaAssistantRefresh = function(pluginDir) {
     497        window.qaAssistantRefresh = function (pluginDir) {
    397498            // Show immediate feedback
    398499            showNotification('Fetching latest branches from remote...', 'info');
    399500
    400501            // 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 () {
    402503                return $(this).attr('onclick') && $(this).attr('onclick').includes(pluginDir);
    403504            });
     
    409510
    410511                refreshBranches(pluginDir)
    411                     .done(function(response) {
     512                    .done(function (response) {
    412513                        if (response.success) {
    413514                            let message = response.data.fetch_success
     
    423524                        }
    424525                    })
    425                     .fail(function(xhr, status, error) {
     526                    .fail(function (xhr, status, error) {
    426527                        showNotification('Network error occurred during refresh. Please try again.', 'error');
    427528                        console.error('Refresh failed:', error);
    428529                    })
    429                     .always(function() {
     530                    .always(function () {
    430531                        // Restore button
    431532                        $button.find('.ab-item').html(originalText);
     
    435536                // Fallback if button not found
    436537                refreshBranches(pluginDir)
    437                     .done(function(response) {
     538                    .done(function (response) {
    438539                        if (response.success) {
    439540                            let message = response.data.fetch_success
     
    446547                        }
    447548                    })
    448                     .fail(function(xhr, status, error) {
     549                    .fail(function (xhr, status, error) {
    449550                        showNotification('Network error occurred during refresh. Please try again.', 'error');
    450551                        console.error('Refresh failed:', error);
     
    471572        }
    472573
     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">&times;</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
    473692    });
    474693
     
    478697    function initializeGitValidation() {
    479698        // Add change event listener to plugin selection dropdown
    480         $('.qa-assistant-select2').on('change', function() {
     699        $('.qa-assistant-select2').on('change', function () {
    481700            validateSelectedPlugins();
    482701        });
    483702
    484703        // Add form submission validation
    485         $('.qa-assistant-form').on('submit', function(e) {
     704        $('.qa-assistant-form').on('submit', function (e) {
    486705            if (!validateSelectedPlugins()) {
    487706                e.preventDefault();
     
    499718
    500719        // Check each selected plugin
    501         selectedValues.forEach(function(pluginDir) {
     720        selectedValues.forEach(function (pluginDir) {
    502721            let pluginCard = $(`.qa-plugin-card[data-plugin-dir="${pluginDir}"]`);
    503722            if (pluginCard.length > 0) {
  • qa-assistant/trunk/composer.json

    r3370854 r3469660  
    1212    "minimum-stability": "stable",
    1313    "require": {
     14        "php": ">=8.0",
    1415        "czproject/git-php": "^4.0"
    1516    },
     
    1819            "QaAssistant\\": "includes/"
    1920        },
    20         "files": [ "includes/functions.php" ]
     21        "files": [
     22            "includes/functions.php"
     23        ]
    2124    }
    2225}
  • qa-assistant/trunk/includes/Admin/Menu.php

    r3370854 r3469660  
    1111 * The Menu handler class
    1212 */
    13 class Menu {
     13class Menu
     14{
    1415
    1516    /**
    1617     * Initialize the class
    1718     */
    18     function __construct( ) {
    19         add_action( 'admin_menu', [ $this, 'admin_menu' ] );
     19    function __construct()
     20    {
     21        add_action('admin_menu', [$this, 'admin_menu']);
    2022    }
    2123
     
    2527     * @return void
    2628     */
    27     public function admin_menu() {
     29    public function admin_menu()
     30    {
    2831        $parent_slug = 'qa-assistant';
    29         $capability = apply_filters('qa-assistant/menu/capability', 'manage_options');
     32        $capability = apply_filters('qa_assistant_menu_capability', 'manage_options');
    3033
    3134        // $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');
    3235        // 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']);
    3437
    3538        add_action('admin_head-' . $hook, [$this, 'enqueue_assets']);
     
    4144     * @return void
    4245     */
    43     public function settings_page() {
     46    public function settings_page()
     47    {
    4448        // Check user capabilities
    4549        if (!current_user_can('manage_options')) {
     
    4953        $settings = new Settings();
    5054
    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');
    5761
    5862        $available_plugins = $settings->get_available_plugins();
     
    6468        }
    6569
    66         // Get currently selected plugins for the dropdown
    67         $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();
    6973
    7074        // 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')) {
    7276            // get posted data and sanitize
    7377            $selected_plugins = [];
     
    141145        }
    142146
    143         require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php';
     147        require QA_ASSISTANT_PLUGIN_DIR_PATH . 'templates/settings-page.php';
    144148    }
    145149
     
    149153     * @return void
    150154     */
    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');
    154159    }
    155160}
  • qa-assistant/trunk/includes/Ajax.php

    r3370854 r3469660  
    4242        add_action('wp_ajax_qa_assistant_refresh_branches', [$this, 'refresh_branches']);
    4343
     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
    4448        // Legacy support
    4549        add_action('wp_ajax_qa_assistant_get_branch_data', [$this, 'get_branch_data']);
    4650
     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
    4772        $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        ]);
    48174    }
    49175
     
    84210
    85211        if ($result['success']) {
     212            $this->log_activity('switch', $plugin_dir, $result['current_branch'], 'success', 'Switched to ' . $result['current_branch']);
    86213            wp_send_json_success([
    87214                'message' => $result['message'],
     
    162289
    163290        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
    164296            wp_send_json_success([
    165297                'message' => $result['message'],
    166298                'branch' => $result['branch'],
    167299                'output' => $result['output'] ?? '',
    168                 'plugin_dir' => $plugin_dir
     300                'plugin_dir' => $plugin_dir,
     301                'lastPulled' => time(),
    169302            ]);
    170303        } else {
     
    293426        }
    294427    }
     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    }
    295964}
  • qa-assistant/trunk/includes/Assets.php

    r3370854 r3469660  
    1111 * Assets handler class
    1212 */
    13 class Assets {
     13class Assets
     14{
    1415
    1516    /**
    1617     * Class constructor
    1718     */
    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']);
    2125        // 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']);
    2330    }
    2431
     
    2835     * @return array
    2936     */
    30     public function get_scripts() {
     37    public function get_scripts()
     38    {
    3139        return [
    3240            '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']
    3644            ],
    3745            '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']
    4149            ],
    4250            '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']
    4654            ],
    4755            // 'qa-assistant-bootstrap-script' => [
     
    6876     * @return array
    6977     */
    70     public function get_styles() {
     78    public function get_styles()
     79    {
    7180        return [
    7281            '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')
    7584            ],
    7685            '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')
    7988            ],
    8089            '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')
    8392            ],
    8493            // 'qa-assistant-bootstrap-style' => [
     
    94103     * @return void
    95104     */
    96     public function register_assets() {
     105    public function register_assets()
     106    {
    97107        $scripts = $this->get_scripts();
    98         $styles  = $this->get_styles();
     108        $styles = $this->get_styles();
    99109
    100110        // Load admin assets only in admin area
    101111        if (is_admin()) {
    102             foreach ( $scripts as $handle => $script ) {
     112            foreach ($scripts as $handle => $script) {
    103113                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) {
    110120                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'),
    120130                'ajaxUrl' => admin_url('admin-ajax.php'),
    121             ] );
     131            ]);
    122132        }
    123133
    124134        // Load frontend assets only if needed (adjust condition as necessary)
    125135        if (!is_admin()) {
    126             foreach ( $scripts as $handle => $script ) {
     136            foreach ($scripts as $handle => $script) {
    127137                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) {
    134144                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']);
    137147                }
    138148            }
     
    143153     * Enqueue admin bar dropdown assets on frontend if admin bar is showing
    144154     */
    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()) {
    147158            // 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);
    152163            // 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'),
    157168                '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        ]);
    160267    }
    161268}
  • qa-assistant/trunk/includes/GitManager.php

    r3370854 r3469660  
    164164            $hasChanges = $repo->hasChanges();
    165165            $branches = $this->getBranches($path, true, $force_refresh);
    166            
     166
    167167            $status = [
    168168                'valid' => true,
     
    467467        }
    468468    }
     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    }
    469609}
  • qa-assistant/trunk/qa-assistant.php

    r3370854 r3469660  
    44Plugin URI: https://obayedmamur.com/qa-assistant
    55Description: A comprehensive tool for SQA Engineers with GitHub Desktop-like Git branch switching functionality.
    6 Version: 1.0.3
     6Version: 2.0.0
    77Author: Obayed Mamur
    88Author URI: https://obayedmamur.com
     
    1010*/
    1111
    12 if (! defined('ABSPATH')) {
     12if (!defined('ABSPATH')) {
    1313    exit;
    1414}
    1515
    1616// Define plugin constants
    17 define('QA_ASSISTANT_VERSION', '1.0.3');
     17define('QA_ASSISTANT_VERSION', '2.0.0');
    1818define('QA_ASSISTANT_PLUGIN_FILE', __FILE__);
    1919define('QA_ASSISTANT_PLUGIN_DIR', plugin_dir_path(__FILE__));
     
    2727 * @return string The absolute path to the plugin directory
    2828 */
    29 function qa_assistant_get_plugin_path($plugin_dir) {
     29// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug.
     30function qa_assistant_get_plugin_path($plugin_dir)
     31{
    3032    // Use WordPress function to get plugins directory
    3133    $plugins_dir = dirname(plugin_dir_path(__FILE__));
     
    6870        add_action('plugins_loaded', [$this, 'init_plugin']);
    6971
    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
    7176    }
    7277
     
    8085        static $instance = false;
    8186
    82         if (! $instance) {
     87        if (!$instance) {
    8388            $instance = new self();
    8489        }
     
    121126        }
    122127
     128        // Initialize Admin Bar
     129        if (is_user_logged_in()) {
     130            new QaAssistant\Admin\AdminBar();
     131        }
     132
    123133        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;
    124147    }
    125148
     
    146169        return $this->gitManager->getCurrentBranch($path, $force_refresh);
    147170    }
    148 
    149     public function add_git_branch_to_admin_bar($wp_admin_bar)
    150     {
    151         // List of plugin directories with their aliases and custom colors
    152         $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 caching
    170             $branches = $this->gitManager->getBranches($path, false);
    171 
    172             // Use alias or plugin directory name if alias is not provided
    173             $alias = isset($settings['alias']) ? $settings['alias'] : $plugin_dir;
    174 
    175             // Use custom color or generate a random one if not provided
    176             $color = isset($settings['color']) ? $settings['color'] : '#00fffe';
    177 
    178             // Add node to the admin bar for each plugin directory as a Dropdown Sub Menu Item
    179             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 branch
    194                 $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 branches
    207                 $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 branches
    220                 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 branch
    258                 $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 branches
    271                 $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 branches
    284                 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     }
    317171}
    318172
     
    322176 * @return \Qa_Assistant
    323177 */
     178// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Uses 'qa_assistant' prefix matching the plugin slug.
    324179function qa_assistant()
    325180{
     
    329184// Call the plugin
    330185qa_assistant();
     186
     187// Test uncommitted change for git pull modal
  • qa-assistant/trunk/readme.txt

    r3370854 r3469660  
    33Tags: qa assistant, quality assurance, help, sqa helper tool
    44Requires at least: 5.0
    5 Tested up to: 6.8
    6 Requires PHP: 7.4
    7 Stable tag: 1.0.3
     5Tested up to: 6.9
     6Requires PHP: 8.0
     7Stable tag: 2.0.0
    88License: GPLv3
    99License URI: https://opensource.org/licenses/GPL-3.0
     
    9191== Changelog ==
    9292
     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
    93115= 1.0.3 - Initial Release =
    94116
  • qa-assistant/trunk/templates/settings-page.php

    r3370854 r3469660  
    22// Prevent direct access
    33if (!defined('ABSPATH')) {
    4     exit; // Exit if accessed directly
     4    exit;
    55}
    66?>
    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  
    2424    'CzProject\\GitPhp\\Runners\\OldGitRunner' => $vendorDir . '/czproject/git-php/src/Runners/OldGitRunner.php',
    2525    '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',
    3626);
  • qa-assistant/trunk/vendor/composer/autoload_static.php

    r3370854 r3469660  
    1212
    1313    public static $prefixLengthsPsr4 = array (
    14         'Q' => 
     14        'Q' =>
    1515        array (
    1616            'QaAssistant\\' => 12,
     
    1919
    2020    public static $prefixDirsPsr4 = array (
    21         'QaAssistant\\' => 
     21        'QaAssistant\\' =>
    2222        array (
    2323            0 => __DIR__ . '/../..' . '/includes',
     
    4343        'CzProject\\GitPhp\\Runners\\OldGitRunner' => __DIR__ . '/..' . '/czproject/git-php/src/Runners/OldGitRunner.php',
    4444        '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',
    5545    );
    5646
  • qa-assistant/trunk/vendor/composer/installed.json

    r3370854 r3469660  
    6161        }
    6262    ],
    63     "dev": false,
     63    "dev": true,
    6464    "dev-package-names": []
    6565}
  • qa-assistant/trunk/vendor/composer/installed.php

    r3370854 r3469660  
    44        'pretty_version' => 'dev-main',
    55        'version' => 'dev-main',
    6         'reference' => '86dd52d03562c58827cfda99707efa65f6b31e7a',
     6        'reference' => '4b3e48af653456751624d26a957aa04e19a791b1',
    77        'type' => 'wordpress-plugin',
    88        'install_path' => __DIR__ . '/../../',
    99        'aliases' => array(),
    10         'dev' => false,
     10        'dev' => true,
    1111    ),
    1212    'versions' => array(
     
    2323            'pretty_version' => 'dev-main',
    2424            'version' => 'dev-main',
    25             'reference' => '86dd52d03562c58827cfda99707efa65f6b31e7a',
     25            'reference' => '4b3e48af653456751624d26a957aa04e19a791b1',
    2626            'type' => 'wordpress-plugin',
    2727            'install_path' => __DIR__ . '/../../',
  • qa-assistant/trunk/vendor/composer/platform_check.php

    r3370854 r3469660  
    2020        }
    2121    }
    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)
    2524    );
    2625}
Note: See TracChangeset for help on using the changeset viewer.