Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.2] - 2026-01-22

### Fixed
- Fixed folder counts not updating in sidebar after rule-based folder assignment on upload
- Now explicitly calls `vmfRefreshFolders()` after folder assignment to ensure accurate counts

## [1.1.1] - 2026-01-21

### Changed
Expand Down
1 change: 1 addition & 0 deletions build/media-upload.asset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array('dependencies' => array(), 'version' => 'afbe49c870543f9806fe');
1 change: 1 addition & 0 deletions build/media-upload.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 120 additions & 0 deletions docs/parent-plugin-integration-proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Parent Plugin Integration Proposal

**For:** virtual-media-folders
**From:** vmfa-rules-engine add-on
**Date:** 2025-01-21

## Problem Statement

When the rules engine add-on assigns a folder to an attachment during upload (via `wp_generate_attachment_metadata` filter), the parent plugin's UI doesn't reflect the change until the page is refreshed. This happens because:

1. WordPress adds the attachment to the media library collection in the frontend
2. The rules engine assigns a folder server-side during metadata generation
3. The frontend UI has no mechanism to know the folder assignment changed
4. The item appears in the wrong location until manual refresh

## Proposed Changes

### 1. Expose `refreshMediaLibrary()` globally

**File:** `src/admin/media-library.js`
**Location:** After the function definition (around line 291)

```javascript
window.vmfRefreshMediaLibrary = refreshMediaLibrary;
```

This allows add-ons to trigger a UI refresh when they've made server-side folder changes.

### 2. Add PHP action hook when folder is assigned

**File:** `src/Admin.php` (or wherever `wp_set_object_terms` is called for folders)

After successful folder assignment:

```php
/**
* Fires after a media item has been assigned to a folder.
*
* @since X.X.X
*
* @param int $attachment_id The attachment ID.
* @param int $folder_id The folder term ID.
* @param array $result The result from wp_set_object_terms.
*/
do_action( 'vmfo_folder_assigned', $attachment_id, $folder_id, $result );
```

### 3. (Optional) Expose single-attachment folder update

For more targeted updates without full refresh:

**File:** `src/admin/media-library.js`

```javascript
/**
* Update a single attachment's folder in the UI without full refresh.
* Useful for add-ons that assign folders programmatically.
*
* @param {number} attachmentId - The attachment post ID.
* @param {number} folderId - The target folder term ID.
*/
window.vmfUpdateAttachmentFolder = function(attachmentId, folderId) {
// Dispatch event so folder tree updates counts
window.dispatchEvent(new CustomEvent('vmf:folders-updated'));

// If we're viewing a specific folder and this attachment doesn't belong, remove it
const $selectedFolder = jQuery('.vmf-folder-button.is-selected');
if ($selectedFolder.length) {
const currentFolderId = $selectedFolder.data('folder-id');
if (currentFolderId && currentFolderId !== folderId) {
// The attachment moved to a different folder - trigger refresh
refreshMediaLibrary();
}
}
};
```

## How Add-ons Would Use These

### Example: Rules Engine triggering refresh after upload

```javascript
// In add-on's JavaScript
jQuery(document).on('vmfa:folder-assigned', function(e, data) {
if (window.vmfRefreshMediaLibrary) {
window.vmfRefreshMediaLibrary();
}
// Or dispatch the standard event
window.dispatchEvent(new CustomEvent('vmf:folders-updated'));
});
```

### Example: Listening to folder assignments

```php
// In add-on's PHP
add_action( 'vmfo_folder_assigned', function( $attachment_id, $folder_id, $result ) {
// React to folder assignments made by parent plugin
// e.g., log, sync, trigger additional processing
}, 10, 3 );
```

## Backward Compatibility

All proposed changes are additive:
- New global functions won't affect existing code
- New action hooks are optional for add-ons to use
- No changes to existing function signatures or behavior

## Priority

1. **High:** `window.vmfRefreshMediaLibrary` - Solves the immediate upload UX issue
2. **Medium:** `vmfo_folder_assigned` action - Enables better add-on integration
3. **Low:** `window.vmfUpdateAttachmentFolder` - Nice-to-have optimization

## Related

- Rules engine uses `wp_generate_attachment_metadata` filter to assign folders
- Counter updates work correctly (using `wp_update_term_count_now`)
- Only the UI refresh is missing
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "vmfa-rules-engine",
"version": "1.1.1",
"version": "1.1.2",
"description": "Rule-based automatic folder assignment for media uploads",
"author": "Per Soderlind",
"license": "GPL-2.0-or-later",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build src/js/index.js --output-path=build",
"start": "wp-scripts start src/js/index.js --output-path=build",
"build": "wp-scripts build src/js/index.js src/js/media-upload.js --output-path=build",
"start": "wp-scripts start src/js/index.js src/js/media-upload.js --output-path=build",
"lint:js": "wp-scripts lint-js src/js",
"lint:css": "wp-scripts lint-style src/js/styles",
"test": "vitest run",
Expand Down
6 changes: 5 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tags: media library, virtual folders, automation, rules engine, media organizati
Requires at least: 6.8
Tested up to: 6.8
Requires PHP: 8.3
Stable tag: 1.1.1
Stable tag: 1.1.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -108,6 +108,10 @@ Each imported file will be evaluated against your rules and assigned to the matc

== Changelog ==

= 1.1.2 =
* Fixed: Folder counts not updating in sidebar after rule-based folder assignment on upload
* Fixed: Now explicitly calls vmfRefreshFolders() after folder assignment to ensure accurate counts

= 1.1.1 =
* Changed: Replaced gear icon with settings icon for "Edit rule" button
* Changed: Replaced search icon with funnel icon for "Scan with this rule" button
Expand Down
82 changes: 82 additions & 0 deletions src/js/media-upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Media Upload Handler for Rules Engine.
*
* Hooks into WordPress media uploader to refresh the media library
* after uploads are processed by the rules engine.
*
* @package VmfaRulesEngine
*/

( function () {
'use strict';

/**
* Debounce timer for batching multiple uploads.
*
* @type {number|null}
*/
let refreshTimer = null;

/**
* Refresh the media library and folder counts with debouncing.
* Waits for multiple uploads to complete before refreshing.
*/
function scheduleRefresh() {
// Clear any pending refresh.
if ( refreshTimer ) {
clearTimeout( refreshTimer );
}

// Schedule a refresh after a short delay.
// This batches multiple rapid uploads into a single refresh.
refreshTimer = setTimeout( function () {
// Refresh folder counts in the sidebar.
// The main plugin (v1.6.2+) has an upload listener, but it may fire
// before the rules engine assigns the folder. Explicitly calling
// vmfRefreshFolders() ensures counts are accurate after assignment.
if ( typeof window.vmfRefreshFolders === 'function' ) {
window.vmfRefreshFolders();
}

// Refresh the media library grid if available.
if ( typeof window.vmfRefreshMediaLibrary === 'function' ) {
window.vmfRefreshMediaLibrary();
} else {
// Fallback: dispatch the folders-updated event.
window.dispatchEvent( new CustomEvent( 'vmf:folders-updated' ) );
}
refreshTimer = null;
}, 500 );
}

/**
* Initialize upload hooks when wp.Uploader is available.
*/
function init() {
// Check if wp.Uploader exists (media library page).
if ( typeof wp === 'undefined' || ! wp.Uploader ) {
return;
}

// Hook into the uploader's success callback.
const originalInit = wp.Uploader.prototype.init;
wp.Uploader.prototype.init = function () {
originalInit.apply( this, arguments );

// Listen for file upload success.
this.uploader.bind( 'FileUploaded', function () {
// Schedule a refresh after the rules engine processes the upload.
// The rules engine runs on wp_generate_attachment_metadata filter,
// which happens after the file is uploaded but before this callback.
scheduleRefresh();
} );
};
}

// Initialize when DOM is ready.
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
} )();
34 changes: 34 additions & 0 deletions src/php/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ private function init_hooks() {
add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
}

// Enqueue media upload handler on media library page.
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_media_upload_script' ) );
}

// REST API.
Expand Down Expand Up @@ -186,6 +189,37 @@ public function enqueue_admin_assets( $hook_suffix ) {
$this->do_enqueue_assets();
}

/**
* Enqueue media upload script on media library page.
*
* This script refreshes the media library after uploads
* when the rules engine has assigned a folder.
*
* @param string $hook_suffix The current admin page hook suffix.
* @return void
*/
public function enqueue_media_upload_script( $hook_suffix ): void {
if ( 'upload.php' !== $hook_suffix ) {
return;
}

$asset_file = VMFA_RULES_ENGINE_PATH . 'build/media-upload.asset.php';

if ( ! file_exists( $asset_file ) ) {
return;
}

$asset = require $asset_file;

wp_enqueue_script(
'vmfa-rules-engine-media-upload',
VMFA_RULES_ENGINE_URL . 'build/media-upload.js',
$asset[ 'dependencies' ],
$asset[ 'version' ],
true
);
}

/**
* Actually enqueue the scripts and styles.
*
Expand Down
6 changes: 6 additions & 0 deletions src/php/Services/RuleEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ public function assign_folder( $attachment_id, $folder_id, $rule = array() ) {
return false;
}

// Update term count immediately so parent plugin's UI reflects the change.
wp_update_term_count_now( $result, self::TAXONOMY );

// Clean object term cache to ensure the attachment shows in correct folder.
clean_object_term_cache( $attachment_id, 'attachment' );

/**
* Action fired after rule-based folder assignment.
*
Expand Down
8 changes: 8 additions & 0 deletions tests/Unit/Services/RuleEvaluatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ public function test_assign_folder_assigns_term(): void {
->with( 123, [ 5 ], 'vmfo_folder' )
->andReturn( [ 5 ] );

Functions\expect( 'wp_update_term_count_now' )
->once()
->with( [ 5 ], 'vmfo_folder' );

Functions\expect( 'clean_object_term_cache' )
->once()
->with( 123, 'attachment' );

Actions\expectDone( 'vmfa_rules_engine_folder_assigned' )
->once()
->with( 123, 5, Mockery::type( 'array' ) );
Expand Down
4 changes: 2 additions & 2 deletions vendor/composer/installed.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'name' => 'soderlind/vmfa-rules-engine',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '3b2e2d7a769b883b126a728417815d2616f55035',
'reference' => '92c6c335c9dc485e4e0bc4286e34948cff8f85a8',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
Expand All @@ -13,7 +13,7 @@
'soderlind/vmfa-rules-engine' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '3b2e2d7a769b883b126a728417815d2616f55035',
'reference' => '92c6c335c9dc485e4e0bc4286e34948cff8f85a8',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
Expand Down
4 changes: 2 additions & 2 deletions vmfa-rules-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Virtual Media Folders - Rules Engine
* Plugin URI: https://github.com/soderlind/vmfa-rules-engine
* Description: Rule-based automatic folder assignment for media uploads. Add-on for Virtual Media Folders.
* Version: 1.1.1
* Version: 1.1.2
* Author: Per Soderlind
* Author URI: https://soderlind.no
* License: GPL-2.0-or-later
Expand All @@ -20,7 +20,7 @@
defined( 'ABSPATH' ) || exit;

// Plugin constants.
define( 'VMFA_RULES_ENGINE_VERSION', '1.1.1' );
define( 'VMFA_RULES_ENGINE_VERSION', '1.1.2' );
define( 'VMFA_RULES_ENGINE_FILE', __FILE__ );
define( 'VMFA_RULES_ENGINE_PATH', plugin_dir_path( __FILE__ ) );
define( 'VMFA_RULES_ENGINE_URL', plugin_dir_url( __FILE__ ) );
Expand Down