Add hierarchical tree view for category selection

Features:
- Tree view mode for categories with expand/collapse
- Product count badges with clickable preview popover
- Select parent with all children button
- Client-side tree filtering (refine search)
- Keyboard shortcuts: Ctrl+A (select all), Ctrl+D (clear)
- View mode switching between tree/list/columns
- Tree view as default for categories, respects user preference

Backend:
- Add previewCategoryProducts and previewCategoryPages AJAX handlers
- Support pagination and filtering in category previews

Styling:
- Consistent count-badge styling across tree and other views
- Loading and popover-open states for count badges

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 15:03:51 +01:00
parent b79a89bbb4
commit 7d79273743
37 changed files with 4620 additions and 1913 deletions

View File

@@ -319,22 +319,13 @@
border-top: 1px dashed $es-border-color;
}
// Legacy exclude-rows (if used elsewhere)
.exclude-rows {
display: flex;
flex-direction: column;
gap: $es-spacing-sm;
}
.exclude-row {
display: flex;
align-items: flex-start;
gap: $es-spacing-sm;
padding: $es-spacing-sm;
background: rgba($es-danger, 0.05);
border: 1px solid rgba($es-danger, 0.2);
border-radius: $es-radius-md;
}
.exclude-row-content {
flex: 1;
}
@@ -534,9 +525,13 @@
}
}
// Group include section
// Group include section - green accent to distinguish from exclude
.group-include {
margin-bottom: $es-spacing-md;
padding: $es-spacing-sm;
background: rgba($es-success, 0.03);
border: 1px solid rgba($es-success, 0.2);
border-radius: $es-radius-md;
}
.section-row {
@@ -565,32 +560,81 @@
cursor: pointer;
}
// Lock indicator for method selector (when excludes are present)
.selector-locked {
.include-method-select {
opacity: 0.7;
cursor: not-allowed;
}
}
.lock-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: $es-warning;
cursor: help;
i {
font-size: 14px;
}
.mpr-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: $es-spacing-xs $es-spacing-sm;
background: $es-slate-800;
color: $es-white;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-normal;
white-space: nowrap;
border-radius: $es-radius-sm;
z-index: 100;
}
&:hover .mpr-tooltip {
display: block;
}
}
// Group excludes section
.group-excludes {
margin-top: $es-spacing-md;
&.has-excludes {
padding-top: $es-spacing-md;
border-top: 1px dashed $es-border-color;
}
}
.except-separator {
display: flex;
align-items: center;
margin-bottom: $es-spacing-sm;
gap: $es-spacing-sm;
margin: 0 0 $es-spacing-sm 0;
// Lines on both sides
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: rgba($es-danger, 0.3);
}
}
.except-label {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
padding: 0.25rem 0.75rem;
background: $es-danger-light;
color: $es-danger;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-semibold;
border-radius: $es-radius-sm;
border-radius: $es-radius-full;
white-space: nowrap;
flex-shrink: 0;
i {
font-size: 10px;
@@ -604,17 +648,36 @@
}
.exclude-row {
display: flex;
flex-direction: column;
padding: $es-spacing-sm;
background: rgba($es-danger, 0.03);
border: 1px solid rgba($es-danger, 0.15);
border-radius: $es-radius-md;
// Value picker inside exclude row - full width
.value-picker {
width: 100%;
margin-top: $es-spacing-sm;
}
}
.exclude-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-sm;
margin-bottom: $es-spacing-sm;
width: 100%;
.method-selector-wrapper {
flex: 1;
}
// Delete button at the far right
.btn-remove-exclude-row {
flex-shrink: 0;
margin-left: auto;
}
}
.btn-remove-exclude-row {
@@ -680,9 +743,14 @@
gap: 0.375rem;
}
// Common height for all modifier controls
$modifier-height: 26px;
.group-modifier-limit {
width: 60px;
padding: 0.25rem 0.5rem;
width: 50px;
max-width: 50px;
height: $modifier-height;
padding: 0 0.375rem;
font-size: $es-font-size-xs;
text-align: center;
border: 1px solid $es-border-color;
@@ -694,8 +762,59 @@
}
}
// Sort modifier - input group style (select + button glued together)
.modifier-sort {
gap: 0; // Remove gap to glue select + button together
.modifier-label {
margin-right: 0.375rem; // Keep space between label and input group
}
.group-modifier-sort {
height: $modifier-height;
padding: 0 0.5rem;
font-size: $es-font-size-xs;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm 0 0 $es-radius-sm;
border-right: none;
cursor: pointer;
&:focus {
border-color: $es-primary;
outline: none;
position: relative;
z-index: 1;
}
}
.btn-sort-dir {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: $modifier-height;
height: $modifier-height;
color: $es-text-muted;
background: $es-slate-100;
border: 1px solid $es-border-color;
border-radius: 0 $es-radius-sm $es-radius-sm 0;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i {
font-size: 11px;
}
}
}
// Fallback for elements outside .modifier-sort context
.group-modifier-sort {
padding: 0.25rem 0.5rem;
height: $modifier-height;
padding: 0 0.5rem;
font-size: $es-font-size-xs;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;
@@ -712,8 +831,8 @@
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
width: $modifier-height;
height: $modifier-height;
color: $es-text-muted;
border: 1px solid $es-border-color;
border-radius: $es-radius-sm;