Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Advanced Query Loop (AQL) is a WordPress plugin that extends the core Query Loop block with advanced querying capabilities. It provides a block variation with additional controls for taxonomy queries, post meta queries, date queries, post ordering, and more.

The plugin uses a hybrid architecture combining PHP server-side logic with TypeScript/React for the block editor interface.

## Development Commands

### Setup
```bash
npm run setup # Install PHP dependencies (runs composer run dev)
composer run dev # Install PHP dev dependencies with autoload
```

### Development
```bash
npm start # Start development mode (TypeScript watch + webpack)
npm run start:webpack # Start webpack only
npm run start:hot # Start with hot module replacement
npm run tsc # Run TypeScript compiler once
npm run tsc:watch # Run TypeScript compiler in watch mode
```

### Building
```bash
npm run build # Build production JavaScript/TypeScript
npm run release # Full release build (composer build + npm build + plugin-zip)
npm run plugin-zip # Create distributable plugin ZIP
composer run build # Production PHP build (no dev dependencies)
```

### Testing
```bash
npm run test:unit # Run PHPUnit tests
./vendor/bin/phpunit # Run PHPUnit directly
```

### Local Environment
```bash
npm run wp-env start # Start local WordPress environment
npm run wp-env stop # Stop local WordPress environment
```

### Code Quality
```bash
npm run format # Format code using WordPress standards
```

## Architecture

### PHP Architecture (Server-Side)

**Entry Point**: `index.php` - Main plugin file that loads the autoloader.

**Core Query Processing**:
- `includes/Query_Params_Generator.php` - Central class that processes all custom query parameters using traits
- `includes/query-loop.php` - Hooks into WordPress filters to modify queries on both frontend and REST API
- Uses the `pre_render_block` filter to intercept Query Loop blocks with `namespace: 'advanced-query-loop'`
- Uses `query_loop_block_query_vars` filter for non-inherited queries
- Uses `rest_{post_type}_query` filters to enable custom parameters in the block editor

**Query Parameter Traits** (`includes/Traits/`):
Each trait in the Traits directory handles a specific query modification:
- `Date_Query.php` - Before/after/relative date filtering
- `Disable_Pagination.php` - Performance optimization by disabling pagination
- `Exclude_Current.php` - Remove current post from results
- `Exclude_Taxonomies.php` - Exclude posts by taxonomy terms
- `Include_Posts.php` - Manually select specific posts
- `Meta_Query.php` - Post meta filtering with multiple conditions
- `Multiple_Posts.php` - Multiple post type selection
- `Post_Parent.php` - Child post filtering
- `Tax_Query.php` - Advanced taxonomy queries with AND/OR logic

**Key Filter Hook**: `aql_query_vars` - This filter allows extensions to modify query arguments. It receives:
1. `$query_args` - Arguments to be passed to WP_Query
2. `$block_query` - The query attribute from the block
3. `$inherited` - Whether the query is being inherited from template

### TypeScript/React Architecture (Block Editor)

**Entry Point**: `src/variations/index.ts` - Registers the block variation and exports SlotFills.

**Block Variation Registration**: Registers `advanced-query-loop` as a variation of `core/query` with custom namespace attribute.

**Controls System** (`src/variations/controls.tsx`):
- Uses `addFilter` on `editor.BlockEdit` to inject custom controls
- Conditionally renders different control sets based on `query.inherit` attribute
- When `inherit: false` - Shows all advanced controls in the "Advanced Query Settings" panel
- When `inherit: true` - Shows limited controls (only PostOrderControls and inherited query slot)

**UI Components** (`src/components/`):
Each component corresponds to a query feature:
- `post-meta-query-controls.js` - Complex meta query builder
- `post-date-query-controls.js` - Date filtering UI
- `multiple-post-select.js` - Post type selector
- `post-order-controls.tsx` - Order/orderby controls
- `post-exclude-controls.js` - Exclude current post, categories
- `taxonomy-query-control.js` - Advanced taxonomy query builder
- `post-include-controls.js` - Manual post selection
- `pagination-toggle.js` - Enable/disable pagination
- `child-items-toggle.js` - Show only child items

**SlotFill System** (`src/slots/`):
Extensibility mechanism exposed via `window.aql`:
- `AQLControls` - Slot for controls shown when NOT inheriting query
- `AQLControlsInheritedQuery` - Slot for controls shown when inheriting query
- `AQLLegacyControls` - Slot for legacy Gutenberg < 19 controls

### Build System

**Webpack Configuration** (`webpack.config.js`):
- Extends `@wordpress/scripts` default configuration
- Entry point: `src/variations/index.ts`
- Additional entry for legacy pre-GB-19 controls: `src/legacy-controls/pre-gb-19.js`
- Exports library to global `window.aql`
- Dev server allows all hosts for cross-environment testing

**TypeScript Configuration**:
- Strict mode enabled with `noUncheckedIndexedAccess`
- Target: ES2022
- No emit (webpack handles compilation)
- Preserves JSX and modules

### Data Flow

1. **In Block Editor**:
- User interacts with React controls in Inspector panel
- Controls update block attributes via `setAttributes`
- Attributes stored in block's `query` object with custom properties
- REST API requests use `rest_{post_type}_query` filters to preview results
- `Query_Params_Generator` processes custom params into WP_Query format

2. **On Frontend**:
- `pre_render_block` filter catches blocks with `namespace: 'advanced-query-loop'`
- For inherited queries: Modifies global `$wp_query` directly
- For non-inherited queries: Hooks into `query_loop_block_query_vars`
- `Query_Params_Generator` converts block attributes to WP_Query args
- `aql_query_vars` filter allows final modifications before query execution

### Extensibility

Developers can extend AQL in two ways:

1. **JavaScript SlotFills**: Add custom controls via `window.aql.AQLControls` or `window.aql.AQLControlsInheritedQuery`
2. **PHP Filter Hook**: Modify query arguments via `aql_query_vars` filter

See `extending-aql.md` for detailed examples.

## Testing

PHPUnit tests are located in `tests/unit/`. Configuration in `phpunit.xml` uses PHPUnit 8.5 with Yoast polyfills for PHP 7.4+ compatibility.

## Plugin Distribution

The plugin follows WordPress.org conventions:
- `readme.txt` - WordPress.org plugin readme
- `readme.md` - GitHub readme
- Main branch: `trunk`
- PHP namespace: `AdvancedQueryLoop\`
- Text domain: `advanced-query-loop`
16 changes: 16 additions & 0 deletions _blueprints/post-selection-e2e-blueprint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"constants": {
"SCRIPT_DEBUG": true
},
"steps": [
{
"step": "activatePlugin",
"pluginPath": "/wordpress/wp-content/plugins/advanced-query-loop/index.php"
},
{
"step": "wp-cli",
"command": "wp post generate --count=15 --post_type=post --post_status=publish"
}
]
}
11 changes: 8 additions & 3 deletions src/components/post-exclude-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useEntityRecord, store as coreDataStore } from '@wordpress/core-data';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';

Expand Down Expand Up @@ -163,6 +164,8 @@ const ExcludePostsControl = ( {
} = {},
} = attributes;

const [ searchArg, setSearchArg ] = useState( '' );

// Get the posts for all post types used in the query.
const posts = useSelect(
( select ) => {
Expand All @@ -171,16 +174,16 @@ const ExcludePostsControl = ( {
// Fetch posts for each post type and combine them into one array
return [ ...multiplePosts, postType ].reduce(
( accumulator, type ) => {
// Depending on the number of posts this could take a while, since we can't paginate here
const records = getEntityRecords( 'postType', type, {
per_page: -1,
per_page: 100,
search: searchArg,
} );
return [ ...accumulator, ...( records || [] ) ];
},
[]
);
},
[ postType, multiplePosts ]
[ postType, multiplePosts, searchArg ]
);

if ( ! allowedControls.includes( 'exclude_posts' ) ) {
Expand Down Expand Up @@ -217,6 +220,7 @@ const ExcludePostsControl = ( {
suggestions={ posts.map( ( post ) =>
decodeEntities( post.title.rendered.trim() )
) }
onInputChange={ ( searchPost ) => setSearchArg( searchPost ) }
onChange={ ( titles ) => {
// Converts the Titles to Post IDs before saving them
setAttributes( {
Expand All @@ -228,6 +232,7 @@ const ExcludePostsControl = ( {
) || [],
},
} );
setSearchArg( '' );
} }
__experimentalExpandOnFocus
__experimentalShowHowTo={ false }
Expand Down
2 changes: 1 addition & 1 deletion src/components/post-include-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const PostIncludeControls = ( {
'postType',
currentPostType,
{
per_page: 10,
per_page: 100,
search: searchArg,
exclude: excludeCurrent ? [ excludeCurrent ] : [],
}
Expand Down
131 changes: 131 additions & 0 deletions tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# E2E Tests for Advanced Query Loop

This directory contains end-to-end tests for the Advanced Query Loop plugin using Playwright.

## Setup

The tests use WordPress Playground to create isolated test environments. No local WordPress installation is required.

## Running Tests

### Run all e2e tests
```bash
npm run test:e2e
```

### Run tests with UI mode (interactive)
```bash
npm run test:e2e:ui
```

### Run a specific test file
```bash
npx playwright test tests/e2e/tests/post-selection.spec.ts --config=tests/e2e/playwright.config.ts
```

### Run tests in debug mode
```bash
npx playwright test --debug --config=tests/e2e/playwright.config.ts
```

## Test Structure

### Blueprints (`_blueprints/`)
Blueprint JSON files define the WordPress environment setup for tests:
- `e2e-blueprint.json` - Base blueprint that activates the plugin
- `post-selection-e2e-blueprint.json` - Creates 150 test posts for post selection tests

### Test Files (`tests/e2e/tests/`)
- `basic.spec.ts` - Basic plugin functionality tests
- `additional-post-types.spec.ts` - Multiple post type tests
- `pagination-toggle.spec.ts` - Pagination control tests
- `post-selection.spec.ts` - Post include/exclude controls with large content sets

### Fixtures (`aql-fixtures.ts`)
Custom Playwright fixtures that provide:
- `playground` - WordPress Playground instance
- `editor` - WordPress block editor utilities
- `admin` - WordPress admin utilities
- `selectors` - Custom selectors for AQL

### Utilities (`utils.ts`)
Helper functions for common test operations:
- `insertAQL()` - Inserts an Advanced Query Loop block

## Writing New Tests

1. Create a new `.spec.ts` file in `tests/e2e/tests/`
2. Import test fixtures: `import { test, expect } from '../aql-fixtures';`
3. Import utilities: `import { insertAQL } from '../utils';`
4. Follow the existing test patterns

Example:
```typescript
import { test, expect } from '../aql-fixtures';
import { insertAQL } from '../utils';

test.describe( 'My Feature Tests', () => {
test.beforeEach( async ( { page, editor, playground, admin } ) => {
await playground.init( { page, editor } );
await admin.visitAdminPage( 'post-new.php' );
await editor.setPreferences( 'core/edit-post', {
welcomeGuide: false,
fullscreenMode: false,
} );
await insertAQL( { editor, page } );
} );

test.afterEach( async ( { playground } ) => {
await playground.cleanUp();
} );

test( 'should do something', async ( { page, editor } ) => {
// Your test code here
} );
} );
```

## Creating Custom Blueprints

If your tests need specific content or configuration:

1. Create a new blueprint JSON file in `_blueprints/`
2. Use the `runPHP` step to execute PHP code for content creation
3. Reference it in your test: `new Playground( '_blueprints/your-blueprint.json' )`

Example blueprint with posts:
```json
{
"$schema": "https://playground.wordpress.net/blueprint-schema.json",
"steps": [
{
"step": "activatePlugin",
"pluginPath": "/wordpress/wp-content/plugins/advanced-query-loop/index.php"
},
{
"step": "runPHP",
"code": "<?php\\nfor ( $i = 1; $i <= 100; $i++ ) {\\n\\twp_insert_post( array(\\n\\t\\t'post_title' => 'Test Post ' . $i,\\n\\t\\t'post_status' => 'publish'\\n\\t) );\\n}\\n?>"
}
]
}
```

## Troubleshooting

### Tests are slow
- Reduce the number of posts created in blueprints
- Run tests in parallel with `--workers` flag (be careful with Playground instances)

### Tests fail intermittently
- Increase timeout values: `await page.waitForTimeout( 1000 );`
- Use `{ timeout: 10000 }` option on assertions
- Check if elements need to be scrolled into view

### Playground cleanup issues
- Ensure `playground.cleanUp()` is called in `afterEach`
- Check that no other processes are using port 8889
- Restart your terminal if ports remain occupied

## CI/CD

Tests run automatically on pull requests. Check the Actions tab on GitHub for results.
Loading
Loading