├── .editorconfig ├── .eslintrc ├── .github ├── release-drafter.yml └── workflows │ ├── phpunit.yml │ └── release-drafter.yml ├── .gitignore ├── .nvmrc ├── .wp-env.json ├── _blueprints ├── aql-content.xml ├── blueprint.json ├── cpt-demo.php └── demo-content.xml ├── composer.json ├── extending-aql.md ├── includes ├── Query_Params_Generator.php ├── Traits │ ├── Date_Query.php │ ├── Disable_Pagination.php │ ├── Exclude_Current.php │ ├── Exclude_Taxonomies.php │ ├── Include_Posts.php │ ├── Meta_Query.php │ ├── Multiple_Posts.php │ ├── Post_Parent.php │ └── Tax_Query.php ├── enqueues.php ├── query-loop.php ├── taxonomy.php └── utilities.php ├── index.php ├── package.json ├── phpcs.xml ├── phpunit.xml ├── readme.md ├── readme.txt ├── src ├── components │ ├── child-items-toggle.js │ ├── exclude-taxonomies.js │ ├── icons.js │ ├── multiple-post-select.js │ ├── pagination-toggle.js │ ├── post-count-controls.js │ ├── post-date-query-controls.js │ ├── post-exclude-controls.js │ ├── post-include-controls.js │ ├── post-meta-control.js │ ├── post-meta-query-controls.js │ ├── post-offset-controls.js │ ├── post-order-controls.js │ ├── single-taxonomy-control.js │ └── taxonomy-query-control.js ├── hooks │ └── useDebouncedInputValue.js ├── legacy-controls │ └── pre-gb-19.js ├── slots │ ├── aql-controls-inherited-query.js │ ├── aql-controls.js │ └── aql-legacy-controls.js ├── utils │ └── index.js └── variations │ ├── controls.js │ └── index.js ├── tests └── unit │ ├── Date_Query_Tests.php │ ├── Exclude_Current_Tests.php │ ├── Multiple_Post_Types_Tests.php │ ├── Query_Params_Generator_Tests.php │ └── bootstrap.php └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [*.{yml,yaml}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "plugin:@wordpress/eslint-plugin/recommended" ] 3 | } 4 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$NEXT_PATCH_VERSION' 2 | tag-template: '$NEXT_PATCH_VERSION' 3 | template: | 4 | ## What’s Changed 5 | 6 | $CHANGES 7 | 8 | categories: 9 | - title: '🚀 Features' 10 | label: 'enhancement' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | push: 5 | branches: [ "trunk" ] 6 | pull_request: 7 | branches: [ "trunk" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Validate composer.json and composer.lock 21 | run: composer validate --strict 22 | 23 | - name: Cache Composer packages 24 | id: composer-cache 25 | uses: actions/cache@v3 26 | with: 27 | path: vendor 28 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-php- 31 | 32 | - name: Install dependencies 33 | run: composer install --prefer-dist --no-progress 34 | 35 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 36 | # Docs: https://getcomposer.org/doc/articles/scripts.md 37 | 38 | - name: Run test suite 39 | run: composer run-script phpunit 40 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - trunk 8 | # pull_request event is required only for autolabeler 9 | pull_request: 10 | # Only following types are handled by the action, but one can default to all as well 11 | types: [opened, reopened, synchronize] 12 | # pull_request_target event is required for autolabeler to support PRs from forks 13 | # pull_request_target: 14 | # types: [opened, reopened, synchronize] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | update_release_draft: 21 | permissions: 22 | # write permission is required to create a github release 23 | contents: write 24 | # write permission is required for autolabeler 25 | # otherwise, read permission is required at least 26 | pull-requests: write 27 | runs-on: ubuntu-latest 28 | steps: 29 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 30 | #- name: Set GHE_HOST 31 | # run: | 32 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 33 | 34 | # Drafts your next Release notes as Pull Requests are merged into "master" 35 | - uses: release-drafter/release-drafter@v6 36 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 37 | # with: 38 | # config-name: my-config.yml 39 | # disable-autolabeler: true 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | package-lock.json 4 | .DS_Store 5 | *.zip 6 | DNU 7 | 8 | /vendor/ 9 | composer.lock 10 | .phpunit.* 11 | artifacts 12 | .vscode 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": "WordPress/WordPress", 3 | "plugins": [ 4 | ".", 5 | "../advanced-query-loop-extension", 6 | "../advanced-query-loop-testing" 7 | ], 8 | "env": { 9 | "tests": { 10 | "phpVersion": "8.1" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /_blueprints/aql-content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | AQL Demo Content 30 | http://aql-demo-content.local 31 | 32 | Thu, 20 Jun 2024 15:52:14 +0000 33 | en-US 34 | 1.2 35 | http://aql-demo-content.local 36 | http://aql-demo-content.local 37 | 38 | 1 39 | 40 | 41 | 1 42 | 43 | 44 | 45 | 46 | 47 | 2 48 | 49 | 50 | 51 | 52 | 53 | 54 | 1 55 | 56 | 57 | 58 | 59 | 60 | 61 | https://wordpress.org/?v=6.5.4 62 | 63 | 64 | <![CDATA[Post with Featured Image 2]]> 65 | http://aql-demo-content.local/hello-world/ 66 | Thu, 20 Jun 2024 15:45:03 +0000 67 | 68 | http://aql-demo-content.local/?p=1 69 | 70 | 71 | 72 | 1 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 0 82 | 0 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 92 | 1 93 | 94 | 95 | https://wordpress.org/ 96 | 97 | 98 | 99 | Gravatar.]]> 102 | 103 | 104 | 0 105 | 0 106 | 107 | 108 | 109 | <![CDATA[Featured Image Query]]> 110 | http://aql-demo-content.local/sample-page/ 111 | Thu, 20 Jun 2024 15:45:03 +0000 112 | 113 | http://aql-demo-content.local/?page_id=2 114 | 115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |

133 | 134 |
135 | ]]>
136 | 137 | 2 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 0 147 | 0 148 | 149 | 150 | 0 151 | 152 | 153 | 154 | 155 |
156 | 157 | <![CDATA[Privacy Policy]]> 158 | http://aql-demo-content.local/?page_id=3 159 | Thu, 20 Jun 2024 15:45:03 +0000 160 | 161 | http://aql-demo-content.local/?page_id=3 162 | 163 | 164 |

Who we are

165 | 166 | 167 |

Suggested text: Our website address is: http://aql-demo-content.local.

168 | 169 | 170 |

Comments

171 | 172 | 173 |

Suggested text: When visitors leave comments on the site we collect the data shown in the comments form, and also the visitor’s IP address and browser user agent string to help spam detection.

174 | 175 | 176 |

An anonymized string created from your email address (also called a hash) may be provided to the Gravatar service to see if you are using it. The Gravatar service privacy policy is available here: https://automattic.com/privacy/. After approval of your comment, your profile picture is visible to the public in the context of your comment.

177 | 178 | 179 |

Media

180 | 181 | 182 |

Suggested text: If you upload images to the website, you should avoid uploading images with embedded location data (EXIF GPS) included. Visitors to the website can download and extract any location data from images on the website.

183 | 184 | 185 |

Cookies

186 | 187 | 188 |

Suggested text: If you leave a comment on our site you may opt-in to saving your name, email address and website in cookies. These are for your convenience so that you do not have to fill in your details again when you leave another comment. These cookies will last for one year.

189 | 190 | 191 |

If you visit our login page, we will set a temporary cookie to determine if your browser accepts cookies. This cookie contains no personal data and is discarded when you close your browser.

192 | 193 | 194 |

When you log in, we will also set up several cookies to save your login information and your screen display choices. Login cookies last for two days, and screen options cookies last for a year. If you select "Remember Me", your login will persist for two weeks. If you log out of your account, the login cookies will be removed.

195 | 196 | 197 |

If you edit or publish an article, an additional cookie will be saved in your browser. This cookie includes no personal data and simply indicates the post ID of the article you just edited. It expires after 1 day.

198 | 199 | 200 |

Embedded content from other websites

201 | 202 | 203 |

Suggested text: Articles on this site may include embedded content (e.g. videos, images, articles, etc.). Embedded content from other websites behaves in the exact same way as if the visitor has visited the other website.

204 | 205 | 206 |

These websites may collect data about you, use cookies, embed additional third-party tracking, and monitor your interaction with that embedded content, including tracking your interaction with the embedded content if you have an account and are logged in to that website.

207 | 208 | 209 |

Who we share your data with

210 | 211 | 212 |

Suggested text: If you request a password reset, your IP address will be included in the reset email.

213 | 214 | 215 |

How long we retain your data

216 | 217 | 218 |

Suggested text: If you leave a comment, the comment and its metadata are retained indefinitely. This is so we can recognize and approve any follow-up comments automatically instead of holding them in a moderation queue.

219 | 220 | 221 |

For users that register on our website (if any), we also store the personal information they provide in their user profile. All users can see, edit, or delete their personal information at any time (except they cannot change their username). Website administrators can also see and edit that information.

222 | 223 | 224 |

What rights you have over your data

225 | 226 | 227 |

Suggested text: If you have an account on this site, or have left comments, you can request to receive an exported file of the personal data we hold about you, including any data you have provided to us. You can also request that we erase any personal data we hold about you. This does not include any data we are obliged to keep for administrative, legal, or security purposes.

228 | 229 | 230 |

Where your data is sent

231 | 232 | 233 |

Suggested text: Visitor comments may be checked through an automated spam detection service.

234 | 235 | ]]>
236 | 237 | 3 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 0 247 | 0 248 | 249 | 250 | 0 251 | 252 | 253 | 254 | 255 |
256 | 257 | <![CDATA[Custom Styles]]> 258 | http://aql-demo-content.local/wp-global-styles-twentytwentyfour/ 259 | Thu, 20 Jun 2024 15:45:55 +0000 260 | 261 | http://aql-demo-content.local/wp-global-styles-twentytwentyfour/ 262 | 263 | 264 | 265 | 5 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 0 275 | 0 276 | 277 | 278 | 0 279 | 280 | 281 | 282 | <![CDATA[image]]> 283 | http://aql-demo-content.local/hello-world/image/ 284 | Thu, 20 Jun 2024 15:46:17 +0000 285 | 286 | http://aql-demo-content.local/wp-content/uploads/2024/06/image.webp 287 | 288 | 289 | Free spotted whale shark image"/ CC0 1.0]]> 290 | 6 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 1 300 | 0 301 | 302 | 303 | 0 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | <![CDATA[image-1]]> 316 | http://aql-demo-content.local/hello-world/image-1/ 317 | Thu, 20 Jun 2024 15:46:24 +0000 318 | 319 | http://aql-demo-content.local/wp-content/uploads/2024/06/image-1.webp 320 | 321 | 322 | Free shark image"/ CC0 1.0]]> 323 | 7 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 1 333 | 0 334 | 335 | 336 | 0 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | <![CDATA[image-2]]> 349 | http://aql-demo-content.local/hello-world/image-2/ 350 | Thu, 20 Jun 2024 15:46:31 +0000 351 | 352 | http://aql-demo-content.local/wp-content/uploads/2024/06/image-2.webp 353 | 354 | 355 | Free shark image"/ CC0 1.0]]> 356 | 8 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 1 366 | 0 367 | 368 | 369 | 0 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | <![CDATA[Post with Featured Image 1]]> 382 | http://aql-demo-content.local/post-with-featured-image-1/ 383 | Thu, 20 Jun 2024 15:47:10 +0000 384 | 385 | http://aql-demo-content.local/?p=9 386 | 387 | 388 | 389 | 9 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 0 399 | 0 400 | 401 | 402 | 0 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | <![CDATA[Post with Featured Image 3]]> 411 | http://aql-demo-content.local/post-with-featured-image-3/ 412 | Thu, 20 Jun 2024 15:47:53 +0000 413 | 414 | http://aql-demo-content.local/?p=12 415 | 416 | 417 | 418 | 12 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 0 428 | 0 429 | 430 | 431 | 0 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | <![CDATA[Post with no Featured Image]]> 440 | http://aql-demo-content.local/post-with-no-featured-image/ 441 | Thu, 20 Jun 2024 15:49:16 +0000 442 | 443 | http://aql-demo-content.local/?p=15 444 | 445 | 446 |

Repellat nostra, vivamus nonummy. Malesuada voluptates debitis ullamcorper, neque do ea eget. Debitis rem nostrud nonummy consequatur, mollit nesciunt lectus, ultrices torquent. Tenetur consectetuer corporis nisl, sequi etiam. Felis mi do laborum facilis dui, nemo earum, ea vivamus soluta possimus molestiae sed alias blandit molestiae, curae! Adipisci nostrud molestie condimentum.

447 | ]]>
448 | 449 | 15 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 0 459 | 0 460 | 461 | 462 | 0 463 | 464 |
465 |
466 |
467 | -------------------------------------------------------------------------------- /_blueprints/blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/post.php?post=59&action=edit", 4 | "preferredVersions": { 5 | "php": "8.0", 6 | "wp": "latest" 7 | }, 8 | "phpExtensionBundles": [ "kitchen-sink" ], 9 | "features": { 10 | "networking": true 11 | }, 12 | "steps": [ 13 | { 14 | "step": "login", 15 | "username": "admin", 16 | "password": "password" 17 | }, 18 | { 19 | "step": "installPlugin", 20 | "pluginZipFile": { 21 | "resource": "url", 22 | "url": "https://downloads.wordpress.org/plugin/advanced-query-loop.zip" 23 | }, 24 | "options": { 25 | "activate": true 26 | } 27 | }, 28 | { 29 | "step": "mkdir", 30 | "path": "/wordpress/wp-content/plugins/cpts/" 31 | }, 32 | { 33 | "step": "writeFile", 34 | "path": "/wordpress/wp-content/plugins/cpts/cpt-demo-plugin.php", 35 | "data": { 36 | "resource": "url", 37 | "url": "https://raw.githubusercontent.com/ryanwelcher/advanced-query-loop/trunk/_blueprints/cpt-demo.php" 38 | } 39 | }, 40 | { 41 | "step": "activatePlugin", 42 | "pluginPath": "/wordpress/wp-content/plugins/cpts/" 43 | }, 44 | { 45 | "step": "importFile", 46 | "file": { 47 | "resource": "url", 48 | "url": "https://raw.githubusercontent.com/ryanwelcher/advanced-query-loop/trunk/_blueprints/demo-content.xml" 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /_blueprints/cpt-demo.php: -------------------------------------------------------------------------------- 1 | _x( 'Streams', 'Post type general name', 'twitch-theme' ), 25 | 'singular_name' => _x( 'Stream', 'Post type singular name', 'twitch-theme' ), 26 | 'menu_name' => _x( 'Streams', 'Admin Menu text', 'twitch-theme' ), 27 | 'name_admin_bar' => _x( 'Stream', 'Add New on Toolbar', 'twitch-theme' ), 28 | 'add_new' => __( 'Add New', 'twitch-theme' ), 29 | 'add_new_item' => __( 'Add New Stream', 'twitch-theme' ), 30 | 'new_item' => __( 'New Stream', 'twitch-theme' ), 31 | 'edit_item' => __( 'Edit Stream', 'twitch-theme' ), 32 | 'view_item' => __( 'View Stream', 'twitch-theme' ), 33 | 'all_items' => __( 'All Stream', 'twitch-theme' ), 34 | 'search_items' => __( 'Search Streams', 'twitch-theme' ), 35 | 'parent_item_colon' => __( 'Parent Stream:', 'twitch-theme' ), 36 | 'not_found' => __( 'No Streams found.', 'twitch-theme' ), 37 | 'not_found_in_trash' => __( 'No Streams found in Trash.', 'twitch-theme' ), 38 | 'featured_image' => _x( 'Stream Thumbnail', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'twitch-theme' ), 39 | 'set_featured_image' => _x( 'Set stream thumbnail', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'twitch-theme' ), 40 | 'remove_featured_image' => _x( 'Remove stream thumbnail', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'twitch-theme' ), 41 | 'use_featured_image' => _x( 'Use as stream thumbnail', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'twitch-theme' ), 42 | 'archives' => _x( 'Stream archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'twitch-theme' ), 43 | 'insert_into_item' => _x( 'Insert into Stream', 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'twitch-theme' ), 44 | 'uploaded_to_this_item' => _x( 'Uploaded to this Stream', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'twitch-theme' ), 45 | 'filter_items_list' => _x( 'Filter Stream list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'twitch-theme' ), 46 | 'items_list_navigation' => _x( 'Streams list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'twitch-theme' ), 47 | 'items_list' => _x( 'Streams list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'twitch-theme' ), 48 | ); 49 | 50 | $args = array( 51 | 'labels' => $labels, 52 | 'description' => 'Stream custom post type.', 53 | 'public' => true, 54 | 'publicly_queryable' => true, 55 | 'show_ui' => true, 56 | 'show_in_menu' => true, 57 | 'query_var' => true, 58 | 'rewrite' => array( 'slug' => 'streams' ), 59 | 'capability_type' => 'post', 60 | 'has_archive' => true, 61 | 'hierarchical' => false, 62 | 'menu_position' => 20, 63 | 'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields', 'author' ), 64 | 'taxonomies' => array( 'category', 'post_tag' ), 65 | 'show_in_rest' => true, 66 | 'menu_icon' => 'dashicons-format-video', 67 | ); 68 | 69 | register_post_type( 'twitch-stream', $args ); 70 | 71 | // Register some post meta. 72 | register_post_meta( 73 | 'twitch-stream', 74 | 'stream-duration', 75 | array( 76 | 'show_in_rest' => true, 77 | 'single' => true, 78 | 'type' => 'string', 79 | ) 80 | ); 81 | 82 | register_post_meta( 83 | 'twitch-stream', 84 | 'stream-date', 85 | array( 86 | 'show_in_rest' => true, 87 | 'single' => true, 88 | 'type' => 'string', 89 | ) 90 | ); 91 | 92 | $labels = array( 93 | 'name' => _x( 'Genres', 'taxonomy general name', 'textdomain' ), 94 | 'singular_name' => _x( 'Genre', 'taxonomy singular name', 'textdomain' ), 95 | 'search_items' => __( 'Search Genres', 'textdomain' ), 96 | 'all_items' => __( 'All Genres', 'textdomain' ), 97 | 'parent_item' => __( 'Parent Genre', 'textdomain' ), 98 | 'parent_item_colon' => __( 'Parent Genre:', 'textdomain' ), 99 | 'edit_item' => __( 'Edit Genre', 'textdomain' ), 100 | 'update_item' => __( 'Update Genre', 'textdomain' ), 101 | 'add_new_item' => __( 'Add New Genre', 'textdomain' ), 102 | 'new_item_name' => __( 'New Genre Name', 'textdomain' ), 103 | 'menu_name' => __( 'Genre', 'textdomain' ), 104 | ); 105 | 106 | $args = array( 107 | 'hierarchical' => true, 108 | 'labels' => $labels, 109 | 'show_ui' => true, 110 | 'show_admin_column' => true, 111 | 'query_var' => true, 112 | 'show_in_rest' => true, 113 | 'rewrite' => array( 'slug' => 'genre' ), 114 | ); 115 | 116 | register_taxonomy( 'genre', array( 'twitch-stream' ), $args ); 117 | 118 | } 119 | add_action( 'init', 'register_stream_post_type' ); 120 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ryanwelcher/advanced-query-loop", 3 | "description": "Query loop block variations to create custom queries.", 4 | "authors": [ 5 | { 6 | "name": "Ryan Welcher", 7 | "email": "me@ryanwelcher.com" 8 | } 9 | ], 10 | "keywords": [ 11 | "wordpress" 12 | ], 13 | "type": "wordpress-plugin", 14 | "homepage": "https://github.com/ryanwelcher/advanced-query-loop", 15 | "license": "GPL-2.0-or-later", 16 | "support": { 17 | "issues": "https://github.com/ryanwelcher/advanced-query-loop/issues", 18 | "source": "https://github.com/ryanwelcher/advanced-query-loop/" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "AdvancedQueryLoop\\": "includes/" 23 | }, 24 | "files": [ 25 | "includes/enqueues.php", 26 | "includes/query-loop.php", 27 | "includes/utilities.php" 28 | ] 29 | }, 30 | "require-dev": { 31 | "dealerdirect/phpcodesniffer-composer-installer": "*", 32 | "object-calisthenics/phpcs-calisthenics-rules": "*", 33 | "phpcompatibility/php-compatibility": "*", 34 | "wp-coding-standards/wpcs": "*", 35 | "phpunit/phpunit": "^8.5", 36 | "yoast/phpunit-polyfills": "^2.0" 37 | }, 38 | "require": { 39 | "php": ">=7.4" 40 | }, 41 | "config": { 42 | "allow-plugins": { 43 | "dealerdirect/phpcodesniffer-composer-installer": true, 44 | "composer/installers": true 45 | } 46 | }, 47 | "scripts": { 48 | "install-codestandards": [ 49 | "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run" 50 | ], 51 | "post-install-cmd": [ 52 | "@install-codestandards" 53 | ], 54 | "build": [ 55 | "composer update --no-dev", 56 | "composer dump-autoload -o --no-dev" 57 | ], 58 | "dev": [ 59 | "composer update", 60 | "composer dump-autoload" 61 | ], 62 | "phpunit": "vendor/bin/phpunit" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /extending-aql.md: -------------------------------------------------------------------------------- 1 | ## Extending AQL 2 | 3 | Since version 1.5, AQL is now completely extendable. Using the SlotFills and filter outlined below, you can add any custom control you may need! The code below can be seen in my [advanced-query-loop-extension plugin here](https://github.com/ryanwelcher/advanced-query-loop-extension) 4 | 5 | #### SlotFills 6 | 7 | There are two SlotFills available to extend the UI of AQL that are exposed based the value of the `Inherit query from template` setting of the block. 8 | 9 | The purpose of having two options is to be able to customize when a UI element is added. There may be cases that a particular control doesn't make sense to be shown when the query is being inherited. 10 | For example, a control that makes changes to the content types being displayed may not make sense when used in an archive template and so that control would only be added using the ` { 26 | const { query: { authorContent = false } = {} } = attributes; 27 | return ( 28 | <> 29 | { 33 | setAttributes( { 34 | query: { 35 | ...attributes.query, 36 | authorContent: ! authorContent, 37 | }, 38 | } ); 39 | } } 40 | /> 41 | 42 | ); 43 | }; 44 | 45 | registerPlugin( 'aql-extension', { 46 | render: () => { 47 | return ( 48 | <> 49 | 50 | { ( props ) => } 51 | 52 | 53 | { ( props ) => } 54 | 55 | 56 | ); 57 | }, 58 | } ); 59 | ``` 60 | 61 | #### Filters 62 | 63 | Once the control is in place and saving, you will need to use that new query variable to modify the underlying `WP_Query` instance via the `aql_query_vars` filter. 64 | 65 | The filter provides three parameters: 66 | 67 | - `$query_args` Arguments to be passed to WP_Query. 68 | - `$block_query` The query attribute retrieved from the block. 69 | - `$inherited` Whether the query is being inherited. 70 | 71 | The example code below modifies the query based on the status of the control added above. 72 | 73 | ```php 74 | /** 75 | * Add a filter to only show logged-in user content. 76 | * 77 | * @param array $query_args Arguments to be passed to WP_Query. 78 | * @param array $block_query The query attribute retrieved from the block. 79 | * @param boolean $inherited Whether the query is being inherited. 80 | */ 81 | function aql_extension_show_current_author_only( $query_args, $block_query, $inherited ) { 82 | if ( 83 | isset( $block_query['authorContent'] ) && 84 | true === filter_var( $block_query['authorContent'], FILTER_VALIDATE_BOOLEAN ) 85 | ) { 86 | $query_args['author'] = get_current_user_id(); 87 | } 88 | return $query_args; 89 | } 90 | 91 | \add_filter( 'aql_query_vars', 'aql_extension_show_current_author_only', 10, 3 ); 92 | ``` 93 | 94 | ### Tutorial 95 | 96 | Using he example code above, you can make a custom extension plugin for AQL that will filter the displayed posts by author. 97 | 98 | #### Step 1 99 | 100 | Start by using the `@wordpress/create-block` package to scaffold all of the files we need. We will be removing all of the block-related ones but this tool can quickly get us set up and ready to go. 101 | 102 | The the following in the command line tool of your choice inside the wp-content folder of a local WordPress installation. 103 | 104 | ```bash 105 | npx @wordpress/create-block custom-aql-extension 106 | 107 | ``` 108 | 109 | #### Step 2 110 | 111 | Once the scaffold has been completed, delete all of the files in `custom-aql-extension/src` we don't need them. 112 | 113 | #### Step 3 114 | 115 | Create a new files called`webpack.config.js` in the root of the directory with with the following contents: 116 | 117 | ```js 118 | // Import the original config from the @wordpress/scripts package. 119 | const defaultConfig = require("@wordpress/scripts/config/webpack.config"); 120 | 121 | // Add any a new entry point by extending the webpack config. 122 | module.exports = { 123 | ...defaultConfig, 124 | entry: { 125 | `aql-extension`: './src/index.js 126 | }, 127 | }; 128 | ``` 129 | 130 | #### Step 4 131 | 132 | Create an `index.js` file inside of the `./src` directory with the following contents: 133 | 134 | ```js 135 | const { AQLControls, AQLControlsInheritedQuery } = window.aql; 136 | import { registerPlugin } from '@wordpress/plugins'; 137 | import { ToggleControl } from '@wordpress/components'; 138 | import { __ } from '@wordpress/i18n'; 139 | 140 | const LoggedInUserControl = ( { attributes, setAttributes } ) => { 141 | const { query: { authorContent = false } = {} } = attributes; 142 | return ( 143 | <> 144 | { 148 | setAttributes( { 149 | query: { 150 | ...attributes.query, 151 | authorContent: ! authorContent, 152 | }, 153 | } ); 154 | } } 155 | /> 156 | 157 | ); 158 | }; 159 | 160 | registerPlugin( 'aql-extension', { 161 | render: () => { 162 | return ( 163 | <> 164 | 165 | { ( props ) => } 166 | 167 | 168 | { ( props ) => } 169 | 170 | 171 | ); 172 | }, 173 | } ); 174 | ``` 175 | 176 | #### Step 5 177 | 178 | Next open the scaffolded `index.php` file in the root directory and remove the existing code ( be sure to leave the header comments) and add the following to enqueue the JavaScript file. 179 | 180 | ```php 181 | \add_action( 182 | 'enqueue_block_editor_assets', 183 | function () { 184 | $extension_assets_file = plugin_dir_path( __FILE__ ) . 'build/aql-extension.asset.php'; 185 | 186 | if ( file_exists( $extension_assets_file ) ) { 187 | $assets = include $extension_assets_file; 188 | \wp_enqueue_script( 189 | 'aql-extension', 190 | plugin_dir_url( __FILE__ ). 'build/aql-extension.js', 191 | $assets['dependencies'], 192 | $assets['version'], 193 | true 194 | ); 195 | } 196 | } 197 | ); 198 | ``` 199 | 200 | Next, add the following hooks to filter AQL 201 | 202 | ```php 203 | /** 204 | * Add a filter to only show logged-in user content. 205 | * 206 | * @param array $query_args Arguments to be passed to WP_Query. 207 | * @param array $block_query The query attribute retrieved from the block. 208 | * @param boolean $inherited Whether the query is being inherited. 209 | */ 210 | function aql_extension_show_current_author_only( $query_args, $block_query, $inherited ) { 211 | if ( 212 | isset( $block_query['authorContent'] ) && 213 | true === filter_var( $block_query['authorContent'], FILTER_VALIDATE_BOOLEAN ) 214 | ) { 215 | $query_args['author'] = get_current_user_id(); 216 | } 217 | return $query_args; 218 | } 219 | 220 | \add_filter( 'aql_query_vars', 'aql_extension_show_current_author_only', 10, 3 ); 221 | ``` 222 | 223 | #### Step 6 224 | 225 | Now that all the code is in place, back in your terminal run the one of the following commands: 226 | 227 | - `npm run start` - starts development mode for the plugin that will rebuild when things change 228 | - `npm run build` - creates a production build of the plugin 229 | 230 | Before you can use the plugin you need to run of the two 231 | 232 | #### Step 7 233 | 234 | Enable the plugin in your local WordPress environment 235 | -------------------------------------------------------------------------------- /includes/Query_Params_Generator.php: -------------------------------------------------------------------------------- 1 | 'multiple_posts', 29 | 'taxonomy_query_builder' => 'tax_query', 30 | 'post_meta_query' => 'meta_query', 31 | 'post_order' => 'post_order', 32 | 'exclude_current_post' => 'exclude_current', 33 | 'include_posts' => 'include_posts', 34 | 'child_items_only' => 'child_items_only', 35 | 'date_query_dynamic_range' => 'date_query', 36 | 'date_query_relationship' => 'date_query', 37 | 'pagination' => 'disable_pagination', 38 | ); 39 | 40 | 41 | /** 42 | * Default values from the default block. 43 | * 44 | * @var array 45 | */ 46 | private array $default_params; 47 | 48 | /** 49 | * Custom values from AQL 50 | * 51 | * @var array 52 | */ 53 | private array $custom_params; 54 | 55 | /** 56 | * Customized params to return 57 | * 58 | * @var array 59 | */ 60 | private array $custom_args = array(); 61 | 62 | /** 63 | * Construct method 64 | * 65 | * @param array $default_params Default values from the default block. 66 | * @param array $custom_params Custom values from AQL. 67 | */ 68 | public function __construct( $default_params, $custom_params ) { 69 | $this->default_params = is_array( $default_params ) ? $default_params : array(); 70 | $this->custom_params = is_array( $custom_params ) ? $custom_params : array(); 71 | } 72 | 73 | /** 74 | * Checks to see if the item that is passed is a post ID. 75 | * 76 | * This is used to check if the user is editing a template 77 | * 78 | * @param mixed $possible_post_id The potential post id 79 | * 80 | * @return bool Whether the passed item is a post id or not. 81 | */ 82 | private function is_post_id( $possible_post_id ) { 83 | return is_int( $possible_post_id ) || ! preg_match( '/[a-z\-]+\/\/[a-z\-]+/', $possible_post_id ); 84 | } 85 | 86 | /** 87 | * Check to see if a param is in the list. 88 | * 89 | * @param string $param_name The param to look for. 90 | */ 91 | public function has_custom_param( string $param_name ): bool { 92 | return array_key_exists( $param_name, $this->custom_params ) && ! empty( $this->custom_params[ $param_name ] ); 93 | } 94 | 95 | /** 96 | * Retrieve a single param. 97 | * 98 | * @param string $name The param to retrieve. 99 | * 100 | * @todo Return mixed type hint for 8.0 101 | * 102 | * @return mixed 103 | */ 104 | public function get_custom_param( string $name ) { 105 | if ( $this->has_custom_param( $name ) ) { 106 | return $this->custom_params[ $name ]; 107 | } 108 | return false; 109 | } 110 | /** 111 | * Static function to return the list of allowed controls and their associated params in the query. 112 | * 113 | * @return array 114 | */ 115 | public static function get_allowed_controls() { 116 | return \apply_filters( 'aql_allowed_controls', array_keys( self::ALLOWED_CONTROLS ) ); 117 | } 118 | 119 | protected function get_params_to_process() { 120 | $params = array(); 121 | foreach ( self::get_allowed_controls() as $control ) { 122 | $params[] = self::ALLOWED_CONTROLS[ $control ]; 123 | } 124 | return $params; 125 | } 126 | 127 | /** 128 | * Process all params at once. 129 | */ 130 | public function process_all(): void { 131 | // Get the params from the allowed controls and remove any duplicates. 132 | $params = array_unique( $this->get_params_to_process() ); 133 | foreach ( $params as $param_name ) { 134 | if ( $this->has_custom_param( $param_name ) ) { 135 | call_user_func( array( $this, 'process_' . $param_name ) ); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Retrieve the custom args 142 | */ 143 | public function get_query_args(): array { 144 | return $this->custom_args; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /includes/Traits/Date_Query.php: -------------------------------------------------------------------------------- 1 | custom_params['date_query'] ?? null; 19 | 20 | // Ranges and Relationships can't co-exist. 21 | $range = $date_query['range'] ?? false; 22 | 23 | if ( $date_query && $range && ! empty( $range ) ) { 24 | $inclusive_range = isset( $date_query['current_date_in_range'] ) ? ( true === $date_query['current_date_in_range'] || 'true' === $date_query['current_date_in_range'] ) : false; 25 | $date_queries = $this->process_date_range( $range, $inclusive_range ); 26 | } else { 27 | $date_queries = array(); 28 | $date_relationship = $date_query['relation'] ?? null; 29 | $date_primary = $date_query['date_primary'] ?? null; 30 | 31 | if ( $date_query && $date_relationship ) { 32 | 33 | if ( 'before-current' === $date_relationship || 'after-current' === $date_relationship ) { 34 | switch ( $date_relationship ) { 35 | case 'before-current': 36 | $date_queries = $this->show_before_current_date(); 37 | break; 38 | case 'after-current': 39 | $date_queries = $this->show_after_current_date(); 40 | break; 41 | } 42 | } elseif ( $date_primary ) { 43 | $date_is_inclusive = $date_query['inclusive'] ?? false; 44 | $date_secondary = $date_query['date_secondary'] ?? null; 45 | 46 | // Date format: 2022-12-27T11:14:21. 47 | $primary_year = substr( $date_primary, 0, 4 ); 48 | $primary_month = substr( $date_primary, 5, 2 ); 49 | $primary_day = substr( $date_primary, 8, 2 ); 50 | 51 | if ( 'between' === $date_relationship && $date_secondary ) { 52 | $secondary_year = substr( $date_secondary, 0, 4 ); 53 | $secondary_month = substr( $date_secondary, 5, 2 ); 54 | $secondary_day = substr( $date_secondary, 8, 2 ); 55 | 56 | $date_queries = array( 57 | 'after' => array( 58 | 'year' => $primary_year, 59 | 'month' => $primary_month, 60 | 'day' => $primary_day, 61 | ), 62 | 'before' => array( 63 | 'year' => $secondary_year, 64 | 'month' => $secondary_month, 65 | 'day' => $secondary_day, 66 | ), 67 | ); 68 | } else { 69 | $date_queries = array( 70 | $date_relationship => array( 71 | 'year' => $primary_year, 72 | 'month' => $primary_month, 73 | 'day' => $primary_day, 74 | ), 75 | ); 76 | } 77 | $date_queries['inclusive'] = $date_is_inclusive; 78 | } 79 | } 80 | } 81 | 82 | // Return the date queries. 83 | $this->custom_args['date_query'] = array_filter( $date_queries ); 84 | } 85 | 86 | /** 87 | * Generate the query to only show content before the current date 88 | */ 89 | public function show_before_current_date() { 90 | $today = strtotime( 'today' ); 91 | // Return the date query. 92 | return array( 93 | 'before' => array( 94 | 'year' => gmdate( 'Y', $today ), 95 | 'month' => gmdate( 'm', $today ), 96 | 'day' => gmdate( 'd', $today ), 97 | ), 98 | ); 99 | } 100 | 101 | /** 102 | * Generate the query to only show content after the current date 103 | */ 104 | public function show_after_current_date() { 105 | $today = strtotime( 'today' ); 106 | // Return the date query. 107 | return array( 108 | 'after' => array( 109 | 'year' => gmdate( 'Y', $today ), 110 | 'month' => gmdate( 'm', $today ), 111 | 'day' => gmdate( 'd', $today ), 112 | ), 113 | ); 114 | } 115 | 116 | /** 117 | * Generate the date ranges data 118 | * 119 | * @param string $range The range as provided by the UI. 120 | * @param bool $inclusive_range Does the range end at the current date. 121 | */ 122 | public function process_date_range( string $range, bool $inclusive_range = false ) { 123 | 124 | switch ( $range ) { 125 | case 'last-month': 126 | $months_offset = '-1'; 127 | break; 128 | case 'three-months': 129 | $months_offset = '-3'; 130 | break; 131 | case 'six-months': 132 | $months_offset = '-6'; 133 | break; 134 | case 'twelve-months': 135 | $months_offset = '-12'; 136 | break; 137 | } 138 | // Get the dates for the first and last day of the month offset. 139 | $today = strtotime( 'today' ); 140 | $after = strtotime( "first day of {$months_offset} months" ); 141 | $before = strtotime( 'last day of last month' ); 142 | 143 | // Are we add the current date? 144 | $range_to_use = $inclusive_range ? $today : $before; 145 | 146 | // Return the date query. 147 | $date_query = array( 148 | 'before' => array( 149 | 'year' => gmdate( 'Y', $range_to_use ), 150 | 'month' => gmdate( 'm', $range_to_use ), 151 | 'day' => gmdate( 'd', $range_to_use ), 152 | ), 153 | 'after' => array( 154 | 'year' => gmdate( 'Y', $after ), 155 | 'month' => gmdate( 'm', $after ), 156 | 'day' => gmdate( 'd', $after ), 157 | ), 158 | ); 159 | 160 | return $date_query; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /includes/Traits/Disable_Pagination.php: -------------------------------------------------------------------------------- 1 | custom_args['no_found_rows'] = $this->get_custom_param( 'disable_pagination' ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /includes/Traits/Exclude_Current.php: -------------------------------------------------------------------------------- 1 | custom_args['post__not_in'] = $this->get_exclude_ids( $this->custom_params['exclude_current'] ); 18 | } 19 | 20 | /** 21 | * Helper to generate the array 22 | * 23 | * @param mixed $to_exclude The value to be excluded. 24 | * 25 | * @return array The ids to exclude 26 | */ 27 | public function get_exclude_ids( $to_exclude ) { 28 | // If there are already posts to be excluded, we need to add to them. 29 | $exclude_ids = $this->custom_args['post__not_in'] ?? array(); 30 | 31 | if ( $this->is_post_id( $to_exclude ) ) { 32 | array_push( $exclude_ids, intval( $to_exclude ) ); 33 | } else { 34 | // This is usually when this was set on a template. 35 | global $post; 36 | if ( $post ) { 37 | array_push( $exclude_ids, $post->ID ); 38 | } 39 | } 40 | return $exclude_ids; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /includes/Traits/Exclude_Taxonomies.php: -------------------------------------------------------------------------------- 1 | custom_params['exclude_taxonomies']; 18 | if( count( $taxonomies_to_exclude ) ) { 19 | $tax_query = []; 20 | foreach ( $taxonomies_to_exclude as $slug ) { 21 | $tax_query[] = [ 22 | 'taxonomy' => $slug, 23 | 'operator' => 'NOT EXISTS' 24 | ]; 25 | } 26 | $this->custom_args['tax_query'] = $tax_query; 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /includes/Traits/Include_Posts.php: -------------------------------------------------------------------------------- 1 | custom_args['post__in'] = $this->get_include_ids( $this->get_custom_param( 'include_posts' ) ); 12 | } 13 | 14 | /** 15 | * Returns an array with Post IDs to be included on the Query 16 | * 17 | * @param array $include_posts Array of posts to include. 18 | * 19 | * @return array 20 | */ 21 | protected function get_include_ids( $include_posts ) { 22 | if ( is_array( $include_posts ) ) { 23 | return array_column( $include_posts, 'id' ); 24 | } 25 | return array(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /includes/Traits/Meta_Query.php: -------------------------------------------------------------------------------- 1 | custom_args['meta_query'] = $this->parse_meta_query( $this->custom_params['meta_query'] ); 12 | } 13 | 14 | public function parse_meta_query( $meta_query_data ) { 15 | $meta_queries = array(); 16 | if ( isset( $meta_query_data ) ) { 17 | $meta_queries = array( 18 | 'relation' => isset( $meta_query_data['relation'] ) ? $meta_query_data['relation'] : '', 19 | ); 20 | 21 | if ( isset( $meta_query_data['queries'] ) ) { 22 | foreach ( $meta_query_data['queries'] as $query ) { 23 | $meta_queries[] = array_filter( 24 | array( 25 | 'key' => $query['meta_key'] ?? '', 26 | 'value' => $query['meta_value'] ?? '', 27 | 'compare' => $query['meta_compare'] ?? '', 28 | ) 29 | ); 30 | } 31 | } 32 | } 33 | 34 | return array_filter( $meta_queries ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /includes/Traits/Multiple_Posts.php: -------------------------------------------------------------------------------- 1 | custom_args['post_type'] = array_unique( 17 | array_merge( 18 | (array) $this->default_params['post_type'], 19 | (array) $this->custom_params['multiple_posts'] 20 | ), 21 | SORT_REGULAR 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /includes/Traits/Post_Parent.php: -------------------------------------------------------------------------------- 1 | custom_params['post_parent']; 18 | 19 | if ( $this->is_post_id( $parent ) ) { 20 | $this->custom_args['post_parent'] = $parent; 21 | } else { 22 | // This is usually when this was set on a template. 23 | global $post; 24 | $this->custom_args['post_parent'] = $post->ID; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /includes/Traits/Tax_Query.php: -------------------------------------------------------------------------------- 1 | custom_args['tax_query'] = $this->parse_tax_query( $this->custom_params['tax_query'] ); 12 | } 13 | 14 | public function parse_tax_query( $queries ) { 15 | $tax_query = array(); 16 | // Don't process empty array of queries. 17 | if ( isset( $queries['queries'] ) && count( $queries['queries'] ) > 0 ) { 18 | // Handle the relation parameter. 19 | if ( isset( $queries['relation'] ) && count( $queries['queries'] ) > 1 ) { 20 | $tax_query['relation'] = $queries['relation']; 21 | } 22 | // Loop the queries 23 | foreach ( $queries['queries'] as $query ) { 24 | if ( isset( $query['taxonomy'] ) && isset( $query['terms'] ) && count( $query['terms'] ) > 0 ) { 25 | $processed_query = array_filter( $query, fn( $key ) => 'id' !== $key, ARRAY_FILTER_USE_KEY ); 26 | $processed_query['include_children'] = filter_var( $query['include_children'], FILTER_VALIDATE_BOOLEAN ); 27 | $processed_query['terms'] = array_filter( 28 | array_map( 29 | function ( $term ) use ( $query ) { 30 | $term_obj = get_term_by( 'name', $term, $query['taxonomy'] ); 31 | return $term_obj ? $term_obj->term_id : null; 32 | }, 33 | $query['terms'] 34 | ) 35 | ); 36 | $tax_query[] = $processed_query; 37 | } 38 | } 39 | } 40 | return $tax_query; 41 | } 42 | } 43 | 44 | /** 45 | * Example complex query: 46 | * $tax_query = array( 47 | * 'relation' => 'OR', 48 | * array( 49 | * 'taxonomy' => 'category', 50 | * 'field' => 'slug', 51 | * 'terms' => array( 'quotes' ), 52 | * ), 53 | * array( 54 | * 'taxonomy' => 'tag', 55 | * 'field' => 'slug', 56 | * 'terms' => array( 2 ), 57 | * ), 58 | * array( 59 | * 'relation' => 'AND', 60 | * array( 61 | * 'taxonomy' => 'post_format', 62 | * 'field' => 'slug', 63 | * 'terms' => array( 'post-format-quote' ), 64 | * ), 65 | * array( 66 | * 'taxonomy' => 'category', 67 | * 'field' => 'slug', 68 | * 'terms' => array( 'wisdom' ), 69 | * ), 70 | * ), 71 | * ); 72 | */ 73 | -------------------------------------------------------------------------------- /includes/enqueues.php: -------------------------------------------------------------------------------- 1 | query_vars, 28 | array( 29 | 'posts_per_page' => $parsed_block['attrs']['query']['perPage'], 30 | 'order' => $parsed_block['attrs']['query']['order'], 31 | 'orderby' => $parsed_block['attrs']['query']['orderBy'], 32 | ) 33 | ); 34 | 35 | /** 36 | * Filter the query vars. 37 | * 38 | * Allows filtering query params when the query is being inherited. 39 | * 40 | * @since 1.5 41 | * 42 | * @param array $query_args Arguments to be passed to WP_Query. 43 | * @param array $block_query The query attribute retrieved from the block. 44 | * @param boolean $inherited Whether the query is being inherited. 45 | * 46 | * @param array $filtered_query_args Final arguments list. 47 | */ 48 | $filtered_query_args = \apply_filters( 49 | 'aql_query_vars', 50 | $query_args, 51 | $parsed_block['attrs']['query'], 52 | true, 53 | ); 54 | 55 | $wp_query = new \WP_Query( array_filter( $filtered_query_args ) ); 56 | } else { 57 | \add_filter( 58 | 'query_loop_block_query_vars', 59 | function ( $default_query, $block ) { 60 | // Retrieve the query from the passed block context. 61 | $block_query = $block->context['query'] ?? array(); 62 | 63 | // Process all of the params 64 | $qpg = new Query_Params_Generator( $default_query, $block_query ); 65 | $qpg->process_all(); 66 | $query_args = $qpg->get_query_args(); 67 | 68 | /** This filter is documented in includes/query-loop.php */ 69 | $filtered_query_args = \apply_filters( 70 | 'aql_query_vars', 71 | $query_args, 72 | $block_query, 73 | false 74 | ); 75 | 76 | // Return the merged query. 77 | return array_merge( 78 | $default_query, 79 | $filtered_query_args 80 | ); 81 | }, 82 | 10, 83 | 2 84 | ); 85 | } 86 | } 87 | 88 | return $pre_render; 89 | }, 90 | 10, 91 | 2 92 | ); 93 | 94 | /** 95 | * Updates the query vars for the Query Loop block in the block editor 96 | */ 97 | // Add a filter to each rest endpoint to add our custom query params. 98 | \add_action( 99 | 'init', 100 | function () { 101 | $registered_post_types = \get_post_types( array( 'show_in_rest' => true, 'publicly_queryable' => true ) ); 102 | foreach ( $registered_post_types as $registered_post_type ) { 103 | \add_filter( 'rest_' . $registered_post_type . '_query', __NAMESPACE__ . '\add_custom_query_params', 10, 2 ); 104 | 105 | // We need more sortBy options. 106 | \add_filter( 'rest_' . $registered_post_type . '_collection_params', __NAMESPACE__ . '\add_more_sort_by', 10, 2 ); 107 | } 108 | }, 109 | PHP_INT_MAX 110 | ); 111 | 112 | 113 | /** 114 | * Override the allowed items 115 | * 116 | * @see https://developer.wordpress.org/reference/classes/wp_rest_posts_controller/get_collection_params/ 117 | * 118 | * @param array $query_params The query params. 119 | * @param array $post_type The post type. 120 | * 121 | * @return array 122 | */ 123 | function add_more_sort_by( $query_params ) { 124 | $query_params['orderby']['enum'][] = 'menu_order'; 125 | $query_params['orderby']['enum'][] = 'meta_value'; 126 | $query_params['orderby']['enum'][] = 'meta_value_num'; 127 | $query_params['orderby']['enum'][] = 'rand'; 128 | $query_params['orderby']['enum'][] = 'post__in'; 129 | $query_params['orderby']['enum'][] = 'comment_count'; 130 | $query_params['orderby']['enum'][] = 'name'; 131 | return $query_params; 132 | } 133 | 134 | /** 135 | * Callback to handle the custom query params. Updates the block editor. 136 | * 137 | * @param array $args The query args. 138 | * @param WP_REST_Request $request The request object. 139 | */ 140 | function add_custom_query_params( $args, $request ) { 141 | 142 | // Process all of the params 143 | $qpg = new Query_Params_Generator( $args, $request->get_params() ); 144 | $qpg->process_all(); 145 | $query_args = $qpg->get_query_args(); 146 | 147 | /** This filter is documented in includes/query-loop.php */ 148 | $filtered_query_args = \apply_filters( 149 | 'aql_query_vars', 150 | $query_args, 151 | $request->get_params(), 152 | false, 153 | ); 154 | // Merge all queries. 155 | $merged = array_merge( 156 | $args, 157 | array_filter( $filtered_query_args ) 158 | ); 159 | 160 | return $merged; 161 | } 162 | -------------------------------------------------------------------------------- /includes/taxonomy.php: -------------------------------------------------------------------------------- 1 | term_id; 16 | } 17 | } 18 | return $rtn; 19 | } 20 | 21 | function parse_taxonomy_query( $tax_query_data ) { 22 | return [ 23 | [ 24 | 'taxonomy' => $tax_query_data['taxonomy'], 25 | 'terms' => convert_names_to_ids( $tax_query_data['terms'], $tax_query_data['taxonomy'] ), 26 | 'include_children' => ( ! isset( $tax_query_data['include_children'] ) || 'true' === $tax_query_data['include_children'] ) ? true : false, 27 | 'operator' => $tax_query_data['operator'], 28 | ], 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /includes/utilities.php: -------------------------------------------------------------------------------- 1 | =' ); 21 | } 22 | return false; 23 | } 24 | 25 | /** 26 | * Helper to determine is the current WP install is at or higher than a given version. 27 | * 28 | * @param string $version The version to check for. 29 | 30 | * @return boolean. 31 | */ 32 | function is_core_version_or_higher( string $version ) { 33 | $core = get_bloginfo( 'version' ); 34 | return version_compare( $core, $version, '>=' ); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | A base ruleset that all other 10up rulesets should extend. 4 | 5 | */phpunit.xml* 6 | */dist/* 7 | */languages/* 8 | 9 | 10 | */bower-components/* 11 | */node_modules/* 12 | */vendor/* 13 | 14 | 15 | *\.(css|js) 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | 0 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | warning 81 | 82 | 83 | 84 | 85 | warning 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests/unit 16 | 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Advanced Query Loop 2 | 3 | ![](https://github.com/ryanwelcher/advanced-query-loop/actions/workflows/phpunit.yml/badge.svg?branch=trunk) 4 | 5 | ## Description 6 | 7 | This plugin introduces a Query Loop block variation that will empower users to be able to do much more complicated queries with the Query Loop block, such number of posts to display and post meta 8 | 9 | ### Support/Issues 10 | 11 | Please use the either the [support](https://wordpress.org/support/plugin/advanced-query-loop/) forum or the [official repository](https://github.com/ryanwelcher/advanced-query-loop) for any questions or to log issues. 12 | 13 | ### Available Controls 14 | 15 | #### Taxonomy queries 16 | 17 | Built complicated taxonomy queries! 18 | 19 | #### Multiple post types 20 | 21 | Select additional post types for your query! 22 | 23 | #### Include Posts 24 | 25 | Choose the posts you want to display manually or only the children of the current content. 26 | 27 | #### Exclude current post 28 | 29 | Remove the current post from the query. 30 | 31 | #### Exclude posts by category 32 | 33 | Choose to exclude posts from a list of categories. 34 | 35 | #### Post Meta Query 36 | 37 | Generate complicated post meta queries using an interface that allows you to create a query based on `meta_key`, `meta_value` and the `compare` options. Combine multiple queries and determine if they combine results (OR) or narrow them down (AND). 38 | 39 | #### Date Query 40 | 41 | Query items before/after the current or selected or choose to show the post from the last 1, 3, 6 and 12 months. 42 | 43 | #### Post Order controls 44 | 45 | Sort in ascending or descending order by: 46 | 47 | - Author 48 | - Date 49 | - Last Modified Date 50 | - Title 51 | - Meta Value 52 | - Meta Value Num 53 | - Random 54 | - Menu Order (props to @jvanja) 55 | - Name (props @philbee) 56 | - Post ID (props to @markhowellsmead) 57 | 58 | **Please note that this is a slight duplication of the existing sorting controls. They both work interchangeably but it just looks a bit odd in the UI** 59 | 60 | #### Disable Pagination 61 | 62 | Improve the performance of the query by disabling pagination. This is done automatically when there is now Pagination block in teh Post Template. 63 | 64 | ## Filtering the available controls 65 | 66 | It is possible to remove controls from AQL using the `aql_allowed_controls` filter. The filter receives a single parameter containing an array of allowed controls. This can be modified to remove the control from the UI and stop processing the associated query param. 67 | 68 | ```php 69 | add_filter( 70 | 'aql_allowed_controls', 71 | function( $controls ) { 72 | // Exclude the additional_post_types and taxonomy_query_builder controls. 73 | $to_exclude = array( 'additional_post_types', 'taxonomy_query_builder' ); 74 | $filtered_controls = array_filter( 75 | $controls, 76 | function( $control ) use ( $to_exclude ) { 77 | if ( ! in_array( $control, $to_exclude, true ) ) { 78 | return $control; 79 | } 80 | }, 81 | ); 82 | return $filtered_controls; 83 | } 84 | ); 85 | ``` 86 | 87 | ### List of control identifiers 88 | 89 | - `'additional_post_types'` 90 | - `'taxonomy_query_builder'` 91 | - `'post_meta_query'` 92 | - `'post_order'` 93 | - `'exclude_current_post'` 94 | - `'include_posts'` 95 | - `'child_items_only'` 96 | - `'date_query_dynamic_range'` 97 | - `'date_query_relationship'` 98 | - `'pagination'` 99 | 100 | ## Extending AQL 101 | 102 | Detailed instructions on how to extend AQL as well as an example are available [here](./extending-aql.md) 103 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Advanced Query Loop === 2 | Contributors: welcher 3 | Tags: Query Loop, Custom Queries, Advanced Queries, Post Meta, Taxonomy, Date Queries 4 | Requires at least: 6.2 5 | Tested up to: 6.8.1 6 | Stable tag: 4.2.0 7 | Requires PHP: 7.4 8 | License: GPL v2 or later 9 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | Transform your Query Loop blocks into powerful, flexible content engines! 🚀 12 | 13 | == Description == 14 | 15 | **Supercharge your queries without any code** 16 | 17 | Tired of the limitations of standard Query Loop blocks? Advanced Query Loop gives you the superpowers you need to create sophisticated, dynamic content queries that go far beyond the basics. Whether you're building a portfolio, news site, or complex content hub, this plugin puts you in complete control of your content display. 18 | 19 | **What makes Advanced Query Loop special?** 20 | 21 | * **No coding required** - Everything works through an intuitive visual interface 22 | * **Powerful query building** - Create complex queries that would normally require custom code 23 | * **Flexible and extensible** - Built with developers in mind, but accessible to everyone 24 | * **Performance optimized** - Smart caching and efficient queries keep your site fast 25 | 26 | === Support & Community === 27 | 28 | Need help? We've got you covered! 29 | 30 | * **WordPress.org Support Forum**: [Get help here](https://wordpress.org/support/plugin/advanced-query-loop/) 31 | * **GitHub Repository**: [Report issues & contribute](https://github.com/ryanwelcher/advanced-query-loop) 32 | 33 | === Powerful Features at Your Fingertips === 34 | 35 | ==== 🏷️ Advanced Taxonomy Queries ==== 36 | 37 | Build sophisticated taxonomy queries that let you filter content by multiple categories, tags, or custom taxonomies. Create complex relationships between different taxonomy terms to display exactly the content you want. 38 | 39 | ==== 📝 Multiple Post Types ==== 40 | 41 | Don't limit yourself to just posts! Query across multiple post types simultaneously. Perfect for portfolios, news sites, or any site that needs to display different types of content together. 42 | 43 | ==== 🎯 Smart Post Inclusion ==== 44 | 45 | Take full control over which posts appear in your query: 46 | * **Manual selection**: Choose specific posts by title or ID 47 | * **Child items only**: Show only child posts of the current content 48 | * **Dynamic filtering**: Combine multiple inclusion rules 49 | 50 | ==== 🚫 Intelligent Post Exclusion ==== 51 | 52 | Keep your queries clean and relevant: 53 | * **Exclude current post**: Automatically hide the post being viewed 54 | * **Category filtering**: Exclude posts from specific categories 55 | * **Smart defaults**: Built-in logic to avoid duplicate content 56 | 57 | ==== 🔍 Advanced Post Meta Queries ==== 58 | 59 | Create powerful meta queries without touching code: 60 | * **Multiple conditions**: Combine different meta fields and values 61 | * **Flexible comparisons**: Use equals, not equals, greater than, less than, and more 62 | * **Logical operators**: Combine queries with AND/OR logic 63 | * **ACF integration**: Works seamlessly with Advanced Custom Fields 64 | 65 | ==== 📅 Dynamic Date Queries ==== 66 | 67 | Time-based content has never been easier: 68 | * **Relative dates**: Show content from last 1, 3, 6, or 12 months 69 | * **Before/after current**: Display content relative to the current date 70 | * **Custom date ranges**: Set specific start and end dates 71 | * **Multiple date conditions**: Combine different date rules 72 | 73 | ==== 📊 Flexible Sorting Options ==== 74 | 75 | Sort your content exactly how you want: 76 | * **Author**: Sort by post author 77 | * **Date**: Sort by publication date 78 | * **Last Modified**: Sort by last update 79 | * **Title**: Alphabetical sorting 80 | * **Meta Values**: Sort by custom field values 81 | * **Random**: Shuffle your content 82 | * **Menu Order**: Use custom ordering 83 | * **Name**: Sort by post slug 84 | * **Post ID**: Sort by post ID 85 | * **Comment Count**: Sort by engagement 86 | 87 | ==== ⚡ Performance Optimization ==== 88 | 89 | * **Smart pagination**: Automatically disable pagination when not needed 90 | * **Efficient queries**: Optimized database queries for better performance 91 | * **Caching friendly**: Works seamlessly with popular caching plugins 92 | 93 | === Customization & Extensibility === 94 | 95 | ==== Filter Available Controls ==== 96 | 97 | Don't need all the features? No problem! You can easily hide specific controls using the `aql_allowed_controls` filter: 98 | 99 | ` 100 | add_filter( 101 | 'aql_allowed_controls', 102 | function( $controls ) { 103 | // Remove specific controls you don't need 104 | $to_exclude = array( 'additional_post_types', 'taxonomy_query_builder' ); 105 | return array_filter( $controls, function( $control ) use ( $to_exclude ) { 106 | return ! in_array( $control, $to_exclude, true ); 107 | } ); 108 | } 109 | ); 110 | ` 111 | 112 | ==== Available Control Identifiers ==== 113 | 114 | * `'additional_post_types'` - Multiple post type selection 115 | * `'taxonomy_query_builder'` - Advanced taxonomy queries 116 | * `'post_meta_query'` - Meta field queries 117 | * `'post_order'` - Sorting options 118 | * `'exclude_current_post'` - Current post exclusion 119 | * `'include_posts'` - Manual post inclusion 120 | * `'child_items_only'` - Child post filtering 121 | * `'date_query_dynamic_range'` - Date range queries 122 | * `'date_query_relationship'` - Date query logic 123 | * `'pagination'` - Pagination controls 124 | 125 | ==== Developer-Friendly ==== 126 | 127 | Advanced Query Loop is built with developers in mind: 128 | * **Extensible architecture**: Add your own custom controls 129 | * **Well-documented hooks**: Easy integration with your themes and plugins 130 | * **Clean code**: Follows WordPress coding standards 131 | * **Comprehensive testing**: Thoroughly tested for reliability 132 | 133 | === Getting Started === 134 | 135 | 1. **Install and activate** the plugin 136 | 2. **Add a Query Loop block** to your page or post 137 | 3. **Look for the "Advanced Query Loop" variation** in the block inserter 138 | 4. **Configure your query** using the intuitive controls 139 | 5. **Preview and publish** your dynamic content! 140 | 141 | === Perfect For === 142 | 143 | * **Portfolio websites** - Showcase work with sophisticated filtering 144 | * **News and magazine sites** - Display content by category, date, and more 145 | * **E-commerce sites** - Filter products by custom fields and taxonomies 146 | * **Educational platforms** - Organize content by course, level, or topic 147 | * **Real estate sites** - Filter properties by location, price, and features 148 | * **Any site needing advanced content queries** - The possibilities are endless! 149 | 150 | == Screenshots == 151 | 152 | 1. Select how many posts you want to display and the number to start at. 153 | 2. Create complicated queries for post types with registered post meta.x 154 | 3. Query posts before a date, after a date or between two dates. 155 | 156 | == Changelog == 157 | = 4.2.0= 158 | * Fix taxonomy pagination limit (props @NickOrtiz). 159 | * Allow controls to be filtered at the code level. 160 | 161 | = 4.1.2= 162 | * Harden up the code to remove a warning. 163 | * Resurrect the disable pagination toggl 164 | 165 | = 4.1.1= 166 | * Allow extended orderby values for all publicly queryable post types (props @ocean90) 167 | * Decode entities in the FormTokenField for post inclusion. 168 | * Fix post type merge issue to retain default post type on frontend (props @mehidi258) 169 | 170 | = 4.1.0= 171 | * The control for Pagination controls has been removed and now is automatically enabled/disabled based whether the Pagination block is in the template. 172 | * Bug fixes. 173 | 174 | = 4.0.2= 175 | * Bug fixes 176 | 177 | = 4.0.1 = 178 | * A few small bug fixes courtesy of @gvgvgvijayan 179 | 180 | = 4.0.0 = 181 | * Introducing the new Taxonomy Builder! 182 | * Show children of current item only. 183 | * Adds before and after current date controls 184 | * Clean up the UI. 185 | 186 | = 3.2.0 = 187 | * Adds the ability to exclude posts by category (props @ghost-ng) 188 | * Adds the ability to disable pagination. 189 | * Deprecate controls that were moved into the Query Loop block in Gutenberg 19. 190 | * Fix fatal error when post include array was empty. 191 | 192 | = 3.1.1 = 193 | * Add better SVG that works in all usages 194 | * Change ranges to allow to not include the current date 195 | * Trim whitespace from title.rendered 196 | 197 | = 3.1.0 = 198 | * Add dynamic date ranges to see posts from the last 1, 3, 6 and 12 months. 199 | * Insert a new instance by typing "AQL" or "aql" and pressing enter. 200 | * Adds sorting by Name (props @philbee). 201 | * Bug fixes. 202 | 203 | = 3.0.1 = 204 | * Addresses some PHP fatal errors caused by type hinting. 205 | 206 | = 3.0.0 = 207 | * Add Sorting by Included Posts IDs. 208 | * Add sorting by Comment Count. 209 | * Major restructure for processing the query params. 210 | * Add release-drafter workflow. 211 | 212 | = 2.2.5 = 213 | * Fixes issue with Exclude Current Post not being correctly set on templates. 214 | 215 | = 2.2.4 = 216 | * Fixes an issue with the Exclude Current Post toggle causing the block to crash in some circumstances 217 | 218 | = 2.2.3 = 219 | * Adds a Include Posts tool to allow manual curation of content to display (@props jenniferfarhat) 220 | 221 | = 2.1.3 = 222 | * Fixes issues in PHP 8 and the date query. (props @krokodok) 223 | 224 | = 2.1.2 = 225 | * Fixes issue with empty search parameter causing incorrect template to load (props @StreetDog71) 226 | * Fixes issue with all post type not being loaded ( props @aaronware) 227 | 228 | = 2.1.1 = 229 | * Fixes issue with multiple AQL instances having settings leaked to each other. 230 | 231 | = 2.1.0 = 232 | * ACF custom fields now show in the auto-complete dropdown list for Post Meta Queries ( props to @jvanja ) 233 | * Adds sort by Post ID ( props to @markhowellsmead ) 234 | * Fixes a typo in the Order By label. 235 | * Fixes a bug where a second AQL instances was getting post meta query values from the first. 236 | 237 | = 2.0.0 = 238 | * Due to a change in core, Post Count can no longer be overridden when the block is set to inherit the query. 239 | * Adds Exclude current post option. Props to @Pulsar-X 240 | * Bump Tested Up To for 6.4 241 | * Adds better instructions for creating extension plugins. 242 | 243 | = 1.5.1 = 244 | * Adds EXISTS as a compare option for Post Meta Queries. 245 | 246 | = 1.5 = 247 | * Moves all controls into a single panel titled "Advanced Query Settings". 248 | * Exposes SlotFills and filters to allow extension of plugin to add any featured you want. 249 | * Minor PHP warning fixes. 250 | 251 | = 1.4.3 = 252 | * Addresses translations from https://translate.wordpress.org/ not loading. HUGE thank you to @himaartwp for opening the issue and to everyone that helped with the Japanese translation! 253 | * Fixes minor php warnings in the editor 254 | 255 | = 1.4.2 = 256 | * Addresses an issue where `noindex` was being added incorrectly due to an empty parameter. Props to @pedjas for reporting. 257 | * Small fixes to address some PHP warnings. 258 | 259 | = 1.4.1 = 260 | * Small fixes to address some PHP warnings. 261 | 262 | = 1.4.0 = 263 | * Adds Menu Order to the sort by options. Props to @jvanja for the Pull Request * 264 | 265 | = 1.3.0 = 266 | * Adds support for sorting by Meta Value, Meta Value Num, and Random. 267 | * Adds transform to convert existing Query Loop instances into Advanced Query Loop blocks. 268 | * Adds a release command. 269 | * Adds support for querying multiple post types. 270 | 271 | = 1.2.1 = 272 | * Fixes missing controls when not inheriting the query. Props to @cvladan for opening the support thread. 273 | 274 | = 1.2.0 = 275 | * Introduce Post Order controls to sort by Author, Date, Last Modified Date, or Title in ascending or descending order. Props to @asterix for the suggestion of adding Last Modified Date. 276 | * Enable Post Count and Post Order controls even when inheriting the query. 277 | 278 | = 1.1.0 = 279 | * Allow manual input of post meta. Props to @svenl77 for opening the support thread. 280 | 281 | = 1.0.5 = 282 | * PRO TIP: Include the PHP files when you release the plugin :/ 283 | 284 | = 1.0.4 = 285 | * Adds custom icon. 286 | * Under the hood restructuring of code. 287 | 288 | = 1.0.3 = 289 | * Small fix for PHP 8. Props to @markus9312 for opening the support thread. 290 | 291 | = 1.0.2 = 292 | * Fix various PHP notices. Props to @wildworks for opening the support thread. 293 | * Add some information to the readmes. 294 | 295 | = 1.0.1 = 296 | * Small fix to no longer show an empty pattern after inserting the block. 297 | 298 | = 1.0.0 = 299 | * Initial release with support for post count, offset, post meta, and date queries. 300 | -------------------------------------------------------------------------------- /src/components/child-items-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { ToggleControl } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | import { useSelect } from '@wordpress/data'; 7 | import { store as coreStore } from '@wordpress/core-data'; 8 | import { store as editorStore } from '@wordpress/editor'; 9 | 10 | export const ChildItemsToggle = ( { 11 | attributes, 12 | setAttributes, 13 | allowedControls, 14 | } ) => { 15 | const { query: { post_parent: postParent } = {} } = attributes; 16 | 17 | const { isHierarchial, postTypeName, postID } = useSelect( ( select ) => { 18 | const post = select( editorStore ).getCurrentPost(); 19 | const postType = select( editorStore ).getCurrentPostType(); 20 | const postTypeObject = select( coreStore ).getPostType( postType ); 21 | 22 | return { 23 | isHierarchial: postTypeObject?.hierarchical, 24 | postTypeName: postType, 25 | postID: post?.id, 26 | }; 27 | }, [] ); 28 | 29 | // If the control is not allowed, return null. 30 | if ( ! allowedControls.includes( 'child_items_only' ) ) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 45 | setAttributes( { 46 | query: { 47 | ...attributes.query, 48 | post_parent: value ? postID : 0, 49 | }, 50 | } ) 51 | } 52 | /> 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/exclude-taxonomies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { FormTokenField, BaseControl } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | import { useSelect } from '@wordpress/data'; 7 | import { store as coreDataStore } from '@wordpress/core-data'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { prepDataFromTokenField } from '../utils'; 13 | 14 | export const ExcludeTaxonomies = ( { attributes, setAttributes } ) => { 15 | const { 16 | query: { 17 | multiple_posts: multiplePosts = [], 18 | postType, 19 | exclude_taxonomies: excludeTaxonomies = [], 20 | } = {}, 21 | } = attributes; 22 | 23 | const taxonomies = useSelect( 24 | ( select ) => { 25 | const knownTaxes = select( coreDataStore ).getTaxonomies(); 26 | return knownTaxes.filter( 27 | ( { types } ) => 28 | types.includes( postType ) || 29 | types.some( ( i ) => multiplePosts.includes( i ) ) 30 | ); 31 | }, 32 | [ multiplePosts, postType ] 33 | ); 34 | 35 | return ( 36 | 43 | name ) ] } 54 | onChange={ ( selectedTaxonomies ) => { 55 | setAttributes( { 56 | query: { 57 | ...attributes.query, 58 | exclude_taxonomies: 59 | prepDataFromTokenField( 60 | selectedTaxonomies, 61 | taxonomies, 62 | 'name', 63 | 'slug' 64 | ) || [], 65 | }, 66 | } ); 67 | } } 68 | __experimentalExpandOnFocus 69 | __experimentalShowHowTo={ false } 70 | __nextHasNoMarginBottom 71 | /> 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/icons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { SVG, Path } from '@wordpress/primitives'; 5 | 6 | function AQLIcon() { 7 | return ( 8 | 15 | 16 | 20 | 24 | 25 | ); 26 | } 27 | 28 | export default AQLIcon; 29 | -------------------------------------------------------------------------------- /src/components/multiple-post-select.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { FormTokenField, BaseControl } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | import { useSelect } from '@wordpress/data'; 7 | import { store as coreStore } from '@wordpress/core-data'; 8 | 9 | export const MultiplePostSelect = ( { 10 | attributes, 11 | setAttributes, 12 | allowedControls, 13 | } ) => { 14 | const { query: { multiple_posts: multiplePosts = [], postType } = {} } = 15 | attributes; 16 | 17 | const postTypes = useSelect( 18 | ( select ) => 19 | select( coreStore ) 20 | .getPostTypes( { per_page: 50 } ) 21 | ?.filter( ( { viewable } ) => viewable ) 22 | ?.map( ( { slug } ) => slug ), 23 | [] 24 | ); 25 | 26 | // If the control is not allowed, return null. 27 | if ( ! allowedControls.includes( 'additional_post_types' ) ) { 28 | return null; 29 | } 30 | 31 | if ( ! postTypes ) { 32 | return
{ __( 'Loading…', 'advanced-query-loop' ) }
; 33 | } 34 | return ( 35 | 42 | type !== postType ), 46 | ] } 47 | suggestions={ [ 48 | ...postTypes?.filter( ( type ) => type !== postType ), 49 | ] } 50 | onChange={ ( posts ) => { 51 | // filter the tokens to remove wrong items. 52 | setAttributes( { 53 | query: { 54 | ...attributes.query, 55 | multiple_posts: posts || [], 56 | }, 57 | } ); 58 | } } 59 | __experimentalExpandOnFocus 60 | __experimentalShowHowTo={ false } 61 | __nextHasNoMarginBottom 62 | /> 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/pagination-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { ToggleControl } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | export const PaginationToggle = ( { 8 | attributes, 9 | setAttributes, 10 | allowedControls, 11 | } ) => { 12 | const { query: { disable_pagination: disablePagination } = {} } = 13 | attributes; 14 | // If the control is not allowed, return null. 15 | if ( ! allowedControls.includes( 'pagination' ) ) { 16 | return null; 17 | } 18 | 19 | return ( 20 | { 28 | setAttributes( { 29 | query: { 30 | ...attributes.query, 31 | disable_pagination: ! disablePagination, 32 | }, 33 | } ); 34 | } } 35 | __nextHasNoMarginBottom 36 | /> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/post-count-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { RangeControl } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * PostCountControls component 9 | * 10 | * @param {*} param0 11 | * @return {Element} PostCountControls 12 | */ 13 | export const PostCountControls = ( { attributes, setAttributes } ) => { 14 | const { query: { perPage, offset = 0 } = {} } = attributes; 15 | 16 | return ( 17 | { 22 | setAttributes( { 23 | query: { 24 | ...attributes.query, 25 | perPage: newCount, 26 | offset, 27 | }, 28 | } ); 29 | } } 30 | value={ perPage } 31 | /> 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/post-date-query-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { 5 | DatePicker, 6 | SelectControl, 7 | CheckboxControl, 8 | } from '@wordpress/components'; 9 | import { __ } from '@wordpress/i18n'; 10 | 11 | export const PostDateQueryControls = ( { 12 | attributes, 13 | setAttributes, 14 | allowedControls, 15 | } ) => { 16 | const { 17 | query: { 18 | date_query: { 19 | relation: relationFromQuery = '', 20 | date_primary: datePrimary = new Date(), 21 | date_secondary: dateSecondary = new Date(), 22 | inclusive: isInclusive = false, 23 | range = '', 24 | current_date_in_range: currentDateInRange = false, 25 | } = {}, 26 | } = {}, 27 | } = attributes; 28 | return ( 29 | <> 30 |

{ __( 'Post Date Query', 'advanced-query-loop' ) }

31 | { allowedControls.includes( 'date_query_dynamic_range' ) && ( 32 | { 66 | setAttributes( { 67 | query: { 68 | ...attributes.query, 69 | date_query: { 70 | ...attributes.query.date_query, 71 | range: newRange, 72 | }, 73 | }, 74 | } ); 75 | } } 76 | __nextHasNoMarginBottom 77 | /> 78 | ) } 79 | { range !== '' && 80 | allowedControls.includes( 'date_query_dynamic_range' ) && ( 81 | { 93 | setAttributes( { 94 | query: { 95 | ...attributes.query, 96 | date_query: { 97 | ...attributes.query.date_query, 98 | current_date_in_range: 99 | newCurrentDateInRange, 100 | }, 101 | }, 102 | } ); 103 | } } 104 | /> 105 | ) } 106 | { allowedControls.includes( 'date_query_relationship' ) && ( 107 | { 161 | setAttributes( { 162 | query: { 163 | ...attributes.query, 164 | date_query: 165 | relation !== '' 166 | ? { 167 | ...attributes.query.date_query, 168 | relation, 169 | } 170 | : '', 171 | }, 172 | } ); 173 | } } 174 | __nextHasNoMarginBottom 175 | /> 176 | ) } 177 | { relationFromQuery !== '' && 178 | ! relationFromQuery.includes( 'current' ) && 179 | allowedControls.includes( 'date_query_relationship' ) && ( 180 | <> 181 | { relationFromQuery === 'between' && ( 182 |

183 | { __( 'Start date', 'advanced-query-loop' ) } 184 |

185 | ) } 186 | { 189 | setAttributes( { 190 | query: { 191 | ...attributes.query, 192 | date_query: { 193 | ...attributes.query.date_query, 194 | date_primary: newDate, 195 | }, 196 | }, 197 | } ); 198 | } } 199 | /> 200 | 201 | { relationFromQuery === 'between' && ( 202 | <> 203 |

204 | { __( 'End date', 'advanced-query-loop' ) } 205 |

206 | { 209 | setAttributes( { 210 | query: { 211 | ...attributes.query, 212 | date_query: { 213 | ...attributes.query 214 | .date_query, 215 | date_secondary: newDate, 216 | }, 217 | }, 218 | } ); 219 | } } 220 | /> 221 | 222 | ) } 223 | 224 |
225 | { 236 | setAttributes( { 237 | query: { 238 | ...attributes.query, 239 | date_query: { 240 | ...attributes.query.date_query, 241 | inclusive: newIsInclusive, 242 | }, 243 | }, 244 | } ); 245 | } } 246 | /> 247 | 248 | ) } 249 | 250 | ); 251 | }; 252 | -------------------------------------------------------------------------------- /src/components/post-exclude-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { ToggleControl } from '@wordpress/components'; 5 | import { useSelect } from '@wordpress/data'; 6 | import { useEntityRecord, store as coreDataStore } from '@wordpress/core-data'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | /** 10 | * A component that lets you pick posts to be excluded from the query 11 | * 12 | * @param {Object} props Component props 13 | * @param {Object} props.attributes Block attributes 14 | * @param {Function} props.setAttributes Block attributes setter 15 | * @param {Array} props.allowedControls Allowed controls 16 | * 17 | * @return {Element} PostExcludeControls 18 | */ 19 | export const PostExcludeControls = ( { 20 | attributes, 21 | setAttributes, 22 | allowedControls, 23 | } ) => { 24 | const { query: { exclude_current: excludeCurrent } = {} } = attributes; 25 | const { record: siteOptions } = useEntityRecord( 'root', 'site' ); 26 | const { currentPost, isAdmin } = useSelect( ( select ) => { 27 | return { 28 | currentPost: select( 'core/editor' ).getCurrentPost(), 29 | isAdmin: select( coreDataStore ).canUser( 'update', { 30 | kind: 'root', 31 | name: 'site', 32 | } ), 33 | }; 34 | }, [] ); 35 | 36 | // If the control is not allowed, return null. 37 | if ( ! allowedControls.includes( 'exclude_current_post' ) ) { 38 | return null; 39 | } 40 | 41 | if ( ! currentPost ) { 42 | return
{ __( 'Loading…', 'advanced-query-loop' ) }
; 43 | } 44 | 45 | const isDisabled = () => { 46 | // If the user is not an admin, they cannot edit template anyway 47 | if ( ! isAdmin ) { 48 | return false; 49 | } 50 | const templatesToExclude = [ 'archive', 'search' ]; 51 | const { 52 | show_on_front: showOnFront, // What is the front page set to show? Options: 'posts' or 'page' 53 | } = siteOptions; 54 | const disabledTemplates = [ 55 | ...templatesToExclude, 56 | ...( showOnFront === 'posts' ? [ 'home', 'front-page' ] : [] ), 57 | ]; 58 | return ( 59 | currentPost.type === 'wp_template' && 60 | disabledTemplates.includes( currentPost.slug ) 61 | ); 62 | }; 63 | 64 | return ( 65 | <> 66 |

{ __( 'Exclude Posts', 'advanced-query-loop' ) }

67 | { 73 | setAttributes( { 74 | query: { 75 | ...attributes.query, 76 | exclude_current: value ? currentPost.id : 0, 77 | }, 78 | } ); 79 | } } 80 | help={ 81 | isDisabled() 82 | ? __( 83 | 'This option is disabled for this template as there is no dedicated post to exclude.', 84 | 'advanced-query-loop' 85 | ) 86 | : __( 87 | 'Remove the associated post for this template/content from the query results.', 88 | 'advanced-query-loop' 89 | ) 90 | } 91 | /> 92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/post-include-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { BaseControl, FormTokenField } from '@wordpress/components'; 5 | import { useSelect } from '@wordpress/data'; 6 | import { useEffect, useState } from '@wordpress/element'; 7 | import { decodeEntities } from '@wordpress/html-entities'; 8 | import { __ } from '@wordpress/i18n'; 9 | 10 | /** 11 | * Generates a post include control component. 12 | * 13 | *@return {Element} PostIncludeControls 14 | */ 15 | 16 | export const PostIncludeControls = ( { 17 | attributes, 18 | setAttributes, 19 | allowedControls, 20 | } ) => { 21 | const { 22 | query: { 23 | include_posts: includePosts = [], 24 | postType, 25 | multiple_posts: multiplePosts = [], 26 | exclude_current: excludeCurrent = 0, 27 | } = {}, 28 | } = attributes; 29 | const [ searchArg, setSearchArg ] = useState( '' ); 30 | const [ multiplePostsState, setMultiplePostsState ] = 31 | useState( multiplePosts ); 32 | 33 | const posts = useSelect( 34 | ( select ) => { 35 | const { getEntityRecords } = select( 'core' ); 36 | 37 | return [ ...multiplePosts, postType ].reduce( 38 | ( totalRecords, currentPostType ) => { 39 | const records = getEntityRecords( 40 | 'postType', 41 | currentPostType, 42 | { 43 | per_page: 10, 44 | search: searchArg, 45 | exclude: excludeCurrent ? [ excludeCurrent ] : [], 46 | } 47 | ); 48 | return [ ...totalRecords, ...( records || [] ) ]; 49 | }, 50 | [] 51 | ); 52 | }, 53 | [ postType, multiplePosts, excludeCurrent, searchArg ] 54 | ); 55 | 56 | /** 57 | * This useEffect hook is triggered whenever the multiplePosts variable changes. 58 | * It checks if the value of multiplePosts is different from the value of multiplePostsState. 59 | * If the condition is true, it updates the query attribute using the setAttributes function, setting include_posts to an empty array. 60 | */ 61 | useEffect( () => { 62 | if ( 63 | JSON.stringify( multiplePosts ) !== 64 | JSON.stringify( multiplePostsState ) 65 | ) { 66 | setAttributes( { 67 | query: { 68 | ...attributes.query, 69 | include_posts: [], 70 | }, 71 | } ); 72 | setMultiplePostsState( multiplePosts ); 73 | } 74 | }, [ multiplePosts ] ); 75 | 76 | // If the control is not allowed, return null.`` 77 | if ( ! allowedControls.includes( 'include_posts' ) ) { 78 | return null; 79 | } 80 | 81 | /** 82 | * Retrieves the ID of a post based on its title. 83 | * 84 | * @param {string} postTitle - The title of the post. 85 | * @return {Array} An array containing the ID of the post. 86 | */ 87 | const getPostId = ( postTitle ) => { 88 | const foundPost = 89 | includePosts.find( 90 | ( post ) => decodeEntities( post.title ) === postTitle 91 | ) || 92 | posts.find( 93 | ( post ) => 94 | decodeEntities( post.title.rendered.trim() ) === postTitle 95 | ); 96 | 97 | return foundPost.title.rendered 98 | ? { id: foundPost.id, title: foundPost.title.rendered } 99 | : foundPost; 100 | }; 101 | 102 | if ( ! posts ) { 103 | return
{ __( 'Loading…', 'advanced-query-loop' ) }
; 104 | } 105 | 106 | // If the first post in the posts array does not have a title, don't render the component. 107 | if ( posts.length > 0 && ! posts[ 0 ].title ) { 108 | return null; 109 | } 110 | 111 | return ( 112 | <> 113 |

{ __( 'Include Posts', 'advanced-query-loop' ) }

114 | 121 | 124 | decodeEntities( item.title ) 125 | ) } 126 | suggestions={ posts.map( ( post ) => 127 | decodeEntities( post?.title?.rendered || '' ) 128 | ) } 129 | onInputChange={ ( searchPost ) => 130 | setSearchArg( searchPost ) 131 | } 132 | onChange={ ( titles ) => { 133 | setAttributes( { 134 | query: { 135 | ...attributes.query, 136 | include_posts: 137 | titles.map( ( title ) => 138 | getPostId( title ) 139 | ) || [], 140 | }, 141 | } ); 142 | setSearchArg( '' ); 143 | } } 144 | __experimentalExpandOnFocus 145 | __experimentalShowHowTo={ false } 146 | __nextHasNoMarginBottom 147 | /> 148 | 149 | 150 | ); 151 | }; 152 | -------------------------------------------------------------------------------- /src/components/post-meta-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { 5 | SelectControl, 6 | TextControl, 7 | Button, 8 | FormTokenField, 9 | BaseControl, 10 | } from '@wordpress/components'; 11 | import { __ } from '@wordpress/i18n'; 12 | 13 | const compareMetaOptions = [ 14 | '=', 15 | '!=', 16 | '>', 17 | '>=', 18 | '<', 19 | '<=', 20 | 'LIKE', 21 | 'NOT LIKE', 22 | 'IN', 23 | 'NOT IN', 24 | 'BETWEEN', 25 | 'NOT BETWEEN', 26 | 'EXISTS', 27 | 'NOT EXISTS', 28 | 'REGEXP', 29 | 'NOT REGEXP', 30 | 'RLIKE', 31 | ]; 32 | 33 | export const PostMetaControl = ( { 34 | registeredMetaKeys, 35 | id, 36 | queries, 37 | attributes, 38 | setAttributes, 39 | } ) => { 40 | const activeQuery = queries.find( ( query ) => query.id === id ); 41 | 42 | /** 43 | * Update a query param. 44 | * 45 | * @param {*} queries 46 | * @param {*} queryId 47 | * @param {*} item 48 | * @param {*} value 49 | * @returns 50 | */ 51 | const updateQueryParam = ( queries, queryId, item, value ) => { 52 | return queries.map( ( query ) => { 53 | if ( query.id === queryId ) { 54 | return { 55 | ...query, 56 | [ item ]: value, 57 | }; 58 | } 59 | return query; 60 | } ); 61 | }; 62 | 63 | return ( 64 | <> 65 | 72 | { 83 | setAttributes( { 84 | query: { 85 | ...attributes.query, 86 | meta_query: { 87 | ...attributes.query.meta_query, 88 | queries: updateQueryParam( 89 | queries, 90 | id, 91 | 'meta_key', 92 | newMeta[ 0 ] 93 | ), 94 | }, 95 | }, 96 | } ); 97 | } } 98 | __nextHasNoMarginBottom 99 | /> 100 | 101 | { 105 | setAttributes( { 106 | query: { 107 | ...attributes.query, 108 | meta_query: { 109 | ...attributes.query.meta_query, 110 | queries: updateQueryParam( 111 | queries, 112 | id, 113 | 'meta_value', 114 | newValue 115 | ), 116 | }, 117 | }, 118 | } ); 119 | } } 120 | /> 121 | { 126 | return { label: operator, value: operator }; 127 | } ), 128 | ] } 129 | onChange={ ( newCompare ) => { 130 | setAttributes( { 131 | query: { 132 | ...attributes.query, 133 | meta_query: { 134 | ...attributes.query.meta_query, 135 | queries: updateQueryParam( 136 | queries, 137 | id, 138 | 'meta_compare', 139 | newCompare 140 | ), 141 | }, 142 | }, 143 | } ); 144 | } } 145 | __nextHasNoMarginBottom 146 | /> 147 | 169 | 170 | ); 171 | }; 172 | -------------------------------------------------------------------------------- /src/components/post-meta-query-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { v4 as uuidv4 } from 'uuid'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { Button, SelectControl, PanelBody } from '@wordpress/components'; 10 | import { __ } from '@wordpress/i18n'; 11 | import { useEntityRecords } from '@wordpress/core-data'; 12 | import { useEffect, useState } from '@wordpress/element'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import { PostMetaControl } from './post-meta-control'; 18 | 19 | /** 20 | * Converts the meta keys from the all sources into a single array. 21 | * 22 | * @param {Array} records 23 | * @return {Array} meta keys 24 | */ 25 | const combineMetaKeys = ( records ) => { 26 | return { 27 | ...records?.[ 0 ]?.meta, 28 | ...records?.[ 0 ]?.acf, 29 | }; 30 | }; 31 | 32 | // A component to render a select control for the post meta query. 33 | export const PostMetaQueryControls = ( { 34 | attributes, 35 | setAttributes, 36 | allowedControls, 37 | } ) => { 38 | const { 39 | query: { 40 | postType, 41 | meta_query: { relation: relationFromQuery = '', queries = [] } = {}, 42 | } = {}, 43 | } = attributes; 44 | 45 | const { records } = useEntityRecords( 'postType', postType, { 46 | per_page: 1, 47 | } ); 48 | 49 | const [ selectedPostType ] = useState( postType ); 50 | 51 | useEffect( () => { 52 | // If the post type changes, reset the meta query. 53 | if ( postType !== selectedPostType ) { 54 | setAttributes( { 55 | query: { 56 | ...attributes.query, 57 | include_posts: [], 58 | meta_query: {}, 59 | }, 60 | } ); 61 | } 62 | }, [ postType ] ); 63 | 64 | // If the control is not allowed, return null. 65 | if ( ! allowedControls.includes( 'post_meta_query' ) ) { 66 | return null; 67 | } 68 | 69 | const registeredMeta = combineMetaKeys( records ); 70 | 71 | return ( 72 | <> 73 |

{ __( 'Post Meta Query', 'advanced-query-loop' ) }

74 | <> 75 | { queries.length > 1 && ( 76 | 88 | setAttributes( { 89 | query: { 90 | ...attributes.query, 91 | meta_query: { 92 | ...attributes.query.meta_query, 93 | relation, 94 | }, 95 | }, 96 | } ) 97 | } 98 | __nextHasNoMarginBottom 99 | /> 100 | ) } 101 | 102 | { queries.length < 1 && ( 103 |

104 | { __( 105 | 'Add a meta query to select post meta to query', 106 | 'advanced-query-loop' 107 | ) } 108 |

109 | ) } 110 | 111 | { queries.map( 112 | ( { 113 | id, 114 | meta_key: metaKey, 115 | meta_value: metaValue, 116 | compare, 117 | } ) => { 118 | return ( 119 | 120 | 132 | 133 | ); 134 | } 135 | ) } 136 | 163 |
164 |
165 | 166 | 167 | ); 168 | }; 169 | -------------------------------------------------------------------------------- /src/components/post-offset-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | 5 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 6 | import { __experimentalNumberControl as NumberControl } from '@wordpress/components'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | export const PostOffsetControls = ( { attributes, setAttributes } ) => { 10 | const { query: { offset = 0 } = {} } = attributes; 11 | return ( 12 | { 17 | setAttributes( { 18 | query: { 19 | ...attributes.query, 20 | offset: newOffset, 21 | }, 22 | } ); 23 | } } 24 | /> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/post-order-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { SelectControl, ToggleControl } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | export const sortOptions = [ 8 | { 9 | label: __( 'Name', 'advanced-query-loop' ), 10 | value: 'name', 11 | }, 12 | { 13 | label: __( 'Author', 'advanced-query-loop' ), 14 | value: 'author', 15 | }, 16 | { 17 | label: __( 'Comment Count', 'advanced-query-loop' ), 18 | value: 'comment_count', 19 | }, 20 | { 21 | label: __( 'Date', 'advanced-query-loop' ), 22 | value: 'date', 23 | }, 24 | { 25 | label: __( 'Included Posts', 'advanced-query-loop' ), 26 | value: 'post__in', 27 | }, 28 | { 29 | label: __( 'Last Modified Date', 'advanced-query-loop' ), 30 | value: 'modified', 31 | }, 32 | { 33 | label: __( 'Menu Order', 'advanced-query-loop' ), 34 | value: 'menu_order', 35 | }, 36 | { 37 | label: __( 'Meta Value', 'advanced-query-loop' ), 38 | value: 'meta_value', 39 | }, 40 | { 41 | label: __( 'Meta Value Num', 'advanced-query-loop' ), 42 | value: 'meta_value_num', 43 | }, 44 | { 45 | label: __( 'Post ID', 'advanced-query-loop' ), 46 | value: 'id', 47 | }, 48 | { 49 | label: __( 'Random', 'advanced-query-loop' ), 50 | value: 'rand', 51 | }, 52 | { 53 | label: __( 'Title', 'advanced-query-loop' ), 54 | value: 'title', 55 | }, 56 | ]; 57 | 58 | /** 59 | * PostOrderControls component 60 | * 61 | * @param {*} param0 62 | * @return {Element} PostCountControls 63 | */ 64 | export const PostOrderControls = ( { 65 | attributes, 66 | setAttributes, 67 | allowedControls, 68 | } ) => { 69 | const { query: { order, orderBy } = {} } = attributes; 70 | 71 | // If the control is not allowed, return null. 72 | if ( ! allowedControls.includes( 'post_order' ) ) { 73 | return null; 74 | } 75 | 76 | return ( 77 | <> 78 | 90 | a.label.localeCompare( b.label ) 91 | ) } 92 | onChange={ ( newOrderBy ) => { 93 | setAttributes( { 94 | query: { 95 | ...attributes.query, 96 | orderBy: newOrderBy, 97 | }, 98 | } ); 99 | } } 100 | __nextHasNoMarginBottom 101 | /> 102 | { 106 | setAttributes( { 107 | query: { 108 | ...attributes.query, 109 | order: order === 'asc' ? 'desc' : 'asc', 110 | }, 111 | } ); 112 | } } 113 | __nextHasNoMarginBottom 114 | /> 115 | 116 | ); 117 | }; 118 | -------------------------------------------------------------------------------- /src/components/single-taxonomy-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { 5 | FormTokenField, 6 | SelectControl, 7 | Button, 8 | ToggleControl, 9 | // eslint-disable-next-line @wordpress/no-unsafe-wp-apis 10 | __experimentalHStack as HStack, 11 | } from '@wordpress/components'; 12 | import { __ } from '@wordpress/i18n'; 13 | import { useEntityRecords } from '@wordpress/core-data'; 14 | import { useMemo, useEffect, useState } from '@wordpress/element'; 15 | 16 | /** 17 | * Internal dependencies 18 | */ 19 | import useDebouncedInputValue from '../hooks/useDebouncedInputValue'; 20 | import { updateTaxonomyQuery } from '../utils'; 21 | const advancedOperators = [ 'EXISTS', 'NOT EXISTS', 'AND' ]; 22 | const operatorOptions = [ 'IN', 'NOT IN', ...advancedOperators ]; 23 | 24 | const toggleMargin = { 25 | marginTop: '1.5em', 26 | marginBottom: '0.75em', 27 | }; 28 | 29 | /* 30 | tax_query: { 31 | relation: 'AND', 32 | queries: [ 33 | { 34 | taxonomy: 'category', 35 | terms: [ 'current events' ], 36 | operator: 'IN', 37 | }, 38 | { 39 | taxonomy: 'category', 40 | terms: [ 'politics' ], 41 | operator: 'NOT_IN', 42 | }, 43 | ], 44 | }, 45 | */ 46 | const SingleTaxonomyControl = ( { 47 | id, 48 | taxonomy, 49 | terms, 50 | operator, 51 | includeChildren, 52 | availableTaxonomies, 53 | attributes, 54 | setAttributes, 55 | } ) => { 56 | const [ searchTerm, setSearchTerm ] = useDebouncedInputValue( '', 500 ); 57 | 58 | const [ advancedMode, setAdvancedMode ] = useState( false ); 59 | const [ disableAdvancedToggle, setDisableAdvancedToggle ] = 60 | useState( false ); 61 | const { records } = useEntityRecords( 'taxonomy', taxonomy, { 62 | per_page: 10, 63 | search: searchTerm, 64 | _fields: 'id,name', 65 | context: 'view', 66 | } ); 67 | 68 | const suggestions = useMemo( () => { 69 | return ( records ?? [] ).map( ( term ) => term.name ); 70 | }, [ records ] ); 71 | 72 | useEffect( () => { 73 | if ( 74 | advancedOperators.includes( operator ) || 75 | includeChildren === false 76 | ) { 77 | setAdvancedMode( true ); 78 | setDisableAdvancedToggle( true ); 79 | } else { 80 | setDisableAdvancedToggle( false ); 81 | } 82 | }, [ operator, includeChildren ] ); 83 | 84 | return ( 85 | <> 86 | { 92 | return { 93 | label: name, 94 | value: slug, 95 | }; 96 | } ), 97 | ] } 98 | onChange={ ( newTaxonomy ) => { 99 | setAttributes( { 100 | query: { 101 | ...attributes.query, 102 | tax_query: { 103 | ...attributes.query.tax_query, 104 | queries: updateTaxonomyQuery( 105 | attributes.query.tax_query.queries, 106 | id, 107 | 'taxonomy', 108 | newTaxonomy 109 | ), 110 | }, 111 | }, 112 | } ); 113 | } } 114 | __next40pxDefaultSize 115 | /> 116 | { taxonomy.length > 1 && ( 117 | <> 118 | { 123 | setSearchTerm( newInput ); 124 | } } 125 | onChange={ ( newTerms ) => { 126 | setAttributes( { 127 | query: { 128 | ...attributes.query, 129 | tax_query: { 130 | ...attributes.query.tax_query, 131 | queries: updateTaxonomyQuery( 132 | attributes.query.tax_query.queries, 133 | id, 134 | 'terms', 135 | newTerms, 136 | 'include' 137 | ), 138 | }, 139 | }, 140 | } ); 141 | } } 142 | __next40pxDefaultSize 143 | /> 144 | { advancedMode ? ( 145 | <> 146 | { 154 | return { label: value, value }; 155 | } ), 156 | ] } 157 | onChange={ ( newOperator ) => { 158 | setAttributes( { 159 | query: { 160 | ...attributes.query, 161 | tax_query: { 162 | ...attributes.query.tax_query, 163 | queries: updateTaxonomyQuery( 164 | attributes.query.tax_query 165 | .queries, 166 | id, 167 | 'operator', 168 | newOperator 169 | ), 170 | }, 171 | }, 172 | } ); 173 | } } 174 | __next40pxDefaultSize 175 | /> 176 |
177 | { 185 | setAttributes( { 186 | query: { 187 | ...attributes.query, 188 | tax_query: { 189 | ...attributes.query 190 | .tax_query, 191 | queries: 192 | updateTaxonomyQuery( 193 | attributes.query 194 | .tax_query 195 | .queries, 196 | id, 197 | 'include_children', 198 | include 199 | ), 200 | }, 201 | }, 202 | } ); 203 | } } 204 | __next40pxDefaultSize 205 | /> 206 |
207 | 208 | ) : ( 209 | { 216 | const currentQuery = 217 | attributes.query.tax_query.queries.find( 218 | ( query ) => query.id === id 219 | ); 220 | 221 | setAttributes( { 222 | query: { 223 | ...attributes.query, 224 | tax_query: { 225 | ...attributes.query.tax_query, 226 | queries: updateTaxonomyQuery( 227 | attributes.query.tax_query 228 | .queries, 229 | id, 230 | 'operator', 231 | currentQuery.operator === 'IN' 232 | ? 'NOT IN' 233 | : 'IN' 234 | ), 235 | }, 236 | }, 237 | } ); 238 | } } 239 | __next40pxDefaultSize 240 | /> 241 | ) } 242 | 243 | ) } 244 |
245 | 249 | { taxonomy && ( 250 | setAdvancedMode( ! advancedMode ) } 254 | disabled={ disableAdvancedToggle } 255 | __nextHasNoMarginBottom 256 | /> 257 | ) } 258 | 281 | 282 |
283 |
284 | 285 | ); 286 | }; 287 | 288 | export default SingleTaxonomyControl; 289 | -------------------------------------------------------------------------------- /src/components/taxonomy-query-control.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @wordpress/no-unsafe-wp-apis */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import { v4 as uuidv4 } from 'uuid'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | import { 12 | Button, 13 | Dropdown, 14 | __experimentalDropdownContentWrapper as DropdownContentWrapper, 15 | PanelBody, 16 | ToggleControl, 17 | Panel, 18 | __experimentalHStack as HStack, 19 | } from '@wordpress/components'; 20 | import { __ } from '@wordpress/i18n'; 21 | import { useSelect } from '@wordpress/data'; 22 | import { store as coreStore } from '@wordpress/core-data'; 23 | 24 | /** 25 | * Internal dependencies 26 | */ 27 | import SingleTaxonomyControl from './single-taxonomy-control'; 28 | 29 | export const TaxonomyQueryControl = ( { 30 | attributes, 31 | setAttributes, 32 | allowedControls, 33 | } ) => { 34 | const { 35 | query: { 36 | postType, 37 | multiple_posts: multiplePosts = [], 38 | tax_query: { relation = '', queries = [] } = {}, 39 | } = {}, 40 | } = attributes; 41 | 42 | const availableTaxonomies = useSelect( ( select ) => 43 | select( coreStore ) 44 | .getTaxonomies( { per_page: 50 } ) 45 | ?.filter( ( { types } ) => 46 | types.some( ( type ) => 47 | [ postType, ...multiplePosts ].includes( type ) 48 | ) 49 | ) 50 | ); 51 | 52 | // If the control is not allowed, return null. 53 | if ( ! allowedControls.includes( 'taxonomy_query_builder' ) ) { 54 | return null; 55 | } 56 | 57 | return ( 58 | <> 59 | ( 65 | 82 | ) } 83 | renderContent={ () => ( 84 | 88 | 94 | 95 | { queries.length > 1 && ( 96 | <> 97 | { 108 | setAttributes( { 109 | query: { 110 | ...attributes.query, 111 | tax_query: { 112 | ...attributes.query 113 | .tax_query, 114 | relation: 115 | attributes.query 116 | .tax_query 117 | .relation === 118 | 'OR' 119 | ? 'AND' 120 | : 'OR', 121 | }, 122 | }, 123 | } ); 124 | } } 125 | __nextHasNoMarginBottom={ false } 126 | /> 127 |
128 | 129 | ) } 130 | 131 | { queries.map( 132 | ( { 133 | id, 134 | taxonomy, 135 | terms, 136 | operator, 137 | include_children: includeChildren, 138 | } ) => { 139 | return ( 140 | 156 | ); 157 | } 158 | ) } 159 | 160 | 161 | 191 | { queries.length > 0 && ( 192 | 209 | ) } 210 | 211 |
212 |
213 |
214 | ) } 215 | /> 216 | 217 | ); 218 | }; 219 | -------------------------------------------------------------------------------- /src/hooks/useDebouncedInputValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress Dependencies 3 | */ 4 | import { useState } from '@wordpress/element'; 5 | import { useDebounce } from '@wordpress/compose'; 6 | 7 | const useDebouncedInputValue = ( defaultVal, debounceTimeout ) => { 8 | const [ searchTerm, setSearchTerm ] = useState( defaultVal ); 9 | const debouncedSetSearchTerm = useDebounce( 10 | setSearchTerm, 11 | debounceTimeout 12 | ); 13 | return [ searchTerm, debouncedSetSearchTerm ]; 14 | }; 15 | 16 | export default useDebouncedInputValue; 17 | -------------------------------------------------------------------------------- /src/legacy-controls/pre-gb-19.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { registerPlugin } from '@wordpress/plugins'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import AQLLegacyControls from '../slots/aql-legacy-controls'; 10 | import { PostCountControls } from '../components/post-count-controls'; 11 | import { PostOffsetControls } from '../components/post-offset-controls'; 12 | 13 | registerPlugin( 'aql-pre-gb-19-controls', { 14 | render: () => { 15 | return ( 16 | <> 17 | 18 | { ( props ) => ( 19 | <> 20 | 21 | 22 | 23 | ) } 24 | 25 | 26 | ); 27 | }, 28 | } ); 29 | -------------------------------------------------------------------------------- /src/slots/aql-controls-inherited-query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createSlotFill } from '@wordpress/components'; 5 | 6 | /** 7 | * Create our Slot and Fill components 8 | */ 9 | const { Fill, Slot } = createSlotFill( 'AQLControlsInheritedQuery' ); 10 | 11 | const AQLControlsInheritedQuery = ( { children } ) => { children }; 12 | 13 | AQLControlsInheritedQuery.Slot = Slot; 14 | 15 | export default AQLControlsInheritedQuery; 16 | -------------------------------------------------------------------------------- /src/slots/aql-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createSlotFill } from '@wordpress/components'; 5 | 6 | /** 7 | * Create our Slot and Fill components 8 | */ 9 | const { Fill, Slot } = createSlotFill( 'AQLControls' ); 10 | 11 | const AQLControls = ( { children } ) => { children }; 12 | 13 | AQLControls.Slot = ( { fillProps } ) => ( 14 | 15 | { ( fills ) => { 16 | return fills.length ? fills : null; 17 | } } 18 | 19 | ); 20 | 21 | export default AQLControls; 22 | -------------------------------------------------------------------------------- /src/slots/aql-legacy-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createSlotFill } from '@wordpress/components'; 5 | 6 | /** 7 | * Create our Slot and Fill components 8 | */ 9 | const { Fill, Slot } = createSlotFill( 'AQLLegacyControls' ); 10 | 11 | /** 12 | * This slot is not exposed and is used to try to maintain the same UI 13 | */ 14 | 15 | const AQLLegacyControls = ( { children } ) => { children }; 16 | 17 | AQLLegacyControls.Slot = ( { fillProps } ) => ( 18 | 19 | { ( fills ) => { 20 | return fills.length ? fills : null; 21 | } } 22 | 23 | ); 24 | 25 | export default AQLLegacyControls; 26 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to update taxonomy queries. 3 | * 4 | * @param {Array} queries The current queries. 5 | * @param {string} queryId The query ID to update. 6 | * @param {string} item The key to update. 7 | * @param {string} value The value to update. 8 | * 9 | * @return {Array} The updated queries. 10 | */ 11 | export const updateTaxonomyQuery = ( queries, queryId, item, value ) => { 12 | return queries.map( ( query ) => { 13 | if ( query.id === queryId ) { 14 | return { 15 | ...query, 16 | [ item ]: value, 17 | }; 18 | } 19 | return query; 20 | } ); 21 | }; 22 | 23 | /** 24 | * A helper to retrieve the correct items to display or save in the token field 25 | * 26 | * @param {Array} subSet 27 | * @param {Array} fullSet 28 | * @param {string} lookupProperty 29 | * @param {string} returnProperty 30 | * @return {Array} The correct items to display or save in the token field 31 | */ 32 | export const prepDataFromTokenField = ( 33 | subSet, 34 | fullSet, 35 | lookupProperty, 36 | returnProperty 37 | ) => { 38 | const subsetFullObjects = fullSet.filter( ( item ) => 39 | subSet.includes( item[ lookupProperty ] ) 40 | ); 41 | return subsetFullObjects.map( 42 | ( { [ returnProperty ]: returnVal } ) => returnVal 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/variations/controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { addFilter } from '@wordpress/hooks'; 5 | import { InspectorControls } from '@wordpress/block-editor'; 6 | import { PanelBody } from '@wordpress/components'; 7 | import { __ } from '@wordpress/i18n'; 8 | import { createBlock } from '@wordpress/blocks'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { AQL } from '.'; 14 | import AQLControls from '../slots/aql-controls'; 15 | import AQLControlsInheritedQuery from '../slots/aql-controls-inherited-query'; 16 | import AQLLegacyControls from '../slots/aql-legacy-controls'; 17 | import { PostMetaQueryControls } from '../components/post-meta-query-controls'; 18 | import { PostDateQueryControls } from '../components/post-date-query-controls'; 19 | import { MultiplePostSelect } from '../components/multiple-post-select'; 20 | import { PostOrderControls } from '../components/post-order-controls'; 21 | import { PostExcludeControls } from '../components/post-exclude-controls'; 22 | import { TaxonomyQueryControl } from '../components/taxonomy-query-control'; 23 | import { PostIncludeControls } from '../components/post-include-controls'; 24 | import { PaginationToggle } from '../components/pagination-toggle'; 25 | import { ChildItemsToggle } from '../components/child-items-toggle'; 26 | 27 | /** 28 | * Determines if the active variation is this one 29 | * 30 | * @param {*} props 31 | * @return {boolean} Is this the correct variation? 32 | */ 33 | const isAdvancedQueryLoop = ( props ) => { 34 | const { 35 | attributes: { namespace }, 36 | } = props; 37 | return namespace && namespace === AQL; 38 | }; 39 | 40 | /** 41 | * Custom controls 42 | * 43 | * @param {*} BlockEdit 44 | * @return {Element} BlockEdit instance 45 | */ 46 | const withAdvancedQueryControls = ( BlockEdit ) => ( props ) => { 47 | // If the is the correct variation, add the custom controls. 48 | if ( isAdvancedQueryLoop( props ) ) { 49 | const { allowedControls } = window?.aql; 50 | const { attributes } = props; 51 | const allowedControlsArray = allowedControls.split( ',' ); 52 | const propsWithControls = { 53 | ...props, 54 | allowedControls: allowedControlsArray, 55 | }; 56 | // If the inherit prop is false or undefined, add all the controls. 57 | if ( ! attributes.query.inherit ) { 58 | return ( 59 | <> 60 | 61 | 62 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 84 | 85 | 86 | 87 | ); 88 | } 89 | // Add some controls if the inherit prop is true. 90 | return ( 91 | <> 92 | 93 | 94 | 100 | 101 | 104 | 105 | 106 | 107 | ); 108 | } 109 | return ; 110 | }; 111 | 112 | addFilter( 113 | 'editor.BlockEdit', 114 | 'aql/add-add-controls/core/query', 115 | withAdvancedQueryControls 116 | ); 117 | 118 | /** 119 | * Filter to add AQL transform to core/query block 120 | * 121 | * @param {Object} settings 122 | * @param {string} name 123 | * @return {Object} settings 124 | */ 125 | function addAQLTransforms( settings, name ) { 126 | if ( name !== 'core/query' ) { 127 | return settings; 128 | } 129 | 130 | return { 131 | ...settings, 132 | keywords: [ ...settings.keywords, 'AQL', 'aql' ], 133 | transforms: { 134 | to: settings?.transforms?.to || [], 135 | from: [ 136 | ...( settings?.transforms?.from || [] ), 137 | { 138 | type: 'enter', 139 | regExp: /^(AQL|aql)$/, 140 | transform: () => { 141 | return createBlock( 142 | 'core/query', 143 | { 144 | namespace: 'advanced-query-loop', 145 | }, 146 | [] 147 | ); 148 | }, 149 | }, 150 | ], 151 | }, 152 | }; 153 | } 154 | 155 | addFilter( 156 | 'blocks.registerBlockType', 157 | 'aql/add-transforms/query-block', 158 | addAQLTransforms 159 | ); 160 | -------------------------------------------------------------------------------- /src/variations/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { registerBlockVariation } from '@wordpress/blocks'; 5 | import { __ } from '@wordpress/i18n'; 6 | /** 7 | * Internal dependencies 8 | */ 9 | import './controls'; 10 | import AQLIcon from '../components/icons'; 11 | import AQLControls from '../slots/aql-controls'; 12 | import AQLControlsInheritedQuery from '../slots/aql-controls-inherited-query'; 13 | const AQL = 'advanced-query-loop'; 14 | 15 | registerBlockVariation( 'core/query', { 16 | name: AQL, 17 | title: __( 'Advanced Query Loop', 'advanced-query-loop' ), 18 | description: __( 'Create advanced queries', 'advanced-query-loop' ), 19 | icon: AQLIcon, 20 | isActive: [ 'namespace' ], 21 | attributes: { 22 | namespace: AQL, 23 | }, 24 | scope: [ 'inserter', 'transform' ], 25 | } ); 26 | 27 | export { AQL, AQLControls, AQLControlsInheritedQuery }; 28 | -------------------------------------------------------------------------------- /tests/unit/Date_Query_Tests.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'range' => 'last-month', 23 | 'relation' => 'before', 24 | 'date_primary' => '2024-06-05T13:31:35', 25 | ], 26 | ], 27 | [ 28 | 'date_query' => [ 29 | 'before' => [ 30 | 'year' => gmdate( 'Y', strtotime( 'last day of last month' ) ), 31 | 'month' => gmdate( 'm', strtotime( 'last day of last month' ) ), 32 | 'day' => gmdate( 'd', strtotime( 'last day of last month' ) ), 33 | ], 34 | 'after' => [ 35 | 'year' => gmdate( 'Y', strtotime( 'first day of -1 months' ) ), 36 | 'month' => gmdate( 'm', strtotime( 'first day of -1 months' ) ), 37 | 'day' => gmdate( 'd', strtotime( 'first day of -1 months' ) ), 38 | ], 39 | ], 40 | ], 41 | ], 42 | [ 43 | // Empty range is ignored 44 | [ 45 | 'date_query' => [ 46 | 'range' => '', 47 | 'relation' => 'before', 48 | 'date_primary' => '2024-06-05T13:31:35', 49 | ], 50 | ], 51 | [ 52 | 'date_query' => [ 53 | 'before' => [ 54 | 'year' => '2024', 55 | 'month' => '06', 56 | 'day' => '05', 57 | ], 58 | ], 59 | ], 60 | ], 61 | [ 62 | // Null range is ignored 63 | [ 64 | 'date_query' => [ 65 | 'range' => null, 66 | 'relation' => 'before', 67 | 'date_primary' => '2024-06-05T13:31:35', 68 | ], 69 | ], 70 | [ 71 | 'date_query' => [ 72 | 'before' => [ 73 | 'year' => '2024', 74 | 'month' => '06', 75 | 'day' => '05', 76 | ], 77 | ], 78 | ], 79 | ], 80 | ]; 81 | } 82 | 83 | /** 84 | * Ranges and Relationships can't co-exist in they query 85 | * 86 | * @param array $custom_data The data from the block. 87 | * @param array $expected_results The expected results to test against. 88 | * 89 | * @dataProvider data_range_and_relationship_are_discreet 90 | */ 91 | public function test_range_and_relationship_are_discreet( $custom_data, $expected_results ) { 92 | $qpg = new Query_Params_Generator( [], $custom_data ); 93 | $qpg->process_all(); 94 | 95 | // Empty arrays return empty. 96 | $this->assertSame( $expected_results, $qpg->get_query_args() ); 97 | } 98 | 99 | /** 100 | * Data provider 101 | */ 102 | public function data_all_ranges_return_expected() { 103 | return [ 104 | // Month 105 | [ 106 | [ 107 | 'date_query' => [ 108 | 'range' => 'last-month', 109 | ], 110 | ], 111 | [ 112 | 'date_query' => [ 113 | 'before' => [ 114 | 'year' => gmdate( 'Y', strtotime( 'last day of last month' ) ), 115 | 'month' => gmdate( 'm', strtotime( 'last day of last month' ) ), 116 | 'day' => gmdate( 'd', strtotime( 'last day of last month' ) ), 117 | ], 118 | 'after' => [ 119 | 'year' => gmdate( 'Y', strtotime( 'first day of -1 months' ) ), 120 | 'month' => gmdate( 'm', strtotime( 'first day of -1 months' ) ), 121 | 'day' => gmdate( 'd', strtotime( 'first day of -1 months' ) ), 122 | ], 123 | ], 124 | ], 125 | ], 126 | [ 127 | [ 128 | 'date_query' => [ 129 | 'range' => 'three-months', 130 | ], 131 | ], 132 | [ 133 | 'date_query' => [ 134 | 'before' => [ 135 | 'year' => gmdate( 'Y', strtotime( 'last day of last month' ) ), 136 | 'month' => gmdate( 'm', strtotime( 'last day of last month' ) ), 137 | 'day' => gmdate( 'd', strtotime( 'last day of last month' ) ), 138 | ], 139 | 'after' => [ 140 | 'year' => gmdate( 'Y', strtotime( 'first day of -3 months' ) ), 141 | 'month' => gmdate( 'm', strtotime( 'first day of -3 months' ) ), 142 | 'day' => gmdate( 'd', strtotime( 'first day of -3 months' ) ), 143 | ], 144 | ], 145 | ], 146 | ], 147 | [ 148 | [ 149 | 'date_query' => [ 150 | 'range' => 'six-months', 151 | ], 152 | ], 153 | [ 154 | 'date_query' => [ 155 | 'before' => [ 156 | 'year' => gmdate( 'Y', strtotime( 'last day of last month' ) ), 157 | 'month' => gmdate( 'm', strtotime( 'last day of last month' ) ), 158 | 'day' => gmdate( 'd', strtotime( 'last day of last month' ) ), 159 | ], 160 | 'after' => [ 161 | 'year' => gmdate( 'Y', strtotime( 'first day of -6 months' ) ), 162 | 'month' => gmdate( 'm', strtotime( 'first day of -6 months' ) ), 163 | 'day' => gmdate( 'd', strtotime( 'first day of -6 months' ) ), 164 | ], 165 | ], 166 | ], 167 | ], 168 | [ 169 | [ 170 | 'date_query' => [ 171 | 'range' => 'twelve-months', 172 | ], 173 | ], 174 | [ 175 | 'date_query' => [ 176 | 'before' => [ 177 | 'year' => gmdate( 'Y', strtotime( 'last day of last month' ) ), 178 | 'month' => gmdate( 'm', strtotime( 'last day of last month' ) ), 179 | 'day' => gmdate( 'd', strtotime( 'last day of last month' ) ), 180 | ], 181 | 'after' => [ 182 | 'year' => gmdate( 'Y', strtotime( 'first day of -12 months' ) ), 183 | 'month' => gmdate( 'm', strtotime( 'first day of -12 months' ) ), 184 | 'day' => gmdate( 'd', strtotime( 'first day of -12 months' ) ), 185 | ], 186 | ], 187 | ], 188 | ], 189 | ]; 190 | } 191 | 192 | /** 193 | * Ensure range return the expected array 194 | * 195 | * @param array $custom_data The data from the block. 196 | * @param array $expected_results The expected results to test against. 197 | * 198 | * @dataProvider data_all_ranges_return_expected 199 | */ 200 | public function test_all_ranges_return_expected( $custom_data, $expected_results ) { 201 | $qpg = new Query_Params_Generator( [], $custom_data ); 202 | $qpg->process_all(); 203 | 204 | // Empty arrays return empty. 205 | $this->assertSame( $expected_results, $qpg->get_query_args() ); 206 | 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /tests/unit/Exclude_Current_Tests.php: -------------------------------------------------------------------------------- 1 | '', 35 | ), 36 | ), 37 | ); 38 | } 39 | 40 | /** 41 | * All of these tests will return empty arrays 42 | * 43 | * @param array $default_data The params coming from the default block. 44 | * @param array $custom_data The params coming from AQL. 45 | * 46 | * @dataProvider data_returns_empty_array 47 | */ 48 | public function test_exclude_current_returns_empty( $default_data, $custom_data ) { 49 | 50 | $qpg = new Query_Params_Generator( $default_data, $custom_data ); 51 | $qpg->process_all(); 52 | 53 | // Empty arrays return empty. 54 | $this->assertEmpty( $qpg->get_query_args() ); 55 | } 56 | 57 | 58 | /** 59 | * Data provider for the empty array tests 60 | * 61 | * @return array 62 | */ 63 | public function data_basic_exclude_current() { 64 | return array( 65 | array( 66 | // Default values. 67 | array(), 68 | // Custom data. 69 | array( 'exclude_current' => '1' ), 70 | ), 71 | array( 72 | // Default values. 73 | array(), 74 | // Custom data. 75 | array( 76 | 'exclude_current' => 1, 77 | ), 78 | ), 79 | ); 80 | } 81 | 82 | /** 83 | * Test that basics of setting an ID 84 | * 85 | * @param array $default_data The params coming from the default block. 86 | * @param array $custom_data The params coming from AQL. 87 | * 88 | * @dataProvider data_basic_exclude_current 89 | */ 90 | public function test_basic_exclude_current( $default_data, $custom_data ) { 91 | $qpg = new Query_Params_Generator( $default_data, $custom_data ); 92 | $qpg->process_all(); 93 | 94 | $this->assertEquals( array( 'post__not_in' => array( 1 ) ), $qpg->get_query_args() ); 95 | } 96 | 97 | 98 | /** 99 | * When Exclude current post is set on a template, it receives a string of the template name. 100 | */ 101 | public function test_exclude_current_receives_a_string() { 102 | 103 | // We need to mock a global post object 104 | $GLOBALS['post'] = (object) array( 'ID' => '1337' ); 105 | 106 | $default_data = array(); 107 | $custom_data = array( 'exclude_current' => 'twentytwentyfour//single' ); 108 | 109 | $qpg = new Query_Params_Generator( $default_data, $custom_data ); 110 | $qpg->process_all(); 111 | 112 | $this->assertEquals( array( 'post__not_in' => array( 1337 ) ), $qpg->get_query_args() ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/unit/Multiple_Post_Types_Tests.php: -------------------------------------------------------------------------------- 1 | array(), 33 | ), 34 | // Custom data. 35 | array( 36 | 'multiple_posts' => array(), 37 | ), 38 | ), 39 | ); 40 | } 41 | 42 | /** 43 | * All of these tests will return empty arrays 44 | * 45 | * @param array $default_data The params coming from the default block. 46 | * @param array $custom_data The params coming from AQL. 47 | * 48 | * @dataProvider data_returns_empty_array 49 | */ 50 | public function test_meta_query_returns_empty( $default_data, $custom_data ) { 51 | 52 | $qpg = new Query_Params_Generator( $default_data, $custom_data ); 53 | $qpg->process_all(); 54 | 55 | // Empty arrays return empty. 56 | $this->assertEmpty( $qpg->get_query_args() ); 57 | } 58 | 59 | 60 | 61 | /** 62 | * Data provider for test_combine_post_types 63 | * 64 | * @return array 65 | */ 66 | public function data_combine_post_types() { 67 | return array( 68 | // Test basic combine of data 69 | array( 70 | // Test data 71 | array( 72 | // Default values. 73 | 'default_data' => array( 74 | 'post_type' => 'posts', 75 | ), 76 | // Custom data. 77 | 'custom_data' => array( 78 | 'multiple_posts' => array( 'pages' ), 79 | ), 80 | ), 81 | // Expected results 82 | array( 'post_type' => array( 'posts', 'pages' ) ), 83 | ), 84 | // Test for duplicates. The UI shouldn't allow this but worth doing anyways. 85 | array( 86 | // Test data 87 | array( 88 | // Default values. 89 | 'default_data' => array( 90 | 'post_type' => 'posts', 91 | ), 92 | // Custom data. 93 | 'custom_data' => array( 94 | 'multiple_posts' => array( 'posts' ), 95 | ), 96 | ), 97 | // Expected results 98 | array( 'post_type' => array( 'posts' ) ), 99 | ), 100 | ); 101 | } 102 | 103 | /** 104 | * All of these tests will return empty arrays 105 | * 106 | * @param array $data Data to pass to the QPG class. 107 | * @param array $expected Expected results of the test. 108 | * 109 | * @dataProvider data_combine_post_types 110 | */ 111 | public function test_combine_post_types( $data, $expected ) { 112 | 113 | $qpg = new Query_Params_Generator( $data['default_data'], $data['custom_data'] ); 114 | $qpg->process_all(); 115 | 116 | $this->assertEquals( $expected, $qpg->get_query_args() ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/unit/Query_Params_Generator_Tests.php: -------------------------------------------------------------------------------- 1 | process_all(); 54 | 55 | // Empty arrays return empty. 56 | $this->assertEmpty( $qpg->get_query_args() ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/unit/bootstrap.php: -------------------------------------------------------------------------------- 1 |