Changeset 3692384
- Timestamp:
- 09/12/2026 06:50:48 AM (6 weeks ago)
- Location:
- ldap-staff-directory
- Files:
-
- 18 edited
- 1 copied
-
tags/1.4.0 (copied) (copied from ldap-staff-directory/trunk )
-
tags/1.4.0/admin/views/settings-page.php (modified) (1 diff)
-
tags/1.4.0/includes/class-admin.php (modified) (2 diffs)
-
tags/1.4.0/includes/class-ldap-connector.php (modified) (2 diffs)
-
tags/1.4.0/includes/class-shortcode.php (modified) (5 diffs)
-
tags/1.4.0/ldap-staff-directory.php (modified) (2 diffs)
-
tags/1.4.0/public/css/directory.css (modified) (5 diffs)
-
tags/1.4.0/public/js/directory.js (modified) (2 diffs)
-
tags/1.4.0/public/views/directory.php (modified) (13 diffs)
-
tags/1.4.0/readme.txt (modified) (4 diffs)
-
trunk/admin/views/settings-page.php (modified) (1 diff)
-
trunk/includes/class-admin.php (modified) (2 diffs)
-
trunk/includes/class-ldap-connector.php (modified) (2 diffs)
-
trunk/includes/class-shortcode.php (modified) (5 diffs)
-
trunk/ldap-staff-directory.php (modified) (2 diffs)
-
trunk/public/css/directory.css (modified) (5 diffs)
-
trunk/public/js/directory.js (modified) (2 diffs)
-
trunk/public/views/directory.php (modified) (13 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
ldap-staff-directory/tags/1.4.0/admin/views/settings-page.php
r3634465 r3692384 145 145 printf( '</div>' ); 146 146 $this->row( __( 'Department order', 'ldap-staff-directory' ), 'ldap_ed_department_order', array( $this, 'render_field_department_order' ) ); 147 $this->row( __( 'Employee order', 'ldap-staff-directory' ), 'ldap_ed_employee_order', array( $this, 'render_field_employee_order' ) ); 147 148 ?> 148 149 </div> -
ldap-staff-directory/tags/1.4.0/includes/class-admin.php
r3634465 r3692384 147 147 ? $raw_department_order 148 148 : 'alpha'; 149 150 $allowed_employee_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' ); 151 $raw_employee_order = sanitize_text_field( $input['employee_order'] ?? 'name_asc' ); 152 $clean['employee_order'] = in_array( $raw_employee_order, $allowed_employee_orders, true ) 153 ? $raw_employee_order 154 : 'name_asc'; 149 155 150 156 $clean['cache_ttl'] = absint( $input['cache_ttl'] ?? 60 ); … … 588 594 } 589 595 596 public function render_field_employee_order() { 597 $saved = $this->get_option( 'employee_order', 'name_asc' ); 598 $options = array( 599 'name_asc' => __( 'Name (A–Z)', 'ldap-staff-directory' ), 600 'name_desc' => __( 'Name (Z–A)', 'ldap-staff-directory' ), 601 'title_asc' => __( 'Title (A–Z)', 'ldap-staff-directory' ), 602 'title_desc' => __( 'Title (Z–A)', 'ldap-staff-directory' ), 603 ); 604 605 printf( 606 '<select id="ldap_ed_employee_order" name="%1$s[employee_order]">', 607 esc_attr( LDAP_ED_OPTION_KEY ) 608 ); 609 foreach ( $options as $ldap_ed_order_value => $ldap_ed_order_label ) { 610 printf( 611 '<option value="%1$s" %2$s>%3$s</option>', 612 esc_attr( $ldap_ed_order_value ), 613 selected( $saved, $ldap_ed_order_value, false ), 614 esc_html( $ldap_ed_order_label ) 615 ); 616 } 617 printf( 618 '</select><p class="description">%s</p>', 619 esc_html__( 'Order of the employee cards within a department on the public directory. Ties on title are broken by name (A–Z).', 'ldap-staff-directory' ) 620 ); 621 } 622 590 623 public function render_field_enable_search() { 591 624 printf( -
ldap-staff-directory/tags/1.4.0/includes/class-ldap-connector.php
r3633538 r3692384 239 239 * Search the LDAP directory and return an array of user data. 240 240 * 241 * The returned array's order is NOT guaranteed — it reflects whatever order 242 * the LDAP server returned entries in. Callers that need a specific display 243 * order (e.g. the shortcode's employee_order setting) must sort explicitly. 244 * 241 245 * @return array|\WP_Error 242 246 */ … … 277 281 } 278 282 279 // Sort alphabetically by name.280 usort( $users, function ( $a, $b ) {281 return strcmp( $a['name'], $b['name'] );282 } );283 284 283 $this->disconnect(); 285 284 return $users; -
ldap-staff-directory/tags/1.4.0/includes/class-shortcode.php
r3633538 r3692384 12 12 class LDAP_ED_Shortcode { 13 13 14 /** 15 * Deterministic avatar background colors, shared between employee and 16 * department avatars. Color is picked via crc32( $name ) % count( palette ). 17 */ 18 private const AVATAR_PALETTE = array( 19 '#4f7df3', '#7c5cbf', '#0e9b8a', 20 '#2e9e4f', '#c0392b', '#d35400', 21 '#1a7bbf', '#8e44ad', 22 ); 23 24 /** 25 * Inline SVG icon paths (Lucide), keyed by icon name. Static, trusted 26 * markup — never built from user input. 27 */ 28 private const ICONS = array( 29 'briefcase' => '<path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/>', 30 'mail' => '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>', 31 'phone' => '<path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/>', 32 'building' => '<rect width="16" height="20" x="4" y="2" rx="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01"/><path d="M16 6h.01"/><path d="M12 6h.01"/><path d="M12 10h.01"/><path d="M12 14h.01"/><path d="M16 10h.01"/><path d="M16 14h.01"/><path d="M8 10h.01"/><path d="M8 14h.01"/>', 33 'chevron-right' => '<path d="m9 18 6-6-6-6"/>', 34 'arrow-left' => '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>', 35 ); 36 14 37 public function __construct() { 15 38 add_shortcode( 'ldap_directory', array( $this, 'render' ) ); … … 56 79 /** 57 80 * Render the directory HTML. 81 * 82 * Renders one of two views depending on whether a department is selected: 83 * - No department (ldap_dept empty): department menu (department-menu.php). 84 * - Department present: department detail — filtered/paginated employee 85 * grid (directory.php), unchanged from the pre-existing behavior. 58 86 * 59 87 * @param array $atts Shortcode attributes. … … 99 127 } 100 128 101 // Extract department list from the full (unfiltered) set for correct chip counts.129 // Extract department list (with counts) from the full, unfiltered set. 102 130 $ldap_ed_department_order = $settings['department_order'] ?? 'alpha'; 103 131 $ldap_ed_departments = $this->extract_departments( $all_users, $ldap_ed_department_order ); 104 132 $ldap_ed_all_count = count( $all_users ); 105 133 134 // No department selected: render the department menu. No filtering or 135 // pagination runs for this state — it's a flat list of all departments. 136 if ( '' === $ldap_ed_current_dept ) { 137 ob_start(); 138 include LDAP_ED_DIR . 'public/views/department-menu.php'; 139 return ob_get_clean(); 140 } 141 106 142 // Apply department and search filters in PHP. 107 143 $filtered_users = $this->filter_users( $all_users, $ldap_ed_search_query, $ldap_ed_current_dept ); 144 145 // Sort the filtered result before paginating, so order is consistent across pages. 146 $ldap_ed_employee_order = $settings['employee_order'] ?? 'name_asc'; 147 $filtered_users = $this->sort_users( $filtered_users, $ldap_ed_employee_order ); 108 148 109 149 // Paginate the filtered result. … … 121 161 include LDAP_ED_DIR . 'public/views/directory.php'; 122 162 return ob_get_clean(); 163 } 164 165 // ------------------------------------------------------------------------- 166 // Shared display helpers (avatars, icons) — used by both views' templates. 167 168 /** 169 * Computes deterministic initials and an avatar background color for a 170 * name (employee or department). Same color formula for both: crc32( $name ) 171 * modulo the shared 8-color palette. 172 * 173 * @param string $name Employee or department name. 174 * @param bool $force_two_chars When true, a single-word name still yields 175 * 2-letter initials (used for department 176 * abbreviations). Employee avatars keep the 177 * pre-existing 1-letter behavior by default. 178 * @return array{initials:string,color:string} 179 */ 180 public function get_initials_and_color( string $name, bool $force_two_chars = false ): array { 181 $parts = preg_split( '/\s+/', trim( $name ), 2 ); 182 $initials = strtoupper( substr( $parts[0] ?? '', 0, 1 ) ); 183 184 if ( ! empty( $parts[1] ) ) { 185 $initials .= strtoupper( substr( $parts[1], 0, 1 ) ); 186 } elseif ( $force_two_chars ) { 187 $initials .= strtoupper( substr( $parts[0] ?? '', 1, 1 ) ); 188 } 189 190 $color = self::AVATAR_PALETTE[ abs( crc32( $name ) ) % count( self::AVATAR_PALETTE ) ]; 191 192 return array( 193 'initials' => $initials, 194 'color' => $color, 195 ); 196 } 197 198 /** 199 * Returns inline SVG markup for a static, trusted icon (Lucide-style). 200 * Never built from user input — safe to echo without escaping. 201 * 202 * @param string $name Icon key, see self::ICONS. 203 * @return string SVG markup, or '' when the icon name is unknown. 204 */ 205 public function get_icon_svg( string $name ): string { 206 if ( ! isset( self::ICONS[ $name ] ) ) { 207 return ''; 208 } 209 210 return sprintf( 211 '<svg class="ldap-icon ldap-icon-%1$s" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">%2$s</svg>', 212 esc_attr( $name ), 213 self::ICONS[ $name ] 214 ); 123 215 } 124 216 … … 207 299 208 300 /** 301 * Sort a user array for display, per the employee_order setting. 302 * 303 * Ties on title (including empty title) are always broken by name ascending, 304 * regardless of the primary order's direction — this is intentional, not a bug. 305 * Falls back to 'name_asc' for any value outside the known whitelist. 306 * 307 * @param array $users Filtered user array. 308 * @param string $order 'name_asc' (default), 'name_desc', 'title_asc', or 'title_desc'. 309 * @return array Sorted array (re-indexed). 310 */ 311 private function sort_users( array $users, string $order = 'name_asc' ): array { 312 $allowed_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' ); 313 if ( ! in_array( $order, $allowed_orders, true ) ) { 314 $order = 'name_asc'; 315 } 316 317 usort( 318 $users, 319 static function ( $a, $b ) use ( $order ) { 320 if ( 'name_desc' === $order ) { 321 return strcmp( $b['name'] ?? '', $a['name'] ?? '' ); 322 } 323 324 if ( 'title_asc' === $order || 'title_desc' === $order ) { 325 $result = strcmp( $a['title'] ?? '', $b['title'] ?? '' ); 326 if ( 'title_desc' === $order ) { 327 $result = -$result; 328 } 329 // Tie on title: always break by name ascending, regardless of direction. 330 return 0 !== $result ? $result : strcmp( $a['name'] ?? '', $b['name'] ?? '' ); 331 } 332 333 // name_asc (default). 334 return strcmp( $a['name'] ?? '', $b['name'] ?? '' ); 335 } 336 ); 337 338 return array_values( $users ); 339 } 340 341 /** 209 342 * Slice a filtered user array for the requested page. 210 343 * -
ldap-staff-directory/tags/1.4.0/ldap-staff-directory.php
r3634465 r3692384 4 4 * Plugin URI: https://wordpress.org/plugins/ldap-staff-directory/ 5 5 * Description: Connects to LDAP or LDAPS to display an employee directory from an OU. Supports Elementor, Beaver Builder and a native shortcode. 6 * Version: 1. 2.16 * Version: 1.4.0 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 19 19 20 20 // Plugin constants. 21 define( 'LDAP_ED_VERSION', '1. 2.1' );21 define( 'LDAP_ED_VERSION', '1.4.0' ); 22 22 define( 'LDAP_ED_FILE', __FILE__ ); 23 23 define( 'LDAP_ED_DIR', plugin_dir_path( __FILE__ ) ); -
ldap-staff-directory/tags/1.4.0/public/css/directory.css
r3606783 r3692384 25 25 --ldap-dept-badge-bg: rgba( 0, 115, 170, .08 ); 26 26 --ldap-dept-badge-color: #005a87; 27 28 /* Inline field icons */ 29 --ldap-icon-color: #8c8f94; 27 30 } 28 31 … … 33 36 font-size: var( --ldap-font-size ); 34 37 color: var( --ldap-text-color ); 35 }36 37 /* ── Department filter bar ──────────────────────────────────────────────── */38 39 .ldap-dept-filters {40 display: flex;41 flex-wrap: nowrap;42 gap: 8px;43 overflow-x: auto;44 white-space: nowrap;45 padding-bottom: 4px;46 margin-bottom: 24px;47 /* Hide scrollbar visually while keeping it functional */48 -ms-overflow-style: none;49 scrollbar-width: none;50 }51 52 .ldap-dept-filters::-webkit-scrollbar {53 display: none;54 }55 56 .ldap-dept-chip {57 display: inline-flex;58 align-items: center;59 gap: 5px;60 padding: 5px 14px;61 font-size: .82em;62 font-family: inherit;63 font-weight: 500;64 color: var( --ldap-text-color );65 background: transparent;66 border: 1px solid var( --ldap-card-border );67 border-radius: 20px;68 text-decoration: none;69 cursor: pointer;70 white-space: nowrap;71 transition: border-color .15s ease, background-color .15s ease, color .15s ease;72 flex-shrink: 0;73 }74 75 a.ldap-dept-chip:hover {76 border-color: var( --ldap-primary-color );77 color: var( --ldap-primary-color );78 }79 80 .ldap-dept-chip.is-active {81 background: var( --ldap-primary-color );82 border-color: var( --ldap-primary-color );83 color: #fff;84 cursor: default;85 }86 87 .ldap-dept-chip-clear {88 display: inline-flex;89 align-items: center;90 justify-content: center;91 margin-left: 2px;92 font-size: 1.1em;93 font-weight: 700;94 color: inherit;95 text-decoration: none;96 opacity: .75;97 cursor: pointer;98 transition: opacity .15s ease;99 }100 101 .ldap-dept-chip-clear:hover {102 opacity: 1;103 38 } 104 39 … … 236 171 } 237 172 173 .ldap-title, 174 .ldap-email, 175 .ldap-phone, 176 .ldap-extension { 177 display: flex; 178 align-items: center; 179 gap: 6px; 180 } 181 238 182 .ldap-email { 239 183 margin-top: 10px; … … 265 209 266 210 .ldap-extension { 267 display: block;268 211 margin-top: 4px; 269 212 font-size: .88em; 270 213 color: var( --ldap-muted-color ); 214 } 215 216 /* ── Inline field icons (employee card, breadcrumb, department rows) ───── */ 217 218 .ldap-icon { 219 width: 12px; 220 height: 12px; 221 flex-shrink: 0; 222 color: var( --ldap-icon-color ); 271 223 } 272 224 … … 341 293 font-variant-numeric: tabular-nums; 342 294 } 295 296 /* ── Department menu (root view) ───────────────────────────────────────── */ 297 298 .ldap-menu-header { 299 margin-bottom: 20px; 300 } 301 302 .ldap-menu-title { 303 margin: 0 0 4px; 304 font-size: 1.4em; 305 font-weight: 700; 306 color: var( --ldap-text-color ); 307 } 308 309 .ldap-menu-sub { 310 margin: 0; 311 font-size: .85em; 312 color: var( --ldap-muted-color ); 313 } 314 315 .ldap-dept-search-wrap { 316 max-width: 320px; 317 } 318 319 .ldap-dept-search { 320 max-width: none; 321 } 322 323 .ldap-menu-eyebrow { 324 margin: 0 0 12px; 325 font-size: .72em; 326 font-weight: 700; 327 letter-spacing: .08em; 328 text-transform: uppercase; 329 color: var( --ldap-muted-color ); 330 } 331 332 /* Two columns on desktop (matches the prototype); pure CSS, no JS balancing. */ 333 .ldap-dept-menu-list { 334 columns: 2; 335 column-gap: 10px; 336 } 337 338 .ldap-dept-row { 339 display: flex; 340 align-items: center; 341 gap: 10px; 342 break-inside: avoid; 343 margin-bottom: 8px; 344 padding: 10px 14px; 345 background: var( --ldap-card-bg ); 346 border: 1px solid var( --ldap-card-border ); 347 border-radius: var( --ldap-card-radius ); 348 color: inherit; 349 text-decoration: none; 350 transition: box-shadow .15s ease, border-color .15s ease; 351 } 352 353 .ldap-dept-row:hover { 354 box-shadow: 0 4px 14px rgba( 0, 0, 0, .1 ); 355 border-color: #c3c4c7; 356 } 357 358 .ldap-dept-row[hidden] { 359 display: none; 360 } 361 362 .ldap-dept-row-avatar { 363 flex-shrink: 0; 364 width: 36px; 365 height: 36px; 366 border-radius: 50%; 367 background-color: var( --ldap-avatar-bg ); 368 color: #fff; 369 display: flex; 370 align-items: center; 371 justify-content: center; 372 font-size: 12px; 373 font-weight: 700; 374 user-select: none; 375 } 376 377 .ldap-dept-row-info { 378 flex: 1; 379 min-width: 0; 380 display: flex; 381 flex-direction: column; 382 gap: 2px; 383 } 384 385 .ldap-dept-row-name { 386 font-size: .93em; 387 font-weight: 600; 388 color: var( --ldap-text-color ); 389 } 390 391 .ldap-dept-row-count { 392 font-size: .8em; 393 color: var( --ldap-muted-color ); 394 } 395 396 .ldap-dept-row .ldap-icon { 397 width: 14px; 398 height: 14px; 399 color: var( --ldap-search-border ); 400 } 401 402 @media ( max-width: 540px ) { 403 .ldap-dept-menu-list { 404 columns: 1; 405 } 406 } 407 408 /* ── Breadcrumb + department header (detail view) ──────────────────────── */ 409 410 .ldap-breadcrumb { 411 display: inline-flex; 412 align-items: center; 413 gap: 6px; 414 margin-bottom: 16px; 415 font-size: .93em; 416 font-weight: 500; 417 color: var( --ldap-primary-color ); 418 text-decoration: none; 419 } 420 421 .ldap-breadcrumb:hover { 422 text-decoration: underline; 423 } 424 425 .ldap-breadcrumb .ldap-icon { 426 width: 13px; 427 height: 13px; 428 color: var( --ldap-primary-color ); 429 } 430 431 .ldap-dept-header { 432 display: flex; 433 align-items: center; 434 justify-content: space-between; 435 gap: 16px; 436 margin-bottom: 20px; 437 } 438 439 .ldap-dept-header-name { 440 margin: 0 0 4px; 441 font-size: 1.25em; 442 font-weight: 700; 443 color: var( --ldap-text-color ); 444 } 445 446 .ldap-dept-header-sub { 447 margin: 0; 448 font-size: .82em; 449 color: var( --ldap-muted-color ); 450 } 451 452 .ldap-dept-header-badge { 453 flex-shrink: 0; 454 width: 44px; 455 height: 44px; 456 border-radius: 50%; 457 background-color: var( --ldap-avatar-bg ); 458 color: #fff; 459 display: flex; 460 align-items: center; 461 justify-content: center; 462 font-size: 14px; 463 font-weight: 700; 464 user-select: none; 465 } 466 467 @media ( max-width: 540px ) { 468 .ldap-dept-header { 469 flex-direction: column; 470 align-items: flex-start; 471 } 472 473 .ldap-dept-header-badge { 474 width: 36px; 475 height: 36px; 476 font-size: 12px; 477 } 478 } -
ldap-staff-directory/tags/1.4.0/public/js/directory.js
r3548101 r3692384 1 1 /** 2 2 * LDAP Staff Directory — public JS 3 * Filtering and pagination are now server-side. This file strips empty 4 * ldap_search from the form submission to keep URLs clean. 3 * 4 * Two independent, small pieces of behavior: 5 * 1. Employee search (department detail view) is server-side — this only 6 * strips an empty ldap_search from the form submission to keep URLs clean. 7 * 2. Department search (department menu view) is client-side — it filters 8 * the already-rendered .ldap-dept-row elements by substring match, with 9 * no network request. Scoped to at most the total number of departments; 10 * it never touches employee cards or pagination. 5 11 */ 6 12 ( function () { … … 16 22 } ); 17 23 } ); 24 25 var deptSearchInput = document.getElementById( 'ldap-dept-search-input' ); 26 if ( ! deptSearchInput ) { 27 return; 28 } 29 30 var wrap = deptSearchInput.closest( '.ldap-directory-wrap' ); 31 if ( ! wrap ) { 32 return; 33 } 34 35 var deptRows = Array.prototype.slice.call( wrap.querySelectorAll( '.ldap-dept-row' ) ); 36 var deptNoResults = wrap.querySelector( '.ldap-dept-no-results' ); 37 38 deptSearchInput.addEventListener( 'input', function () { 39 var query = deptSearchInput.value.trim().toLowerCase(); 40 var visible = 0; 41 42 deptRows.forEach( function ( row ) { 43 var matches = '' === query || row.dataset.name.toLowerCase().indexOf( query ) !== -1; 44 row.hidden = ! matches; 45 if ( matches ) { 46 visible++; 47 } 48 } ); 49 50 if ( deptNoResults ) { 51 deptNoResults.hidden = visible > 0; 52 } 53 } ); 18 54 } ); 19 55 } () ); -
ldap-staff-directory/tags/1.4.0/public/views/directory.php
r3606783 r3692384 1 1 <?php 2 2 /** 3 * Public directory template. 3 * Department detail template — rendered when a department is selected 4 * (ldap_dept present). Shows a breadcrumb back to the department menu, the 5 * active department's header, and the existing filtered/paginated employee 6 * grid. 4 7 * 5 8 * Variables available from class-shortcode.php: … … 7 10 * $ldap_ed_fields — array of field keys to display 8 11 * $ldap_ed_per_page — int, items per page 9 * $ldap_ed_enable_search — bool, show search input and filter bar 10 * $ldap_ed_departments — array [ 'Department' => count ] sorted alphabetically 11 * $ldap_ed_current_dept — string, active department filter ('' = none) 12 * $ldap_ed_enable_search — bool, show the employee search input 13 * $ldap_ed_current_dept — string, active department (always non-empty here) 12 14 * $ldap_ed_search_query — string, active search term 13 15 * $ldap_ed_current_page — int, current page number 14 16 * $ldap_ed_total_pages — int, total page count 15 * $ldap_ed_total_count — int, total filtered employee count 16 * $ldap_ed_all_count — int, total unfiltered employee count 17 * $ldap_ed_total_count — int, total filtered employee count (this department) 17 18 * $ldap_ed_prev_url — string|null, URL for previous page (null = disabled) 18 19 * $ldap_ed_next_url — string|null, URL for next page (null = disabled) … … 25 26 } 26 27 27 // Palette for avatar backgrounds — one color is deterministically assigned per employee. 28 $ldap_avatar_palette = array( 29 '#4f7df3', '#7c5cbf', '#0e9b8a', 30 '#2e9e4f', '#c0392b', '#d35400', 31 '#1a7bbf', '#8e44ad', 32 ); 28 $ldap_ed_dept_avatar = $this->get_initials_and_color( $ldap_ed_current_dept, true ); 33 29 ?> 34 30 <div class="ldap-directory-wrap"> 31 32 <a 33 href="<?php echo esc_url( remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ); ?>" 34 class="ldap-breadcrumb" 35 > 36 <?php echo $this->get_icon_svg( 'arrow-left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 37 <?php esc_html_e( 'Departments', 'ldap-staff-directory' ); ?> 38 </a> 39 40 <div class="ldap-dept-header"> 41 <div class="ldap-dept-header-info"> 42 <h2 class="ldap-dept-header-name"><?php echo esc_html( $ldap_ed_current_dept ); ?></h2> 43 <p class="ldap-dept-header-sub"> 44 <?php 45 echo esc_html( 46 sprintf( 47 /* translators: %s: number of employees in this department */ 48 _n( '%s employee in this department', '%s employees in this department', $ldap_ed_total_count, 'ldap-staff-directory' ), 49 absint( $ldap_ed_total_count ) 50 ) 51 ); 52 ?> 53 </p> 54 </div> 55 <span 56 class="ldap-dept-header-badge" 57 aria-hidden="true" 58 style="--ldap-avatar-bg:<?php echo esc_attr( $ldap_ed_dept_avatar['color'] ); ?>" 59 ><?php echo esc_html( $ldap_ed_dept_avatar['initials'] ); ?></span> 60 </div> 35 61 36 62 <?php if ( $ldap_ed_enable_search ) : ?> … … 45 71 <?php esc_html_e( 'Search employees', 'ldap-staff-directory' ); ?> 46 72 </label> 47 <?php if ( '' !== $ldap_ed_current_dept ) : ?>48 73 <input type="hidden" name="ldap_dept" value="<?php echo esc_attr( $ldap_ed_current_dept ); ?>"> 49 <?php endif; ?>50 74 <input 51 75 type="search" … … 53 77 name="ldap_search" 54 78 class="ldap-search" 55 placeholder="<?php esc_attr_e( 'Search employee…', 'ldap-staff-directory' ); ?>" 79 placeholder="<?php 80 echo esc_attr( 81 sprintf( 82 /* translators: %s: department name */ 83 __( 'Search in %s…', 'ldap-staff-directory' ), 84 $ldap_ed_current_dept 85 ) 86 ); 87 ?>" 56 88 aria-label="<?php esc_attr_e( 'Search employee', 'ldap-staff-directory' ); ?>" 57 89 value="<?php echo esc_attr( $ldap_ed_search_query ); ?>" … … 59 91 </form> 60 92 </div> 61 62 <?php if ( count( $ldap_ed_departments ) >= 2 ) : ?>63 <div class="ldap-dept-filters" role="list" aria-label="<?php esc_attr_e( 'Filter by department', 'ldap-staff-directory' ); ?>">64 65 <?php // "All" chip — active when no department filter is set. ?>66 <?php $ldap_ed_all_url = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) ); ?>67 <?php if ( '' === $ldap_ed_current_dept ) : ?>68 <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">69 <?php70 echo esc_html(71 sprintf(72 /* translators: %s: total employee count */73 __( 'All (%s)', 'ldap-staff-directory' ),74 absint( $ldap_ed_all_count )75 )76 );77 ?>78 </span>79 <?php else : ?>80 <a81 href="<?php echo $ldap_ed_all_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"82 class="ldap-dept-chip"83 role="listitem"84 aria-pressed="false"85 >86 <?php87 echo esc_html(88 sprintf(89 /* translators: %s: total employee count */90 __( 'All (%s)', 'ldap-staff-directory' ),91 absint( $ldap_ed_all_count )92 )93 );94 ?>95 </a>96 <?php endif; ?>97 98 <?php foreach ( $ldap_ed_departments as $ldap_ed_dept_name => $ldap_ed_dept_count ) : ?>99 <?php100 $ldap_ed_is_active = ( 0 === strcasecmp( $ldap_ed_dept_name, $ldap_ed_current_dept ) );101 $ldap_ed_chip_url = esc_url( add_query_arg( array( 'ldap_dept' => $ldap_ed_dept_name, 'ldap_page' => 1 ) ) );102 $ldap_ed_clear_url = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) );103 $ldap_ed_chip_label = esc_html( $ldap_ed_dept_name ) . ' (' . absint( $ldap_ed_dept_count ) . ')';104 ?>105 <?php if ( $ldap_ed_is_active ) : ?>106 <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">107 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>108 <a109 href="<?php echo $ldap_ed_clear_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"110 class="ldap-dept-chip-clear"111 aria-label="<?php esc_attr_e( 'Clear department filter', 'ldap-staff-directory' ); ?>"112 >×</a>113 </span>114 <?php else : ?>115 <a116 href="<?php echo $ldap_ed_chip_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"117 class="ldap-dept-chip"118 role="listitem"119 aria-pressed="false"120 >121 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>122 </a>123 <?php endif; ?>124 <?php endforeach; ?>125 126 </div>127 <?php endif; ?>128 93 <?php endif; ?> 129 94 … … 134 99 <?php else : ?> 135 100 <?php foreach ( $ldap_ed_users as $ldap_ed_user ) : 136 // Compute initials (up to 2 characters) from the display name. 137 $ldap_name = $ldap_ed_user['name'] ?? ''; 138 $ldap_parts = preg_split( '/\s+/', trim( $ldap_name ), 2 ); 139 $ldap_initials = strtoupper( substr( $ldap_parts[0] ?? '', 0, 1 ) ); 140 if ( ! empty( $ldap_parts[1] ) ) { 141 $ldap_initials .= strtoupper( substr( $ldap_parts[1], 0, 1 ) ); 142 } 101 $ldap_ed_name = $ldap_ed_user['name'] ?? ''; 102 $ldap_ed_avatar = $this->get_initials_and_color( $ldap_ed_name ); 103 $ldap_initials = $ldap_ed_avatar['initials']; 104 143 105 // Fall back to first letter of email when name is absent. 144 106 if ( '' === $ldap_initials && ! empty( $ldap_ed_user['email'] ) ) { … … 146 108 } 147 109 148 // Deterministic background color derived from the name. 149 $ldap_color_idx = abs( crc32( $ldap_name ) ) % count( $ldap_avatar_palette ); 150 $ldap_avatar_bg = $ldap_avatar_palette[ $ldap_color_idx ]; 110 $ldap_avatar_bg = $ldap_ed_avatar['color']; 151 111 ?> 152 112 <article class="ldap-staff-card"> … … 162 122 163 123 <?php if ( in_array( 'title', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['title'] ) ) : ?> 164 <p class="ldap-title"><?php echo esc_html( $ldap_ed_user['title'] ); ?></p> 124 <p class="ldap-title"> 125 <?php echo $this->get_icon_svg( 'briefcase' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 126 <?php echo esc_html( $ldap_ed_user['title'] ); ?> 127 </p> 165 128 <?php endif; ?> 166 129 … … 173 136 <?php if ( in_array( 'email', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['email'] ) ) : ?> 174 137 <a class="ldap-email" href="mailto:<?php echo esc_attr( $ldap_ed_user['email'] ); ?>"> 138 <?php echo $this->get_icon_svg( 'mail' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 175 139 <?php echo esc_html( $ldap_ed_user['email'] ); ?> 176 140 </a> … … 179 143 <?php if ( in_array( 'phone', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['phone'] ) ) : ?> 180 144 <a class="ldap-phone" href="tel:<?php echo esc_attr( $ldap_ed_user['phone'] ); ?>"> 145 <?php echo $this->get_icon_svg( 'phone' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 181 146 <?php echo esc_html( $ldap_ed_user['phone'] ); ?> 182 147 </a> … … 184 149 185 150 <?php if ( in_array( 'extension', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['extension'] ) ) : ?> 186 <span class="ldap-extension"><?php echo esc_html( $ldap_ed_user['extension'] ); ?></span> 151 <span class="ldap-extension"> 152 <?php echo $this->get_icon_svg( 'building' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 153 <?php echo esc_html( $ldap_ed_user['extension'] ); ?> 154 </span> 187 155 <?php endif; ?> 188 156 </article> … … 209 177 $ldap_ed_from = absint( ( $ldap_ed_current_page - 1 ) * $ldap_ed_per_page + 1 ); 210 178 $ldap_ed_to = absint( min( $ldap_ed_current_page * $ldap_ed_per_page, $ldap_ed_total_count ) ); 211 if ( '' !== $ldap_ed_current_dept ) : 212 echo esc_html( 213 sprintf( 214 /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */ 215 __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ), 216 absint( $ldap_ed_from ), 217 absint( $ldap_ed_to ), 218 absint( $ldap_ed_total_count ), 219 $ldap_ed_current_dept 220 ) 221 ); 222 else : 223 echo esc_html( 224 sprintf( 225 /* translators: 1: first record number on page, 2: last record number on page, 3: total records */ 226 __( 'Showing %1$s–%2$s of %3$s', 'ldap-staff-directory' ), 227 absint( $ldap_ed_from ), 228 absint( $ldap_ed_to ), 229 absint( $ldap_ed_total_count ) 230 ) 231 ); 232 endif; 179 echo esc_html( 180 sprintf( 181 /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */ 182 __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ), 183 absint( $ldap_ed_from ), 184 absint( $ldap_ed_to ), 185 absint( $ldap_ed_total_count ), 186 $ldap_ed_current_dept 187 ) 188 ); 233 189 endif; ?> 234 190 </span> -
ldap-staff-directory/tags/1.4.0/readme.txt
r3634465 r3692384 3 3 Tags: ldap, directory, wpbeaverbuilder, staff, elementor 4 4 Requires at least: 5.8 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.17 Stable tag: 1.4.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 23 23 * Native Elementor widget with full style controls 24 24 * Native Beaver Builder module with General and Style tabs 25 * Department filter bar with configurable chip order (alphabetical or by contact count) 25 * Public directory browses department-first: a menu of departments (with employee counts) leads to each department's contact list, in configurable order (alphabetical or by contact count) 26 * Employee cards within a department can be ordered by name or job title, ascending or descending, from the admin — no code changes needed 26 27 * Exclude specific departments — or employees with no department assigned — from the public directory, enforced at the LDAP query level so those employees are never fetched 27 28 * Exclude disabled Active Directory accounts … … 77 78 By default 60 minutes. Change the TTL under **Settings → LDAP Directory → Fields → Advanced settings**, or flush immediately with the **Clear Cache** button on that same tab. 78 79 80 = Why do I now see a list of departments instead of all employees? = 81 82 Starting in 1.3.0, the public directory browses department-first: visitors see a menu of departments with employee counts, and click into one to see its contacts. This replaces the previous single-page grid with a department filter bar. There is no setting to restore the old flat grid. 83 79 84 == Screenshots == 80 85 … … 85 90 86 91 == Changelog == 92 93 = 1.4.0 = 94 * Feature: New **Employee order** setting under **Settings → LDAP Directory → Fields** lets you choose how employee cards are ordered within a department: by name or job title, ascending or descending. Ties on job title (including employees with no title) are always broken by name (A–Z). 95 96 = 1.3.0 = 97 * Feature: The public directory now browses department-first. The root view shows a menu of departments (name, employee count, color-coded avatar) instead of the full employee grid; clicking a department opens its filtered, paginated contact list with a breadcrumb back to the menu. 98 * Feature: Employee cards in the department view show a small icon next to job title, email, phone, and extension for easier scanning. 99 * Breaking change: The department filter chip bar has been removed and replaced by the department menu described above. There is no setting to restore the previous single-page grid. 100 * Chore: "Tested up to" updated to WordPress 7.1. 87 101 88 102 = 1.2.1 = -
ldap-staff-directory/trunk/admin/views/settings-page.php
r3634465 r3692384 145 145 printf( '</div>' ); 146 146 $this->row( __( 'Department order', 'ldap-staff-directory' ), 'ldap_ed_department_order', array( $this, 'render_field_department_order' ) ); 147 $this->row( __( 'Employee order', 'ldap-staff-directory' ), 'ldap_ed_employee_order', array( $this, 'render_field_employee_order' ) ); 147 148 ?> 148 149 </div> -
ldap-staff-directory/trunk/includes/class-admin.php
r3634465 r3692384 147 147 ? $raw_department_order 148 148 : 'alpha'; 149 150 $allowed_employee_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' ); 151 $raw_employee_order = sanitize_text_field( $input['employee_order'] ?? 'name_asc' ); 152 $clean['employee_order'] = in_array( $raw_employee_order, $allowed_employee_orders, true ) 153 ? $raw_employee_order 154 : 'name_asc'; 149 155 150 156 $clean['cache_ttl'] = absint( $input['cache_ttl'] ?? 60 ); … … 588 594 } 589 595 596 public function render_field_employee_order() { 597 $saved = $this->get_option( 'employee_order', 'name_asc' ); 598 $options = array( 599 'name_asc' => __( 'Name (A–Z)', 'ldap-staff-directory' ), 600 'name_desc' => __( 'Name (Z–A)', 'ldap-staff-directory' ), 601 'title_asc' => __( 'Title (A–Z)', 'ldap-staff-directory' ), 602 'title_desc' => __( 'Title (Z–A)', 'ldap-staff-directory' ), 603 ); 604 605 printf( 606 '<select id="ldap_ed_employee_order" name="%1$s[employee_order]">', 607 esc_attr( LDAP_ED_OPTION_KEY ) 608 ); 609 foreach ( $options as $ldap_ed_order_value => $ldap_ed_order_label ) { 610 printf( 611 '<option value="%1$s" %2$s>%3$s</option>', 612 esc_attr( $ldap_ed_order_value ), 613 selected( $saved, $ldap_ed_order_value, false ), 614 esc_html( $ldap_ed_order_label ) 615 ); 616 } 617 printf( 618 '</select><p class="description">%s</p>', 619 esc_html__( 'Order of the employee cards within a department on the public directory. Ties on title are broken by name (A–Z).', 'ldap-staff-directory' ) 620 ); 621 } 622 590 623 public function render_field_enable_search() { 591 624 printf( -
ldap-staff-directory/trunk/includes/class-ldap-connector.php
r3633538 r3692384 239 239 * Search the LDAP directory and return an array of user data. 240 240 * 241 * The returned array's order is NOT guaranteed — it reflects whatever order 242 * the LDAP server returned entries in. Callers that need a specific display 243 * order (e.g. the shortcode's employee_order setting) must sort explicitly. 244 * 241 245 * @return array|\WP_Error 242 246 */ … … 277 281 } 278 282 279 // Sort alphabetically by name.280 usort( $users, function ( $a, $b ) {281 return strcmp( $a['name'], $b['name'] );282 } );283 284 283 $this->disconnect(); 285 284 return $users; -
ldap-staff-directory/trunk/includes/class-shortcode.php
r3633538 r3692384 12 12 class LDAP_ED_Shortcode { 13 13 14 /** 15 * Deterministic avatar background colors, shared between employee and 16 * department avatars. Color is picked via crc32( $name ) % count( palette ). 17 */ 18 private const AVATAR_PALETTE = array( 19 '#4f7df3', '#7c5cbf', '#0e9b8a', 20 '#2e9e4f', '#c0392b', '#d35400', 21 '#1a7bbf', '#8e44ad', 22 ); 23 24 /** 25 * Inline SVG icon paths (Lucide), keyed by icon name. Static, trusted 26 * markup — never built from user input. 27 */ 28 private const ICONS = array( 29 'briefcase' => '<path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/>', 30 'mail' => '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>', 31 'phone' => '<path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/>', 32 'building' => '<rect width="16" height="20" x="4" y="2" rx="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01"/><path d="M16 6h.01"/><path d="M12 6h.01"/><path d="M12 10h.01"/><path d="M12 14h.01"/><path d="M16 10h.01"/><path d="M16 14h.01"/><path d="M8 10h.01"/><path d="M8 14h.01"/>', 33 'chevron-right' => '<path d="m9 18 6-6-6-6"/>', 34 'arrow-left' => '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>', 35 ); 36 14 37 public function __construct() { 15 38 add_shortcode( 'ldap_directory', array( $this, 'render' ) ); … … 56 79 /** 57 80 * Render the directory HTML. 81 * 82 * Renders one of two views depending on whether a department is selected: 83 * - No department (ldap_dept empty): department menu (department-menu.php). 84 * - Department present: department detail — filtered/paginated employee 85 * grid (directory.php), unchanged from the pre-existing behavior. 58 86 * 59 87 * @param array $atts Shortcode attributes. … … 99 127 } 100 128 101 // Extract department list from the full (unfiltered) set for correct chip counts.129 // Extract department list (with counts) from the full, unfiltered set. 102 130 $ldap_ed_department_order = $settings['department_order'] ?? 'alpha'; 103 131 $ldap_ed_departments = $this->extract_departments( $all_users, $ldap_ed_department_order ); 104 132 $ldap_ed_all_count = count( $all_users ); 105 133 134 // No department selected: render the department menu. No filtering or 135 // pagination runs for this state — it's a flat list of all departments. 136 if ( '' === $ldap_ed_current_dept ) { 137 ob_start(); 138 include LDAP_ED_DIR . 'public/views/department-menu.php'; 139 return ob_get_clean(); 140 } 141 106 142 // Apply department and search filters in PHP. 107 143 $filtered_users = $this->filter_users( $all_users, $ldap_ed_search_query, $ldap_ed_current_dept ); 144 145 // Sort the filtered result before paginating, so order is consistent across pages. 146 $ldap_ed_employee_order = $settings['employee_order'] ?? 'name_asc'; 147 $filtered_users = $this->sort_users( $filtered_users, $ldap_ed_employee_order ); 108 148 109 149 // Paginate the filtered result. … … 121 161 include LDAP_ED_DIR . 'public/views/directory.php'; 122 162 return ob_get_clean(); 163 } 164 165 // ------------------------------------------------------------------------- 166 // Shared display helpers (avatars, icons) — used by both views' templates. 167 168 /** 169 * Computes deterministic initials and an avatar background color for a 170 * name (employee or department). Same color formula for both: crc32( $name ) 171 * modulo the shared 8-color palette. 172 * 173 * @param string $name Employee or department name. 174 * @param bool $force_two_chars When true, a single-word name still yields 175 * 2-letter initials (used for department 176 * abbreviations). Employee avatars keep the 177 * pre-existing 1-letter behavior by default. 178 * @return array{initials:string,color:string} 179 */ 180 public function get_initials_and_color( string $name, bool $force_two_chars = false ): array { 181 $parts = preg_split( '/\s+/', trim( $name ), 2 ); 182 $initials = strtoupper( substr( $parts[0] ?? '', 0, 1 ) ); 183 184 if ( ! empty( $parts[1] ) ) { 185 $initials .= strtoupper( substr( $parts[1], 0, 1 ) ); 186 } elseif ( $force_two_chars ) { 187 $initials .= strtoupper( substr( $parts[0] ?? '', 1, 1 ) ); 188 } 189 190 $color = self::AVATAR_PALETTE[ abs( crc32( $name ) ) % count( self::AVATAR_PALETTE ) ]; 191 192 return array( 193 'initials' => $initials, 194 'color' => $color, 195 ); 196 } 197 198 /** 199 * Returns inline SVG markup for a static, trusted icon (Lucide-style). 200 * Never built from user input — safe to echo without escaping. 201 * 202 * @param string $name Icon key, see self::ICONS. 203 * @return string SVG markup, or '' when the icon name is unknown. 204 */ 205 public function get_icon_svg( string $name ): string { 206 if ( ! isset( self::ICONS[ $name ] ) ) { 207 return ''; 208 } 209 210 return sprintf( 211 '<svg class="ldap-icon ldap-icon-%1$s" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">%2$s</svg>', 212 esc_attr( $name ), 213 self::ICONS[ $name ] 214 ); 123 215 } 124 216 … … 207 299 208 300 /** 301 * Sort a user array for display, per the employee_order setting. 302 * 303 * Ties on title (including empty title) are always broken by name ascending, 304 * regardless of the primary order's direction — this is intentional, not a bug. 305 * Falls back to 'name_asc' for any value outside the known whitelist. 306 * 307 * @param array $users Filtered user array. 308 * @param string $order 'name_asc' (default), 'name_desc', 'title_asc', or 'title_desc'. 309 * @return array Sorted array (re-indexed). 310 */ 311 private function sort_users( array $users, string $order = 'name_asc' ): array { 312 $allowed_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' ); 313 if ( ! in_array( $order, $allowed_orders, true ) ) { 314 $order = 'name_asc'; 315 } 316 317 usort( 318 $users, 319 static function ( $a, $b ) use ( $order ) { 320 if ( 'name_desc' === $order ) { 321 return strcmp( $b['name'] ?? '', $a['name'] ?? '' ); 322 } 323 324 if ( 'title_asc' === $order || 'title_desc' === $order ) { 325 $result = strcmp( $a['title'] ?? '', $b['title'] ?? '' ); 326 if ( 'title_desc' === $order ) { 327 $result = -$result; 328 } 329 // Tie on title: always break by name ascending, regardless of direction. 330 return 0 !== $result ? $result : strcmp( $a['name'] ?? '', $b['name'] ?? '' ); 331 } 332 333 // name_asc (default). 334 return strcmp( $a['name'] ?? '', $b['name'] ?? '' ); 335 } 336 ); 337 338 return array_values( $users ); 339 } 340 341 /** 209 342 * Slice a filtered user array for the requested page. 210 343 * -
ldap-staff-directory/trunk/ldap-staff-directory.php
r3634465 r3692384 4 4 * Plugin URI: https://wordpress.org/plugins/ldap-staff-directory/ 5 5 * Description: Connects to LDAP or LDAPS to display an employee directory from an OU. Supports Elementor, Beaver Builder and a native shortcode. 6 * Version: 1. 2.16 * Version: 1.4.0 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 19 19 20 20 // Plugin constants. 21 define( 'LDAP_ED_VERSION', '1. 2.1' );21 define( 'LDAP_ED_VERSION', '1.4.0' ); 22 22 define( 'LDAP_ED_FILE', __FILE__ ); 23 23 define( 'LDAP_ED_DIR', plugin_dir_path( __FILE__ ) ); -
ldap-staff-directory/trunk/public/css/directory.css
r3606783 r3692384 25 25 --ldap-dept-badge-bg: rgba( 0, 115, 170, .08 ); 26 26 --ldap-dept-badge-color: #005a87; 27 28 /* Inline field icons */ 29 --ldap-icon-color: #8c8f94; 27 30 } 28 31 … … 33 36 font-size: var( --ldap-font-size ); 34 37 color: var( --ldap-text-color ); 35 }36 37 /* ── Department filter bar ──────────────────────────────────────────────── */38 39 .ldap-dept-filters {40 display: flex;41 flex-wrap: nowrap;42 gap: 8px;43 overflow-x: auto;44 white-space: nowrap;45 padding-bottom: 4px;46 margin-bottom: 24px;47 /* Hide scrollbar visually while keeping it functional */48 -ms-overflow-style: none;49 scrollbar-width: none;50 }51 52 .ldap-dept-filters::-webkit-scrollbar {53 display: none;54 }55 56 .ldap-dept-chip {57 display: inline-flex;58 align-items: center;59 gap: 5px;60 padding: 5px 14px;61 font-size: .82em;62 font-family: inherit;63 font-weight: 500;64 color: var( --ldap-text-color );65 background: transparent;66 border: 1px solid var( --ldap-card-border );67 border-radius: 20px;68 text-decoration: none;69 cursor: pointer;70 white-space: nowrap;71 transition: border-color .15s ease, background-color .15s ease, color .15s ease;72 flex-shrink: 0;73 }74 75 a.ldap-dept-chip:hover {76 border-color: var( --ldap-primary-color );77 color: var( --ldap-primary-color );78 }79 80 .ldap-dept-chip.is-active {81 background: var( --ldap-primary-color );82 border-color: var( --ldap-primary-color );83 color: #fff;84 cursor: default;85 }86 87 .ldap-dept-chip-clear {88 display: inline-flex;89 align-items: center;90 justify-content: center;91 margin-left: 2px;92 font-size: 1.1em;93 font-weight: 700;94 color: inherit;95 text-decoration: none;96 opacity: .75;97 cursor: pointer;98 transition: opacity .15s ease;99 }100 101 .ldap-dept-chip-clear:hover {102 opacity: 1;103 38 } 104 39 … … 236 171 } 237 172 173 .ldap-title, 174 .ldap-email, 175 .ldap-phone, 176 .ldap-extension { 177 display: flex; 178 align-items: center; 179 gap: 6px; 180 } 181 238 182 .ldap-email { 239 183 margin-top: 10px; … … 265 209 266 210 .ldap-extension { 267 display: block;268 211 margin-top: 4px; 269 212 font-size: .88em; 270 213 color: var( --ldap-muted-color ); 214 } 215 216 /* ── Inline field icons (employee card, breadcrumb, department rows) ───── */ 217 218 .ldap-icon { 219 width: 12px; 220 height: 12px; 221 flex-shrink: 0; 222 color: var( --ldap-icon-color ); 271 223 } 272 224 … … 341 293 font-variant-numeric: tabular-nums; 342 294 } 295 296 /* ── Department menu (root view) ───────────────────────────────────────── */ 297 298 .ldap-menu-header { 299 margin-bottom: 20px; 300 } 301 302 .ldap-menu-title { 303 margin: 0 0 4px; 304 font-size: 1.4em; 305 font-weight: 700; 306 color: var( --ldap-text-color ); 307 } 308 309 .ldap-menu-sub { 310 margin: 0; 311 font-size: .85em; 312 color: var( --ldap-muted-color ); 313 } 314 315 .ldap-dept-search-wrap { 316 max-width: 320px; 317 } 318 319 .ldap-dept-search { 320 max-width: none; 321 } 322 323 .ldap-menu-eyebrow { 324 margin: 0 0 12px; 325 font-size: .72em; 326 font-weight: 700; 327 letter-spacing: .08em; 328 text-transform: uppercase; 329 color: var( --ldap-muted-color ); 330 } 331 332 /* Two columns on desktop (matches the prototype); pure CSS, no JS balancing. */ 333 .ldap-dept-menu-list { 334 columns: 2; 335 column-gap: 10px; 336 } 337 338 .ldap-dept-row { 339 display: flex; 340 align-items: center; 341 gap: 10px; 342 break-inside: avoid; 343 margin-bottom: 8px; 344 padding: 10px 14px; 345 background: var( --ldap-card-bg ); 346 border: 1px solid var( --ldap-card-border ); 347 border-radius: var( --ldap-card-radius ); 348 color: inherit; 349 text-decoration: none; 350 transition: box-shadow .15s ease, border-color .15s ease; 351 } 352 353 .ldap-dept-row:hover { 354 box-shadow: 0 4px 14px rgba( 0, 0, 0, .1 ); 355 border-color: #c3c4c7; 356 } 357 358 .ldap-dept-row[hidden] { 359 display: none; 360 } 361 362 .ldap-dept-row-avatar { 363 flex-shrink: 0; 364 width: 36px; 365 height: 36px; 366 border-radius: 50%; 367 background-color: var( --ldap-avatar-bg ); 368 color: #fff; 369 display: flex; 370 align-items: center; 371 justify-content: center; 372 font-size: 12px; 373 font-weight: 700; 374 user-select: none; 375 } 376 377 .ldap-dept-row-info { 378 flex: 1; 379 min-width: 0; 380 display: flex; 381 flex-direction: column; 382 gap: 2px; 383 } 384 385 .ldap-dept-row-name { 386 font-size: .93em; 387 font-weight: 600; 388 color: var( --ldap-text-color ); 389 } 390 391 .ldap-dept-row-count { 392 font-size: .8em; 393 color: var( --ldap-muted-color ); 394 } 395 396 .ldap-dept-row .ldap-icon { 397 width: 14px; 398 height: 14px; 399 color: var( --ldap-search-border ); 400 } 401 402 @media ( max-width: 540px ) { 403 .ldap-dept-menu-list { 404 columns: 1; 405 } 406 } 407 408 /* ── Breadcrumb + department header (detail view) ──────────────────────── */ 409 410 .ldap-breadcrumb { 411 display: inline-flex; 412 align-items: center; 413 gap: 6px; 414 margin-bottom: 16px; 415 font-size: .93em; 416 font-weight: 500; 417 color: var( --ldap-primary-color ); 418 text-decoration: none; 419 } 420 421 .ldap-breadcrumb:hover { 422 text-decoration: underline; 423 } 424 425 .ldap-breadcrumb .ldap-icon { 426 width: 13px; 427 height: 13px; 428 color: var( --ldap-primary-color ); 429 } 430 431 .ldap-dept-header { 432 display: flex; 433 align-items: center; 434 justify-content: space-between; 435 gap: 16px; 436 margin-bottom: 20px; 437 } 438 439 .ldap-dept-header-name { 440 margin: 0 0 4px; 441 font-size: 1.25em; 442 font-weight: 700; 443 color: var( --ldap-text-color ); 444 } 445 446 .ldap-dept-header-sub { 447 margin: 0; 448 font-size: .82em; 449 color: var( --ldap-muted-color ); 450 } 451 452 .ldap-dept-header-badge { 453 flex-shrink: 0; 454 width: 44px; 455 height: 44px; 456 border-radius: 50%; 457 background-color: var( --ldap-avatar-bg ); 458 color: #fff; 459 display: flex; 460 align-items: center; 461 justify-content: center; 462 font-size: 14px; 463 font-weight: 700; 464 user-select: none; 465 } 466 467 @media ( max-width: 540px ) { 468 .ldap-dept-header { 469 flex-direction: column; 470 align-items: flex-start; 471 } 472 473 .ldap-dept-header-badge { 474 width: 36px; 475 height: 36px; 476 font-size: 12px; 477 } 478 } -
ldap-staff-directory/trunk/public/js/directory.js
r3548101 r3692384 1 1 /** 2 2 * LDAP Staff Directory — public JS 3 * Filtering and pagination are now server-side. This file strips empty 4 * ldap_search from the form submission to keep URLs clean. 3 * 4 * Two independent, small pieces of behavior: 5 * 1. Employee search (department detail view) is server-side — this only 6 * strips an empty ldap_search from the form submission to keep URLs clean. 7 * 2. Department search (department menu view) is client-side — it filters 8 * the already-rendered .ldap-dept-row elements by substring match, with 9 * no network request. Scoped to at most the total number of departments; 10 * it never touches employee cards or pagination. 5 11 */ 6 12 ( function () { … … 16 22 } ); 17 23 } ); 24 25 var deptSearchInput = document.getElementById( 'ldap-dept-search-input' ); 26 if ( ! deptSearchInput ) { 27 return; 28 } 29 30 var wrap = deptSearchInput.closest( '.ldap-directory-wrap' ); 31 if ( ! wrap ) { 32 return; 33 } 34 35 var deptRows = Array.prototype.slice.call( wrap.querySelectorAll( '.ldap-dept-row' ) ); 36 var deptNoResults = wrap.querySelector( '.ldap-dept-no-results' ); 37 38 deptSearchInput.addEventListener( 'input', function () { 39 var query = deptSearchInput.value.trim().toLowerCase(); 40 var visible = 0; 41 42 deptRows.forEach( function ( row ) { 43 var matches = '' === query || row.dataset.name.toLowerCase().indexOf( query ) !== -1; 44 row.hidden = ! matches; 45 if ( matches ) { 46 visible++; 47 } 48 } ); 49 50 if ( deptNoResults ) { 51 deptNoResults.hidden = visible > 0; 52 } 53 } ); 18 54 } ); 19 55 } () ); -
ldap-staff-directory/trunk/public/views/directory.php
r3606783 r3692384 1 1 <?php 2 2 /** 3 * Public directory template. 3 * Department detail template — rendered when a department is selected 4 * (ldap_dept present). Shows a breadcrumb back to the department menu, the 5 * active department's header, and the existing filtered/paginated employee 6 * grid. 4 7 * 5 8 * Variables available from class-shortcode.php: … … 7 10 * $ldap_ed_fields — array of field keys to display 8 11 * $ldap_ed_per_page — int, items per page 9 * $ldap_ed_enable_search — bool, show search input and filter bar 10 * $ldap_ed_departments — array [ 'Department' => count ] sorted alphabetically 11 * $ldap_ed_current_dept — string, active department filter ('' = none) 12 * $ldap_ed_enable_search — bool, show the employee search input 13 * $ldap_ed_current_dept — string, active department (always non-empty here) 12 14 * $ldap_ed_search_query — string, active search term 13 15 * $ldap_ed_current_page — int, current page number 14 16 * $ldap_ed_total_pages — int, total page count 15 * $ldap_ed_total_count — int, total filtered employee count 16 * $ldap_ed_all_count — int, total unfiltered employee count 17 * $ldap_ed_total_count — int, total filtered employee count (this department) 17 18 * $ldap_ed_prev_url — string|null, URL for previous page (null = disabled) 18 19 * $ldap_ed_next_url — string|null, URL for next page (null = disabled) … … 25 26 } 26 27 27 // Palette for avatar backgrounds — one color is deterministically assigned per employee. 28 $ldap_avatar_palette = array( 29 '#4f7df3', '#7c5cbf', '#0e9b8a', 30 '#2e9e4f', '#c0392b', '#d35400', 31 '#1a7bbf', '#8e44ad', 32 ); 28 $ldap_ed_dept_avatar = $this->get_initials_and_color( $ldap_ed_current_dept, true ); 33 29 ?> 34 30 <div class="ldap-directory-wrap"> 31 32 <a 33 href="<?php echo esc_url( remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ); ?>" 34 class="ldap-breadcrumb" 35 > 36 <?php echo $this->get_icon_svg( 'arrow-left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 37 <?php esc_html_e( 'Departments', 'ldap-staff-directory' ); ?> 38 </a> 39 40 <div class="ldap-dept-header"> 41 <div class="ldap-dept-header-info"> 42 <h2 class="ldap-dept-header-name"><?php echo esc_html( $ldap_ed_current_dept ); ?></h2> 43 <p class="ldap-dept-header-sub"> 44 <?php 45 echo esc_html( 46 sprintf( 47 /* translators: %s: number of employees in this department */ 48 _n( '%s employee in this department', '%s employees in this department', $ldap_ed_total_count, 'ldap-staff-directory' ), 49 absint( $ldap_ed_total_count ) 50 ) 51 ); 52 ?> 53 </p> 54 </div> 55 <span 56 class="ldap-dept-header-badge" 57 aria-hidden="true" 58 style="--ldap-avatar-bg:<?php echo esc_attr( $ldap_ed_dept_avatar['color'] ); ?>" 59 ><?php echo esc_html( $ldap_ed_dept_avatar['initials'] ); ?></span> 60 </div> 35 61 36 62 <?php if ( $ldap_ed_enable_search ) : ?> … … 45 71 <?php esc_html_e( 'Search employees', 'ldap-staff-directory' ); ?> 46 72 </label> 47 <?php if ( '' !== $ldap_ed_current_dept ) : ?>48 73 <input type="hidden" name="ldap_dept" value="<?php echo esc_attr( $ldap_ed_current_dept ); ?>"> 49 <?php endif; ?>50 74 <input 51 75 type="search" … … 53 77 name="ldap_search" 54 78 class="ldap-search" 55 placeholder="<?php esc_attr_e( 'Search employee…', 'ldap-staff-directory' ); ?>" 79 placeholder="<?php 80 echo esc_attr( 81 sprintf( 82 /* translators: %s: department name */ 83 __( 'Search in %s…', 'ldap-staff-directory' ), 84 $ldap_ed_current_dept 85 ) 86 ); 87 ?>" 56 88 aria-label="<?php esc_attr_e( 'Search employee', 'ldap-staff-directory' ); ?>" 57 89 value="<?php echo esc_attr( $ldap_ed_search_query ); ?>" … … 59 91 </form> 60 92 </div> 61 62 <?php if ( count( $ldap_ed_departments ) >= 2 ) : ?>63 <div class="ldap-dept-filters" role="list" aria-label="<?php esc_attr_e( 'Filter by department', 'ldap-staff-directory' ); ?>">64 65 <?php // "All" chip — active when no department filter is set. ?>66 <?php $ldap_ed_all_url = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) ); ?>67 <?php if ( '' === $ldap_ed_current_dept ) : ?>68 <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">69 <?php70 echo esc_html(71 sprintf(72 /* translators: %s: total employee count */73 __( 'All (%s)', 'ldap-staff-directory' ),74 absint( $ldap_ed_all_count )75 )76 );77 ?>78 </span>79 <?php else : ?>80 <a81 href="<?php echo $ldap_ed_all_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"82 class="ldap-dept-chip"83 role="listitem"84 aria-pressed="false"85 >86 <?php87 echo esc_html(88 sprintf(89 /* translators: %s: total employee count */90 __( 'All (%s)', 'ldap-staff-directory' ),91 absint( $ldap_ed_all_count )92 )93 );94 ?>95 </a>96 <?php endif; ?>97 98 <?php foreach ( $ldap_ed_departments as $ldap_ed_dept_name => $ldap_ed_dept_count ) : ?>99 <?php100 $ldap_ed_is_active = ( 0 === strcasecmp( $ldap_ed_dept_name, $ldap_ed_current_dept ) );101 $ldap_ed_chip_url = esc_url( add_query_arg( array( 'ldap_dept' => $ldap_ed_dept_name, 'ldap_page' => 1 ) ) );102 $ldap_ed_clear_url = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) );103 $ldap_ed_chip_label = esc_html( $ldap_ed_dept_name ) . ' (' . absint( $ldap_ed_dept_count ) . ')';104 ?>105 <?php if ( $ldap_ed_is_active ) : ?>106 <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">107 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>108 <a109 href="<?php echo $ldap_ed_clear_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"110 class="ldap-dept-chip-clear"111 aria-label="<?php esc_attr_e( 'Clear department filter', 'ldap-staff-directory' ); ?>"112 >×</a>113 </span>114 <?php else : ?>115 <a116 href="<?php echo $ldap_ed_chip_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"117 class="ldap-dept-chip"118 role="listitem"119 aria-pressed="false"120 >121 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>122 </a>123 <?php endif; ?>124 <?php endforeach; ?>125 126 </div>127 <?php endif; ?>128 93 <?php endif; ?> 129 94 … … 134 99 <?php else : ?> 135 100 <?php foreach ( $ldap_ed_users as $ldap_ed_user ) : 136 // Compute initials (up to 2 characters) from the display name. 137 $ldap_name = $ldap_ed_user['name'] ?? ''; 138 $ldap_parts = preg_split( '/\s+/', trim( $ldap_name ), 2 ); 139 $ldap_initials = strtoupper( substr( $ldap_parts[0] ?? '', 0, 1 ) ); 140 if ( ! empty( $ldap_parts[1] ) ) { 141 $ldap_initials .= strtoupper( substr( $ldap_parts[1], 0, 1 ) ); 142 } 101 $ldap_ed_name = $ldap_ed_user['name'] ?? ''; 102 $ldap_ed_avatar = $this->get_initials_and_color( $ldap_ed_name ); 103 $ldap_initials = $ldap_ed_avatar['initials']; 104 143 105 // Fall back to first letter of email when name is absent. 144 106 if ( '' === $ldap_initials && ! empty( $ldap_ed_user['email'] ) ) { … … 146 108 } 147 109 148 // Deterministic background color derived from the name. 149 $ldap_color_idx = abs( crc32( $ldap_name ) ) % count( $ldap_avatar_palette ); 150 $ldap_avatar_bg = $ldap_avatar_palette[ $ldap_color_idx ]; 110 $ldap_avatar_bg = $ldap_ed_avatar['color']; 151 111 ?> 152 112 <article class="ldap-staff-card"> … … 162 122 163 123 <?php if ( in_array( 'title', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['title'] ) ) : ?> 164 <p class="ldap-title"><?php echo esc_html( $ldap_ed_user['title'] ); ?></p> 124 <p class="ldap-title"> 125 <?php echo $this->get_icon_svg( 'briefcase' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 126 <?php echo esc_html( $ldap_ed_user['title'] ); ?> 127 </p> 165 128 <?php endif; ?> 166 129 … … 173 136 <?php if ( in_array( 'email', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['email'] ) ) : ?> 174 137 <a class="ldap-email" href="mailto:<?php echo esc_attr( $ldap_ed_user['email'] ); ?>"> 138 <?php echo $this->get_icon_svg( 'mail' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 175 139 <?php echo esc_html( $ldap_ed_user['email'] ); ?> 176 140 </a> … … 179 143 <?php if ( in_array( 'phone', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['phone'] ) ) : ?> 180 144 <a class="ldap-phone" href="tel:<?php echo esc_attr( $ldap_ed_user['phone'] ); ?>"> 145 <?php echo $this->get_icon_svg( 'phone' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 181 146 <?php echo esc_html( $ldap_ed_user['phone'] ); ?> 182 147 </a> … … 184 149 185 150 <?php if ( in_array( 'extension', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['extension'] ) ) : ?> 186 <span class="ldap-extension"><?php echo esc_html( $ldap_ed_user['extension'] ); ?></span> 151 <span class="ldap-extension"> 152 <?php echo $this->get_icon_svg( 'building' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?> 153 <?php echo esc_html( $ldap_ed_user['extension'] ); ?> 154 </span> 187 155 <?php endif; ?> 188 156 </article> … … 209 177 $ldap_ed_from = absint( ( $ldap_ed_current_page - 1 ) * $ldap_ed_per_page + 1 ); 210 178 $ldap_ed_to = absint( min( $ldap_ed_current_page * $ldap_ed_per_page, $ldap_ed_total_count ) ); 211 if ( '' !== $ldap_ed_current_dept ) : 212 echo esc_html( 213 sprintf( 214 /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */ 215 __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ), 216 absint( $ldap_ed_from ), 217 absint( $ldap_ed_to ), 218 absint( $ldap_ed_total_count ), 219 $ldap_ed_current_dept 220 ) 221 ); 222 else : 223 echo esc_html( 224 sprintf( 225 /* translators: 1: first record number on page, 2: last record number on page, 3: total records */ 226 __( 'Showing %1$s–%2$s of %3$s', 'ldap-staff-directory' ), 227 absint( $ldap_ed_from ), 228 absint( $ldap_ed_to ), 229 absint( $ldap_ed_total_count ) 230 ) 231 ); 232 endif; 179 echo esc_html( 180 sprintf( 181 /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */ 182 __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ), 183 absint( $ldap_ed_from ), 184 absint( $ldap_ed_to ), 185 absint( $ldap_ed_total_count ), 186 $ldap_ed_current_dept 187 ) 188 ); 233 189 endif; ?> 234 190 </span> -
ldap-staff-directory/trunk/readme.txt
r3634465 r3692384 3 3 Tags: ldap, directory, wpbeaverbuilder, staff, elementor 4 4 Requires at least: 5.8 5 Tested up to: 7. 05 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.17 Stable tag: 1.4.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 23 23 * Native Elementor widget with full style controls 24 24 * Native Beaver Builder module with General and Style tabs 25 * Department filter bar with configurable chip order (alphabetical or by contact count) 25 * Public directory browses department-first: a menu of departments (with employee counts) leads to each department's contact list, in configurable order (alphabetical or by contact count) 26 * Employee cards within a department can be ordered by name or job title, ascending or descending, from the admin — no code changes needed 26 27 * Exclude specific departments — or employees with no department assigned — from the public directory, enforced at the LDAP query level so those employees are never fetched 27 28 * Exclude disabled Active Directory accounts … … 77 78 By default 60 minutes. Change the TTL under **Settings → LDAP Directory → Fields → Advanced settings**, or flush immediately with the **Clear Cache** button on that same tab. 78 79 80 = Why do I now see a list of departments instead of all employees? = 81 82 Starting in 1.3.0, the public directory browses department-first: visitors see a menu of departments with employee counts, and click into one to see its contacts. This replaces the previous single-page grid with a department filter bar. There is no setting to restore the old flat grid. 83 79 84 == Screenshots == 80 85 … … 85 90 86 91 == Changelog == 92 93 = 1.4.0 = 94 * Feature: New **Employee order** setting under **Settings → LDAP Directory → Fields** lets you choose how employee cards are ordered within a department: by name or job title, ascending or descending. Ties on job title (including employees with no title) are always broken by name (A–Z). 95 96 = 1.3.0 = 97 * Feature: The public directory now browses department-first. The root view shows a menu of departments (name, employee count, color-coded avatar) instead of the full employee grid; clicking a department opens its filtered, paginated contact list with a breadcrumb back to the menu. 98 * Feature: Employee cards in the department view show a small icon next to job title, email, phone, and extension for easier scanning. 99 * Breaking change: The department filter chip bar has been removed and replaced by the department menu described above. There is no setting to restore the previous single-page grid. 100 * Chore: "Tested up to" updated to WordPress 7.1. 87 101 88 102 = 1.2.1 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)