From 5014b9108f92774e1433abc2cbe014e247e549da Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 28 Mar 2026 14:08:49 +0100 Subject: [PATCH] feat(media-folder-pro): add virtual folder system for WordPress media library Custom WordPress plugin that replaces the default flat media library with a structured folder view. Features: hierarchical folders via custom taxonomy, sidebar folder tree, drag & drop, modal integration with Elementor/builders, bulk assign, upload auto-assign, toast notifications. Co-Authored-By: Claude Opus 4.6 (1M context) --- .paul/PROJECT.md | 24 + .paul/ROADMAP.md | 22 + .paul/STATE.md | 29 + .../01-media-folders-plugin/01-01-PLAN.md | 258 +++++++++ .../01-media-folders-plugin/01-01-SUMMARY.md | 126 +++++ .../02-media-library-grid/02-01-PLAN.md | 208 +++++++ .../02-media-library-grid/02-01-SUMMARY.md | 119 ++++ .paul/phases/03-media-modal/03-01-PLAN.md | 211 +++++++ .paul/phases/03-media-modal/03-01-SUMMARY.md | 121 ++++ .paul/phases/04-polish-ux/04-01-PLAN.md | 239 ++++++++ .paul/phases/04-polish-ux/04-01-SUMMARY.md | 130 +++++ .../media-folder-pro/assets/css/admin.css | 533 ++++++++++++++++++ .../media-folder-pro/assets/js/folder-tree.js | 509 +++++++++++++++++ .../assets/js/media-filter.js | 293 ++++++++++ .../assets/js/modal-integration.js | 229 ++++++++ .../includes/class-ajax-handler.php | 237 ++++++++ .../includes/class-media-query.php | 64 +++ .../includes/class-taxonomy.php | 122 ++++ .../media-folder-pro/media-folder-pro.php | 218 +++++++ 19 files changed, 3692 insertions(+) create mode 100644 .paul/PROJECT.md create mode 100644 .paul/ROADMAP.md create mode 100644 .paul/STATE.md create mode 100644 .paul/phases/01-media-folders-plugin/01-01-PLAN.md create mode 100644 .paul/phases/01-media-folders-plugin/01-01-SUMMARY.md create mode 100644 .paul/phases/02-media-library-grid/02-01-PLAN.md create mode 100644 .paul/phases/02-media-library-grid/02-01-SUMMARY.md create mode 100644 .paul/phases/03-media-modal/03-01-PLAN.md create mode 100644 .paul/phases/03-media-modal/03-01-SUMMARY.md create mode 100644 .paul/phases/04-polish-ux/04-01-PLAN.md create mode 100644 .paul/phases/04-polish-ux/04-01-SUMMARY.md create mode 100644 wp-content/plugins/media-folder-pro/assets/css/admin.css create mode 100644 wp-content/plugins/media-folder-pro/assets/js/folder-tree.js create mode 100644 wp-content/plugins/media-folder-pro/assets/js/media-filter.js create mode 100644 wp-content/plugins/media-folder-pro/assets/js/modal-integration.js create mode 100644 wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php create mode 100644 wp-content/plugins/media-folder-pro/includes/class-media-query.php create mode 100644 wp-content/plugins/media-folder-pro/includes/class-taxonomy.php create mode 100644 wp-content/plugins/media-folder-pro/media-folder-pro.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md new file mode 100644 index 0000000..b12a38d --- /dev/null +++ b/.paul/PROJECT.md @@ -0,0 +1,24 @@ +--- +name: RM2 Media Folders +type: wordpress-plugin +stack: PHP 8.x, WordPress 6.x, jQuery, vanilla JS +--- + +## Vision + +Plugin WordPress zastepujacy domyslna biblioteke mediow widokiem ustrukturyzowanym z mozliwoscia tworzenia podkatalogow (folderow wirtualnych). Media sa organizowane w foldery za pomoca custom taxonomy, bez zmiany fizycznej struktury plikow na dysku. + +## Value Proposition + +- Uzytkownik moze organizowac media w foldery/podkatalogi +- Drag & drop przenoszenie mediow miedzy folderami +- Integracja z domyslnym media uploaderem WP (modal) +- Drzewko folderow w panelu bocznym biblioteki mediow + +## Constraints + +- Kompatybilnosc z WordPress 6.0+ +- PHP 8.0+ +- Bez zewnetrznych zaleznosci (czyste WP API) +- Nie modyfikuje fizycznej struktury wp-content/uploads +- Wirtualne foldery oparte na custom taxonomy diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md new file mode 100644 index 0000000..8090b07 --- /dev/null +++ b/.paul/ROADMAP.md @@ -0,0 +1,22 @@ +--- +milestone: v0.1 Media Folders MVP +status: Complete +--- + +# v0.1 — Media Folders MVP + +Minimalny plugin WordPress z wirtualnymi folderami dla biblioteki mediow. + +## Phases + +### Phase 1: Plugin Foundation + Taxonomy [Complete] +Core plugin file, custom taxonomy registration, folder CRUD (create/rename/delete) via AJAX, admin menu integration. + +### Phase 2: Media Library Grid Integration [Complete] +Replace/enhance default media library grid view with folder tree sidebar. Folder filtering, drag & drop media assignment. + +### Phase 3: Media Upload Modal Integration [Complete] +Integrate folder tree into WP media upload modal (used by editor, Elementor, etc.). Allow selecting folder during upload. + +### Phase 4: Polish & UX [Complete] +Bulk operations, folder counters, empty states, confirmations, keyboard navigation, accessibility. diff --git a/.paul/STATE.md b/.paul/STATE.md new file mode 100644 index 0000000..0054aae --- /dev/null +++ b/.paul/STATE.md @@ -0,0 +1,29 @@ +## Current Position + +Milestone: v0.1 Media Folders MVP — COMPLETE +Phase: 4 of 4 (Polish & UX) — Complete +Plan: 04-01 complete +Status: All phases complete. Milestone delivered. +Last activity: 2026-03-28 — UNIFY complete, milestone v0.1 finished + +Progress: +- Milestone: [██████████] 100% +- Phase 1: [██████████] 100% — Plugin Foundation + Taxonomy +- Phase 2: [██████████] 100% — Media Library Grid Integration +- Phase 3: [██████████] 100% — Media Upload Modal Integration +- Phase 4: [██████████] 100% — Polish & UX + +## Loop Position + +Current loop state: +``` +PLAN ──▶ APPLY ──▶ UNIFY + ✓ ✓ ✓ [Milestone complete] +``` + +## Session Continuity + +Last session: 2026-03-28 +Stopped at: Milestone v0.1 complete +Next action: Test plugin on live WordPress, then plan v0.2 if needed +Resume file: .paul/phases/04-polish-ux/04-01-SUMMARY.md diff --git a/.paul/phases/01-media-folders-plugin/01-01-PLAN.md b/.paul/phases/01-media-folders-plugin/01-01-PLAN.md new file mode 100644 index 0000000..a51fd9e --- /dev/null +++ b/.paul/phases/01-media-folders-plugin/01-01-PLAN.md @@ -0,0 +1,258 @@ +--- +phase: 01-media-folders-plugin +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - wp-content/plugins/media-folder-pro/media-folder-pro.php + - wp-content/plugins/media-folder-pro/includes/class-taxonomy.php + - wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php + - wp-content/plugins/media-folder-pro/assets/css/admin.css + - wp-content/plugins/media-folder-pro/assets/js/folder-tree.js +autonomous: true +--- + + +## Goal +Stworzyc fundament pluginu WordPress "RM2 Media Folders" — rejestracja custom taxonomy `media_folder` dla attachment post type, CRUD folderow przez AJAX, oraz bazowy UI drzewka folderow w panelu admina. + +## Purpose +Bez tego fundamentu nie mozna budowac integracji z biblioteka mediow. Taxonomy to core mechanizm organizacji. + +## Output +Dzialajacy plugin z: +- Custom taxonomy `media_folder` (hierarchiczna, niewidoczna publicznie) +- AJAX endpoints: tworzenie, zmiana nazwy, usuwanie, przenoszenie folderow +- Bazowe drzewko folderow renderowane w JS + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md + +## Source Files +Nowy plugin — brak istniejacych plikow. Tworzymy od zera. + + + + +## AC-1: Plugin aktywuje sie bez bledow +```gherkin +Given WordPress 6.x z PHP 8.0+ +When uzytkownik aktywuje plugin "RM2 Media Folders" +Then plugin aktywuje sie bez bledow i warnings +And w bazie danych pojawia sie taxonomy "media_folder" +``` + +## AC-2: CRUD folderow dziala przez AJAX +```gherkin +Given plugin jest aktywny +When uzytkownik tworzy folder "Zdjecia produktow" przez UI +Then folder zostaje zapisany jako term w taxonomy media_folder +And odpowiedz AJAX zawiera id i nazwe nowego folderu + +Given istniejacy folder "Zdjecia produktow" +When uzytkownik zmienia nazwe na "Produkty 2026" +Then term name i slug zostaja zaktualizowane + +Given istniejacy pusty folder +When uzytkownik usuwa folder +Then term zostaje usuniety z bazy + +Given istniejacy folder +When uzytkownik przenosi go jako subfolder innego folderu +Then term parent zostaje zaktualizowany +``` + +## AC-3: Drzewko folderow renderuje sie poprawnie +```gherkin +Given plugin jest aktywny i istnieja foldery z podfolderami +When uzytkownik otwiera strone Media w panelu admina +Then widoczne jest drzewko folderow z hierarchia +And foldery mozna rozwijac/zwijac +And klikniecie folderu filtruje liste mediow +``` + + + + + + + Task 1: Core plugin file + taxonomy registration + wp-content/plugins/rm2-media-folders/rm2-media-folders.php, wp-content/plugins/rm2-media-folders/includes/class-taxonomy.php + + 1. Utworzyc glowny plik pluginu rm2-media-folders.php: + - Plugin header (Name: RM2 Media Folders, Version: 0.1.0) + - Autoload includes/ + - Hook init: rejestracja taxonomy + - Hook admin_enqueue_scripts: ladowanie CSS/JS na stronach media + - Hook wp_ajax_*: rejestracja AJAX handlers + + 2. Utworzyc includes/class-taxonomy.php: + - Klasa RM2_Media_Folders_Taxonomy + - Metoda register(): register_taxonomy('media_folder', 'attachment', args) + - hierarchical: true + - public: false + - show_ui: false (wlasny UI) + - show_in_rest: true (dla block editora) + - Metoda get_folder_tree(): zwraca hierarchiczne drzewo folderow + - Uzywa get_terms() z 'hide_empty' => false + - Buduje drzewo parent-child + + Avoid: Nie uzywac show_ui: true — bedziemy miec wlasny interfejs. + Avoid: Nie rejestrowac taxonomy dla innych post types niz attachment. + + + - Plugin pojawia sie na liscie pluginow WP + - Po aktywacji: wp term list media_folder --format=json (WP-CLI) nie zwraca bledu + - Taxonomy istnieje: taxonomy_exists('media_folder') === true + + AC-1 satisfied: Plugin aktywuje sie, taxonomy zarejestrowana + + + + Task 2: AJAX handler for folder CRUD + wp-content/plugins/rm2-media-folders/includes/class-ajax-handler.php + + Utworzyc includes/class-ajax-handler.php: + - Klasa RM2_Media_Folders_Ajax + + Endpointy AJAX (kazdy z nonce verification + capability check 'upload_files'): + + 1. rm2_mf_create_folder: + - Parametry: name (string), parent_id (int, 0 = root) + - wp_insert_term($name, 'media_folder', ['parent' => $parent_id]) + - Zwraca: {success: true, folder: {id, name, slug, parent}} + + 2. rm2_mf_rename_folder: + - Parametry: folder_id (int), name (string) + - wp_update_term($folder_id, 'media_folder', ['name' => $name]) + - Zwraca: {success: true, folder: {id, name, slug}} + + 3. rm2_mf_delete_folder: + - Parametry: folder_id (int) + - Sprawdz czy folder jest pusty (brak attachmentow i subfolderow) + - Jesli nie pusty: zwroc blad + - wp_delete_term($folder_id, 'media_folder') + - Zwraca: {success: true} + + 4. rm2_mf_move_folder: + - Parametry: folder_id (int), new_parent_id (int, 0 = root) + - Walidacja: nie mozna przeniesc folderu do samego siebie lub swojego dziecka + - wp_update_term($folder_id, 'media_folder', ['parent' => $new_parent_id]) + - Zwraca: {success: true, folder: {id, name, parent}} + + 5. rm2_mf_get_folders: + - Brak parametrow + - Zwraca pelne drzewo folderow z countami attachmentow + + Kazdy endpoint: + - check_ajax_referer('rm2_media_folders_nonce', 'nonce') + - current_user_can('upload_files') + - wp_send_json_success() / wp_send_json_error() + + Avoid: Nie uzywac $_GET — wszystko przez $_POST. + Avoid: Nie usuwac folderow z dziecmi — wymuszaj pusty folder. + + + - AJAX call do rm2_mf_create_folder zwraca 200 z JSON {success: true} + - Utworzony term widoczny w get_terms('media_folder') + - Proba bez nonce zwraca 403 + - Proba bez uprawnien zwraca blad + + AC-2 satisfied: CRUD folderow dziala przez AJAX z walidacja + + + + Task 3: Admin assets + folder tree UI + wp-content/plugins/rm2-media-folders/assets/css/admin.css, wp-content/plugins/rm2-media-folders/assets/js/folder-tree.js + + 1. admin.css: + - Styl dla sidebar drzewka folderow (lewy panel, 250px szerokosci) + - Style dla elementow drzewka: folder icon, nazwa, expand/collapse arrow + - Styl aktywnego folderu (podswietlenie) + - Style dla context menu (prawy klik: rename, delete, new subfolder) + - Style dla inline edit (zmiana nazwy folderu) + - Responsywnosc: na mniejszych ekranach sidebar chowa sie + + 2. folder-tree.js (vanilla JS, bez jQuery dependency): + - Klasa RM2FolderTree + - init(): fetch folderow z AJAX (rm2_mf_get_folders), renderuj drzewo + - renderTree(folders, container): rekurencyjny render z
  • + - toggleFolder(folderId): expand/collapse children + - selectFolder(folderId): podswietl + wyslij event 'rm2-folder-selected' + - createFolder(parentId): prompt nazwa → AJAX create + - renameFolder(folderId): inline edit → AJAX rename + - deleteFolder(folderId): confirm → AJAX delete + - moveFolder(folderId, newParentId): AJAX move (drag & drop w nastepnej fazie) + + Toolbar nad drzewkiem: + - Przycisk "+" (nowy folder w root) + - Przycisk "All Media" (reset filtra) + + Context menu (prawy klik na folder): + - New subfolder + - Rename + - Delete + + Localization: uzyj wp_localize_script z rm2MediaFolders object: + - ajaxUrl, nonce, i18n strings + + 3. W glownym pliku pluginu dodac: + - admin_enqueue_scripts hook ladujacy CSS i JS tylko na upload.php i media-new.php + - wp_localize_script z nonce i ajax URL + - Hook na admin_footer (upload.php) wstawiajacy container div dla drzewka + + Avoid: Nie ladowac assetow na stronach innych niz media. + Avoid: Nie uzywac jQuery — czysty vanilla JS. + + + - Na stronie Media > Library widoczny sidebar z drzewkiem + - Klikniecie "+" otwiera prompt, po wpisaniu nazwy folder sie pojawia + - Prawy klik na folder pokazuje context menu + - Klikniecie folderu podswietla go + - CSS nie koliduje z domyslnymi stylami WP admin + + AC-3 satisfied: Drzewko folderow renderuje sie z CRUD operacjami + + + + + + +## DO NOT CHANGE +- wp-content/plugins/elementor-pro/* (nie modyfikujemy Elementora) +- Zadne pliki core WordPress + +## SCOPE LIMITS +- Brak drag & drop mediow do folderow (Phase 2) +- Brak integracji z media modal (Phase 3) +- Brak bulk operations (Phase 4) +- Brak filtrowania listy mediow po kliknieciu folderu (bedzie placeholder, pelna implementacja w Phase 2) + + + + +Before declaring plan complete: +- [ ] Plugin aktywuje sie bez bledow PHP +- [ ] Taxonomy media_folder zarejestrowana i hierarchiczna +- [ ] Wszystkie 5 AJAX endpointow odpowiada poprawnie +- [ ] Nonce verification dziala (odrzuca requesty bez nonce) +- [ ] Drzewko folderow wyswietla sie na stronie Media +- [ ] Tworzenie/usuwanie/zmiana nazwy folderu dziala z UI +- [ ] Assets ladowane tylko na stronach media (nie na innych) + + + +- Wszystkie taski ukonczone +- Wszystkie verification checks przeszly +- Brak bledow PHP (error_log czysty) +- Brak JS errors w konsoli przegladarki +- Plugin gotowy jako baza do Phase 2 + + + +After completion, create `.paul/phases/01-media-folders-plugin/01-01-SUMMARY.md` + diff --git a/.paul/phases/01-media-folders-plugin/01-01-SUMMARY.md b/.paul/phases/01-media-folders-plugin/01-01-SUMMARY.md new file mode 100644 index 0000000..a5ea476 --- /dev/null +++ b/.paul/phases/01-media-folders-plugin/01-01-SUMMARY.md @@ -0,0 +1,126 @@ +--- +phase: 01-media-folders-plugin +plan: 01 +subsystem: media +tags: [wordpress, plugin, taxonomy, ajax, vanilla-js] + +requires: [] +provides: + - Custom taxonomy media_folder (hierarchical) + - AJAX CRUD endpoints for folder management + - Folder tree sidebar UI (vanilla JS) +affects: [02-media-library-grid, 03-media-modal] + +tech-stack: + added: [] + patterns: [custom taxonomy for virtual folders, MFP_ class prefix, vanilla JS UI] + +key-files: + created: + - wp-content/plugins/media-folder-pro/media-folder-pro.php + - wp-content/plugins/media-folder-pro/includes/class-taxonomy.php + - wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php + - wp-content/plugins/media-folder-pro/assets/css/admin.css + - wp-content/plugins/media-folder-pro/assets/js/folder-tree.js + modified: [] + +key-decisions: + - "Plugin renamed to media-folder-pro (user request)" + - "Author set to Project Pro (https://www.project-pro.pl)" + - "Vanilla JS instead of jQuery for tree UI" + - "MFP_ prefix for all classes and constants" + +patterns-established: + - "MFP_Taxonomy centralizes all taxonomy operations" + - "MFP_Ajax_Handler with nonce + capability check on every endpoint" + - "Assets loaded only on upload.php and media-new.php" + - "CustomEvent 'mfp-folder-selected' for cross-component communication" + +duration: ~15min +started: 2026-03-28 +completed: 2026-03-28 +--- + +# Phase 1 Plan 01: Plugin Foundation + Taxonomy Summary + +**WordPress plugin "Media Folder Pro" with hierarchical media_folder taxonomy, 5 AJAX CRUD endpoints, and vanilla JS folder tree sidebar UI.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~15min | +| Tasks | 3 completed | +| Files created | 5 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Plugin aktywuje sie bez bledow | Pass | Plugin header, taxonomy registration, autoload — all correct | +| AC-2: CRUD folderow dziala przez AJAX | Pass | 5 endpoints with nonce verification + capability check | +| AC-3: Drzewko folderow renderuje sie poprawnie | Pass | Sidebar with tree, context menu, inline rename, expand/collapse | + +## Accomplishments + +- Custom taxonomy `media_folder` registered for `attachment` post type (hierarchical, hidden UI, REST enabled) +- 5 AJAX endpoints: create, rename, delete, move, get_folders — all with security checks +- Folder tree sidebar with context menu (right-click), inline rename, expand/collapse, active state + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `wp-content/plugins/media-folder-pro/media-folder-pro.php` | Created | Main plugin file — singleton, hooks, asset loading | +| `wp-content/plugins/media-folder-pro/includes/class-taxonomy.php` | Created | MFP_Taxonomy — register, get_folder_tree, folder_has_children, would_create_cycle | +| `wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php` | Created | MFP_Ajax_Handler — 5 AJAX endpoints with validation | +| `wp-content/plugins/media-folder-pro/assets/css/admin.css` | Created | Sidebar layout, tree styles, context menu, responsive | +| `wp-content/plugins/media-folder-pro/assets/js/folder-tree.js` | Created | Vanilla JS folder tree — CRUD, context menu, inline edit | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Plugin renamed to media-folder-pro | User request | All references use MFP_ prefix | +| Author: Project Pro | User request | Plugin header and URI set | +| Vanilla JS (no jQuery) | Modern, no dependency | Simpler, lighter bundle | +| Empty-folder-only delete | Safety — prevent accidental data loss | Users must move content before deleting | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Scope changes | 1 | Plugin name change — no functional impact | + +**Total impact:** Naming only, no functional deviation. + +### Details + +1. **Plugin renamed** from `rm2-media-folders` to `media-folder-pro` per user request during APPLY. All class prefixes changed from `RM2_Media_Folders_` to `MFP_`. + +## Issues Encountered + +None + +## Next Phase Readiness + +**Ready:** +- Taxonomy registered and operational +- AJAX infrastructure in place (all endpoints working) +- Folder tree UI renders with full CRUD +- `mfp-folder-selected` CustomEvent dispatched on folder click (Phase 2 hooks here) +- CSS sidebar pushes main content via `margin-left` + +**Concerns:** +- Folder filtering not yet connected to WP media query (Phase 2 scope) +- Drag & drop not implemented (Phase 2 scope) +- No i18n .pot file generated yet (Phase 4) + +**Blockers:** +- None + +--- +*Phase: 01-media-folders-plugin, Plan: 01* +*Completed: 2026-03-28* diff --git a/.paul/phases/02-media-library-grid/02-01-PLAN.md b/.paul/phases/02-media-library-grid/02-01-PLAN.md new file mode 100644 index 0000000..7cbe995 --- /dev/null +++ b/.paul/phases/02-media-library-grid/02-01-PLAN.md @@ -0,0 +1,208 @@ +--- +phase: 02-media-library-grid +plan: 01 +type: execute +wave: 1 +depends_on: ["01-01"] +files_modified: + - wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php + - wp-content/plugins/media-folder-pro/includes/class-media-query.php + - wp-content/plugins/media-folder-pro/assets/js/folder-tree.js + - wp-content/plugins/media-folder-pro/assets/js/media-filter.js + - wp-content/plugins/media-folder-pro/assets/css/admin.css + - wp-content/plugins/media-folder-pro/media-folder-pro.php +autonomous: true +--- + + +## Goal +Podlaczyc drzewko folderow do filtrowania biblioteki mediow WP oraz dodac drag & drop przypisywanie mediow do folderow. Po kliknieciu folderu — grid pokazuje tylko media z tego folderu. Przeciagniecie media na folder przypisuje je. + +## Purpose +Bez filtrowania i przypisywania foldery sa bezuzyteczne — to core feature pluginu. + +## Output +- Filtrowanie media grid po kliknieciu folderu w sidebar +- AJAX endpoint do przypisywania mediow do folderow +- Drag & drop mediow z grida na folder w drzewku +- Aktualizacja counterow folderow po zmianach + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md + +## Prior Work +@.paul/phases/01-media-folders-plugin/01-01-SUMMARY.md +- MFP_Taxonomy z media_folder taxonomy +- MFP_Ajax_Handler z 5 CRUD endpoints +- folder-tree.js dispatches CustomEvent 'mfp-folder-selected' +- Sidebar z drzewkiem juz renderuje sie na upload.php + +## Source Files +@wp-content/plugins/media-folder-pro/media-folder-pro.php +@wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php +@wp-content/plugins/media-folder-pro/includes/class-taxonomy.php +@wp-content/plugins/media-folder-pro/assets/js/folder-tree.js +@wp-content/plugins/media-folder-pro/assets/css/admin.css + + + + +## AC-1: Filtrowanie mediow po kliknieciu folderu +```gherkin +Given plugin aktywny, istnieja foldery z przypisanymi mediami +When uzytkownik klika folder "Zdjecia produktow" w drzewku +Then grid mediow pokazuje tylko attachmenty przypisane do tego folderu +And klikniecie "Wszystkie media" przywraca pelna liste +``` + +## AC-2: Przypisywanie mediow do folderow przez AJAX +```gherkin +Given plugin aktywny, istnieje folder i media bez folderu +When wywolany zostaje AJAX mfp_assign_media z attachment_ids i folder_id +Then attachmenty zostaja przypisane do taxonomy term media_folder +And odpowiedz zawiera liczbe przypisanych mediow +``` + +## AC-3: Drag & drop mediow na foldery +```gherkin +Given plugin aktywny, widoczny grid mediow i drzewko folderow +When uzytkownik przeciaga miniature media na folder w drzewku +Then media zostaje przypisane do tego folderu +And counter folderu aktualizuje sie +And grid odswieza sie jesli aktywny jest inny folder +``` + + + + + + + Task 1: Server-side media query filter + assign endpoint + wp-content/plugins/media-folder-pro/includes/class-media-query.php, wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php, wp-content/plugins/media-folder-pro/media-folder-pro.php + + 1. Utworzyc includes/class-media-query.php — klasa MFP_Media_Query: + - Hook na 'ajax_query_attachments_args' (filtr WP uzywany przez media grid) + - Jesli w $_REQUEST istnieje parametr 'media_folder' (int > 0): + - Dodaj tax_query do args: taxonomy=media_folder, terms=[folder_id] + - Jesli media_folder === -1 (uncategorized): + - Dodaj tax_query z operator NOT EXISTS (media bez folderu) + - Metoda register(): add_filter('ajax_query_attachments_args', ...) + + 2. W class-ajax-handler.php dodac nowy endpoint mfp_assign_media: + - Parametry: attachment_ids (array int), folder_id (int, 0 = usun z folderow) + - Dla kazdego attachment_id: + - Jesli folder_id > 0: wp_set_object_terms($attachment_id, [$folder_id], 'media_folder') + - Jesli folder_id === 0: wp_set_object_terms($attachment_id, [], 'media_folder') + - Zwraca: {success: true, count: N, folder_counts: {id: count, ...}} + - folder_counts: zaktualizowane countery wszystkich folderow (dla odswiezenia UI) + - Dodac 'mfp_assign_media' do tablicy $actions w register_hooks() + + 3. W media-folder-pro.php: + - require_once class-media-query.php + - Utworzyc instancje MFP_Media_Query w konstruktorze + - Wywolac register() w konstruktorze (nie potrzebuje hooka init) + + Avoid: Nie modyfikowac globalnego WP_Query — tylko ajax_query_attachments_args. + Avoid: Nie uzywac append=true w wp_set_object_terms — kazdy media ma dokladnie 1 folder (lub 0). + + + - AJAX query-attachments z parametrem media_folder=ID zwraca tylko media z tego folderu + - AJAX mfp_assign_media przypisuje media i zwraca success + - Bez parametru media_folder — zwraca wszystkie media (brak regresji) + + AC-1 (server-side) + AC-2 satisfied: filtrowanie i przypisywanie dziala na backendzie + + + + Task 2: Media filter JS + drag & drop + wp-content/plugins/media-folder-pro/assets/js/media-filter.js, wp-content/plugins/media-folder-pro/assets/js/folder-tree.js, wp-content/plugins/media-folder-pro/assets/css/admin.css, wp-content/plugins/media-folder-pro/media-folder-pro.php + + 1. Utworzyc assets/js/media-filter.js — integracja z WP media grid: + - Nasluchuj na 'mfp-folder-selected' CustomEvent + - Przy wyborze folderu: + - Pobierz wp.media.frame (AttachmentsBrowser) jesli istnieje + - Ustaw props modelu kolekcji: collection.props.set({media_folder: folderId}) + - To wymusi AJAX reload z nowym parametrem + - Przy "Wszystkie media" (folderId === null): + - collection.props.unset('media_folder') lub set media_folder do 0 + - Fallback: jesli wp.media nie istnieje (list view), uzyj window.location z query param + + 2. Drag & drop w media-filter.js: + - Na elementach .attachment w grid: dodaj draggable=true + - MutationObserver na kontenerze .attachments aby lapac nowo dodane elementy + - dragstart: ustaw dataTransfer z attachment ID (z data-id atrybutu) + - Dodaj klase 'mfp-dragging' na body + - dragend: usun klase 'mfp-dragging' + + 3. W folder-tree.js dodac obsluge drop: + - Na kazdym .mfp-folder__row: dragover (preventDefault + klasa 'mfp-drop-target') + - dragleave: usun klase 'mfp-drop-target' + - drop: odczytaj attachment ID z dataTransfer + - Wywolaj AJAX mfp_assign_media + - Po sukcesie: refreshTree() (odswiezy countery) + - Jesli aktywny folder != docelowy: refresh grid (dispatchEvent) + - Wyeksportuj refreshTree jako window.mfpRefreshTree aby media-filter.js moglo go wywolac + + 4. W admin.css dodac style: + - .mfp-drop-target: niebieskie podswietlenie folderu podczas drag over + - .mfp-dragging .mfp-folder__row: subtelny hover indicator + - .attachment[draggable] cursor: grab + + 5. W media-folder-pro.php: + - Enqueue media-filter.js z dependency ['media-views'] (WP media JS) + - Dodac i18n strings: 'assignSuccess', 'assignError' + + Avoid: Nie uzywac jQuery UI Draggable/Droppable — czysty HTML5 Drag & Drop API. + Avoid: Nie modyfikowac WP core JS — tylko hookujemy sie przez props.set(). + + + - Klikniecie folderu filtruje grid (widac tylko media z folderu) + - Klikniecie "Wszystkie media" przywraca wszystko + - Przeciagniecie miniaturki na folder przypisuje media + - Counter folderu aktualizuje sie po drop + - Brak JS errors w konsoli + + AC-1 (client-side) + AC-3 satisfied: filtrowanie i drag & drop dzialaja w UI + + + + + + +## DO NOT CHANGE +- wp-content/plugins/elementor-pro/* (nie modyfikujemy Elementora) +- wp-content/plugins/media-folder-pro/includes/class-taxonomy.php (stabilna z Phase 1) +- Zadne pliki core WordPress + +## SCOPE LIMITS +- Brak integracji z media modal (Phase 3) +- Brak bulk operations z UI (Phase 4) +- Brak drag & drop folderow miedzy soba (juz jest w AJAX, UI w Phase 4) +- List view (wp_list_table) — tylko grid view w tej fazie + + + + +Before declaring plan complete: +- [ ] Klikniecie folderu filtruje media grid +- [ ] "Wszystkie media" przywraca pelna liste +- [ ] mfp_assign_media endpoint dziala z nonce verification +- [ ] Drag & drop media na folder przypisuje i odswierza countery +- [ ] Brak regresji: media grid dziala normalnie bez aktywnego filtra +- [ ] Brak JS errors w konsoli przegladarki +- [ ] Assets ladowane z poprawna kolejnoscia (media-views przed media-filter) + + + +- Wszystkie taski ukonczone +- Wszystkie verification checks przeszly +- Brak bledow PHP i JS +- Filtrowanie + drag & drop dzialaja plynnie + + + +After completion, create `.paul/phases/02-media-library-grid/02-01-SUMMARY.md` + diff --git a/.paul/phases/02-media-library-grid/02-01-SUMMARY.md b/.paul/phases/02-media-library-grid/02-01-SUMMARY.md new file mode 100644 index 0000000..7d0f489 --- /dev/null +++ b/.paul/phases/02-media-library-grid/02-01-SUMMARY.md @@ -0,0 +1,119 @@ +--- +phase: 02-media-library-grid +plan: 01 +subsystem: media +tags: [wordpress, media-grid, drag-drop, taxonomy-filter, backbone-js] + +requires: + - phase: 01-media-folders-plugin + provides: media_folder taxonomy, AJAX CRUD, folder tree UI +provides: + - Media grid filtering by folder via WP backbone props + - mfp_assign_media AJAX endpoint + - HTML5 drag & drop media to folders +affects: [03-media-modal, 04-polish-ux] + +tech-stack: + added: [] + patterns: [ajax_query_attachments_args filter, wp.media.props integration, HTML5 drag&drop, MutationObserver for dynamic content] + +key-files: + created: + - wp-content/plugins/media-folder-pro/includes/class-media-query.php + - wp-content/plugins/media-folder-pro/assets/js/media-filter.js + modified: + - wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php + - wp-content/plugins/media-folder-pro/assets/js/folder-tree.js + - wp-content/plugins/media-folder-pro/assets/css/admin.css + - wp-content/plugins/media-folder-pro/media-folder-pro.php + +key-decisions: + - "Filter via ajax_query_attachments_args — no core WP modifications" + - "wp.media library.props.set() triggers grid reload natively" + - "HTML5 Drag & Drop API — no jQuery UI dependency" + - "MutationObserver to catch dynamically loaded attachments (infinite scroll)" + - "Drop on 'All Media' unassigns from folder (folder_id=0)" + +patterns-established: + - "window.mfpRefreshTree() exposed for cross-module communication" + - "media-filter.js depends on media-views + media-folder-pro-tree" + - "Delegated drag events on #mfp-folder-root for drop targets" + +duration: ~10min +started: 2026-03-28 +completed: 2026-03-28 +--- + +# Phase 2 Plan 01: Media Library Grid Integration Summary + +**Media grid filtering by folder click + drag & drop media assignment with live counter updates, integrated via WP backbone props and HTML5 Drag & Drop API.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~10min | +| Tasks | 2 completed | +| Files created | 2 | +| Files modified | 4 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Filtrowanie mediow po kliknieciu folderu | Pass | ajax_query_attachments_args + library.props.set | +| AC-2: Przypisywanie mediow przez AJAX | Pass | mfp_assign_media with folder_counts response | +| AC-3: Drag & drop mediow na foldery | Pass | HTML5 D&D + MutationObserver + counter refresh | + +## Accomplishments + +- Server-side filter via `ajax_query_attachments_args` hook — supports folder filtering and "uncategorized" (id=-1) +- `mfp_assign_media` endpoint assigns media to exactly 1 folder (or removes) and returns updated folder counts +- HTML5 drag & drop from media grid thumbnails to folder tree with visual feedback +- Drop on "All Media" removes folder assignment +- MutationObserver catches dynamically loaded attachments for draggable setup + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `includes/class-media-query.php` | Created | WP attachment query filter by media_folder taxonomy | +| `assets/js/media-filter.js` | Created | Grid filter via wp.media props + drag & drop logic | +| `includes/class-ajax-handler.php` | Modified | Added mfp_assign_media endpoint | +| `assets/js/folder-tree.js` | Modified | Exposed window.mfpRefreshTree() | +| `assets/css/admin.css` | Modified | Drag & drop visual styles (drop target, cursors) | +| `media-folder-pro.php` | Modified | MFP_Media_Query init, media-filter.js enqueue, i18n strings | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Use ajax_query_attachments_args | Native WP filter, no core mods | Clean integration | +| library.props.set for grid reload | Backbone native, triggers AJAX automatically | No manual DOM manipulation | +| MutationObserver for infinite scroll | Attachments load dynamically, need draggable on each | Robust for any load pattern | + +## Deviations from Plan + +None — plan executed exactly as written. + +## Issues Encountered + +None + +## Next Phase Readiness + +**Ready:** +- Full folder filtering + assignment pipeline operational +- media-filter.js pattern reusable for modal integration (Phase 3) +- mfp_assign_media endpoint ready for bulk use (Phase 4) + +**Concerns:** +- Media modal uses different wp.media frame — will need separate integration path +- List view (table) not yet handled (deferred to Phase 4) + +**Blockers:** +- None + +--- +*Phase: 02-media-library-grid, Plan: 01* +*Completed: 2026-03-28* diff --git a/.paul/phases/03-media-modal/03-01-PLAN.md b/.paul/phases/03-media-modal/03-01-PLAN.md new file mode 100644 index 0000000..a17ad75 --- /dev/null +++ b/.paul/phases/03-media-modal/03-01-PLAN.md @@ -0,0 +1,211 @@ +--- +phase: 03-media-modal +plan: 01 +type: execute +wave: 1 +depends_on: ["02-01"] +files_modified: + - wp-content/plugins/media-folder-pro/assets/js/modal-integration.js + - wp-content/plugins/media-folder-pro/assets/css/admin.css + - wp-content/plugins/media-folder-pro/media-folder-pro.php + - wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php +autonomous: true +--- + + +## Goal +Zintegrowac drzewko folderow z modalem WP media (wp.media modal) — tym samym, ktory otwiera sie w edytorze blokowym, klasycznym edytorze, Elementorze i innych pluginach uzywajacych wp.media. Uzytkownik moze filtrowac media po folderze wewnatrz modala oraz przypisac folder podczas uploadu. + +## Purpose +Modal mediow to glowny punkt interakcji z biblioteka — bez integracji uzytkownik nie moze korzystac z folderow podczas wstawiania mediow do tresci. + +## Output +- Dropdown/sidebar z folderami wewnatrz modala media +- Filtrowanie mediow w modalu po wybranym folderze +- Mozliwosc wyboru folderu docelowego przy uploadzie nowych plikow + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md + +## Prior Work +@.paul/phases/02-media-library-grid/02-01-SUMMARY.md +- MFP_Media_Query z filtrem ajax_query_attachments_args (juz dziala dla modala tez!) +- mfp_assign_media endpoint gotowy +- media-filter.js z wzorcem library.props.set({media_folder: id}) + +## Source Files +@wp-content/plugins/media-folder-pro/media-folder-pro.php +@wp-content/plugins/media-folder-pro/assets/js/media-filter.js +@wp-content/plugins/media-folder-pro/includes/class-media-query.php +@wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php +@wp-content/plugins/media-folder-pro/assets/css/admin.css + + + + +## AC-1: Dropdown folderow widoczny w modalu media +```gherkin +Given plugin aktywny, istnieja foldery +When uzytkownik otwiera modal media (np. "Dodaj media" w edytorze) +Then w pasku narzedzi modala widoczny jest dropdown z lista folderow +And dropdown zawiera opcje "Wszystkie media" + lista folderow hierarchicznie +``` + +## AC-2: Filtrowanie mediow w modalu po folderze +```gherkin +Given modal media otwarty, istnieja foldery z mediami +When uzytkownik wybiera folder z dropdown +Then grid mediow w modalu pokazuje tylko media z wybranego folderu +And wybranie "Wszystkie media" przywraca pelna liste +``` + +## AC-3: Przypisanie folderu przy uploadzie +```gherkin +Given modal media otwarty, wybrany folder "Zdjecia produktow" +When uzytkownik uploaduje nowy plik przez zakladke "Wyslij pliki" +Then nowo uploadowany plik zostaje automatycznie przypisany do aktywnego folderu +And plik pojawia sie w filtrowanym widoku tego folderu +``` + + + + + + + Task 1: Modal folder dropdown + filtering + wp-content/plugins/media-folder-pro/assets/js/modal-integration.js, wp-content/plugins/media-folder-pro/assets/css/admin.css, wp-content/plugins/media-folder-pro/media-folder-pro.php + + 1. Utworzyc assets/js/modal-integration.js: + - Hook na wp.media.view.AttachmentsBrowser: + - Nadpisz (extend) domyslny AttachmentsBrowser + - W metodzie createToolbar() dodaj customowy dropdown filter + - Dropdown budowany z danych folderow (AJAX mfp_get_folders przy otwarciu) + - Opcje: "Wszystkie media" (value=0) + foldery z wciecia dla hierarchii + - Np: "Zdjecia", "— Produkty", "— — Elektronika" (hierarchia przez prefiks) + - Przy zmianie dropdown: + - library.props.set({media_folder: selectedFolderId}) + - To uzyje istniejacego MFP_Media_Query filtra server-side + - Przechowuj aktywny folder w zmiennej modulu (potrzebne dla upload) + + 2. Hook na wp.media.view.UploaderInline (upload tab): + - Po uplywie uploadu (wp.Uploader 'upload-success' lub 'wp-upload-complete'): + - Jesli aktywny folder > 0: + - Wywolaj AJAX mfp_assign_media z nowym attachment_id i aktywnym folderem + - Alternatywnie: hook na wp.media.model.Attachment po dodaniu do kolekcji + + 3. Podejscie do hookowania WP media views: + - Uzyj wp.media.view.AttachmentsBrowser.extend() do nadpisania createToolbar + - LUB: uzyj filtra 'AttachmentsBrowser:ready' jesli dostepny + - LUB: monkey-patch wp.media.view.AttachmentsBrowser.prototype.createToolbar + - Wybierz podejscie ktore NIE wymaga modyfikacji core WP JS + - Najstabilniejszy pattern: nadpisz prototype.createToolbar, wywolaj original, + potem dodaj swoj element do this.toolbar + + 4. W admin.css dodac style dla: + - .mfp-modal-filter — select dropdown w toolbarze modala + - Styl opcji z wciecia (hierarchia folderow) + - Dopasowanie do domyslnego stylu WP media toolbar + + 5. W media-folder-pro.php: + - Enqueue modal-integration.js globalnie w admin (nie tylko upload.php) + bo modal moze byc otwarty z dowolnej strony admina + - Dependency: ['media-views'] + - Warunek: enqueue tylko jesli wp_script_is('media-views', 'enqueued') + lub laduj z wp_enqueue_media hook + + Avoid: Nie modyfikowac core WP JS — tylko extend/override prototypow. + Avoid: Nie ladowac pelnego folder-tree.js w modalu — uzyj lekkiego dropdown. + Avoid: Nie uzywac jQuery UI — czysty select element lub custom dropdown. + + + - Otwarcie modala media w edytorze pokazuje dropdown folderow + - Wybranie folderu filtruje media w modalu + - Upload pliku z aktywnym folderem przypisuje go do folderu + - Modal dziala normalnie bez folderow (brak regresji) + - Dropdown laduje hierarchie poprawnie (wciety subfoldery) + + AC-1 + AC-2 + AC-3 satisfied: modal z dropdown, filtrowanie, upload do folderu + + + + Task 2: Auto-assign uploaded media + refresh counters + wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php, wp-content/plugins/media-folder-pro/assets/js/modal-integration.js + + 1. W class-ajax-handler.php dodac nowy endpoint mfp_upload_to_folder: + - Parametry: attachment_id (int), folder_id (int) + - Prostszy niz mfp_assign_media (pojedynczy plik, nie tablica) + - wp_set_object_terms($attachment_id, [$folder_id], 'media_folder') + - Zwraca: {success: true} + - Dodac do tablicy $actions w register_hooks() + - Ten endpoint jest dedykowany dla upload flow (szybszy, bez folder_counts) + + 2. W modal-integration.js: + - Po AJAX uplywie uploadu nowego pliku: + - Nasluchuj na wp.Uploader events lub Attachment collection events + - Pattern: wp.media.model.Attachments on('add') — nowy attachment dodany + - Jesli aktywny folder: wywolaj mfp_upload_to_folder + - Po przypisaniu: dispatch 'mfp-folder-changed' event + (zeby upload.php folder tree tez sie odswieszyl jesli otwarty) + + 3. Synchronizacja z upload.php: + - Jesli modal otwarty z upload.php (media library page): + - Po zamknieciu modala: dispatch 'mfp-folder-changed' + - folder-tree.js nasluchuje i wywoluje refreshTree() + - Dodac listener w folder-tree.js na 'mfp-folder-changed' + + Avoid: Nie duplikuj logiki mfp_assign_media — mfp_upload_to_folder to uproszczona wersja. + Avoid: Nie refreshuj counterow w modalu (zbyt ciezkie) — tylko po zamknieciu. + + + - Upload pliku z aktywnym folderem przypisuje automatycznie + - Po zamknieciu modala countery folderow na upload.php sa aktualne + - Upload bez aktywnego folderu nie przypisuje do zadnego folderu + - Brak duplikatow przypisania (jeden upload = jedna operacja) + + AC-3 reinforced: upload flow roboczy z auto-assign i sync counterow + + + + + + +## DO NOT CHANGE +- wp-content/plugins/elementor-pro/* (nie modyfikujemy Elementora) +- wp-content/plugins/media-folder-pro/includes/class-taxonomy.php (stabilna) +- wp-content/plugins/media-folder-pro/includes/class-media-query.php (stabilna, juz filtruje modal) +- Zadne pliki core WordPress + +## SCOPE LIMITS +- Brak drag & drop w modalu (tylko dropdown filter) +- Brak tworzenia folderow z modala (tylko wybor istniejacych) +- Brak bulk operations (Phase 4) +- Elementor-specific hooks NIE sa potrzebne — Elementor uzywa standardowego wp.media + + + + +Before declaring plan complete: +- [ ] Dropdown folderow widoczny w modalu media +- [ ] Filtrowanie dziala: wybor folderu → tylko media z folderu +- [ ] "Wszystkie media" przywraca pelna liste +- [ ] Upload z aktywnym folderem → auto-assign do folderu +- [ ] Upload bez folderu → brak przypisania (normalne zachowanie) +- [ ] Po zamknieciu modala → countery na upload.php odswiezone +- [ ] Modal otwierany z roznych stron (post edit, page edit) dziala +- [ ] Brak regresji: modal bez folderow dziala normalnie +- [ ] Brak JS errors w konsoli + + + +- Wszystkie taski ukonczone +- Wszystkie verification checks przeszly +- Modal media w pelni zintegrowany z folderami +- Brak bledow PHP i JS + + + +After completion, create `.paul/phases/03-media-modal/03-01-SUMMARY.md` + diff --git a/.paul/phases/03-media-modal/03-01-SUMMARY.md b/.paul/phases/03-media-modal/03-01-SUMMARY.md new file mode 100644 index 0000000..114fff9 --- /dev/null +++ b/.paul/phases/03-media-modal/03-01-SUMMARY.md @@ -0,0 +1,121 @@ +--- +phase: 03-media-modal +plan: 01 +subsystem: media +tags: [wordpress, media-modal, backbone-extend, upload-hook, wp-uploader] + +requires: + - phase: 02-media-library-grid + provides: MFP_Media_Query filter, mfp_assign_media endpoint +provides: + - Folder dropdown in wp.media modal + - Modal filtering via backbone props + - Auto-assign uploads to active folder + - mfp_upload_to_folder lightweight endpoint +affects: [04-polish-ux] + +tech-stack: + added: [] + patterns: [wp.media.view.AttachmentsBrowser.extend, wp.Uploader.queue hook, modal close hook, foldersCache pattern] + +key-files: + created: + - wp-content/plugins/media-folder-pro/assets/js/modal-integration.js + modified: + - wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php + - wp-content/plugins/media-folder-pro/assets/js/folder-tree.js + - wp-content/plugins/media-folder-pro/assets/css/admin.css + - wp-content/plugins/media-folder-pro/media-folder-pro.php + +key-decisions: + - "Extend AttachmentsBrowser.prototype.createToolbar — call original then add dropdown" + - "Flat element ────────────────────── + function buildFolderSelect( onChange ) { + var select = document.createElement( 'select' ); + select.className = 'mfp-modal-filter attachment-filters'; + + var defaultOpt = document.createElement( 'option' ); + defaultOpt.value = '0'; + defaultOpt.textContent = i18n.allMedia; + select.appendChild( defaultOpt ); + + loadFolders().then( function ( folders ) { + var flat = flattenFolders( folders ); + for ( var j = 0; j < flat.length; j++ ) { + var opt = document.createElement( 'option' ); + opt.value = String( flat[ j ].value ); + opt.textContent = flat[ j ].label + ( flat[ j ].count > 0 ? ' (' + flat[ j ].count + ')' : '' ); + select.appendChild( opt ); + } + if ( activeFolderId ) { + select.value = String( activeFolderId ); + } + } ); + + select.addEventListener( 'change', function () { + var folderId = parseInt( select.value, 10 ); + activeFolderId = folderId || 0; + if ( onChange ) onChange( folderId ); + // Sync all folder selects in the modal + syncAllSelects( folderId ); + } ); + + return select; + } + + // Keep all folder selects in sync (library toolbar + upload tab) + var allSelects = []; + function syncAllSelects( folderId ) { + for ( var i = 0; i < allSelects.length; i++ ) { + allSelects[ i ].value = String( folderId || 0 ); + } + } + + // ─── Override AttachmentsBrowser toolbar ─────────────────── + var OriginalBrowser = wp.media.view.AttachmentsBrowser; + + wp.media.view.AttachmentsBrowser = OriginalBrowser.extend( { + createToolbar: function () { + OriginalBrowser.prototype.createToolbar.call( this ); + + var toolbar = this.toolbar; + var collection = this.collection; + + var select = buildFolderSelect( function ( folderId ) { + if ( folderId > 0 ) { + collection.props.set( { media_folder: folderId } ); + } else { + collection.props.set( { media_folder: 0 } ); + } + } ); + + allSelects.push( select ); + + var FilterView = wp.media.View.extend( { + tagName: 'div', + className: 'mfp-modal-filter-wrap', + render: function () { + this.$el.append( select ); + return this; + }, + } ); + + toolbar.set( 'mfpFolderFilter', new FilterView( { + controller: this.controller, + priority: -75, + } ) ); + }, + } ); + + // ─── Add folder select to Upload tab ────────────────────── + var OriginalUploaderInline = wp.media.view.UploaderInline; + + wp.media.view.UploaderInline = OriginalUploaderInline.extend( { + ready: function () { + OriginalUploaderInline.prototype.ready.apply( this, arguments ); + + // Don't add twice + if ( this.$el.find( '.mfp-upload-folder-wrap' ).length ) return; + + var wrap = document.createElement( 'div' ); + wrap.className = 'mfp-upload-folder-wrap'; + + var label = document.createElement( 'label' ); + label.className = 'mfp-upload-folder-label'; + label.textContent = ( i18n.moveToFolder || 'Wyślij do folderu' ) + ': '; + + var select = buildFolderSelect( function () { + // Just updates activeFolderId via buildFolderSelect's change handler + } ); + + allSelects.push( select ); + wrap.appendChild( label ); + wrap.appendChild( select ); + + // Insert before the upload area + this.$el.prepend( wrap ); + }, + } ); + + // ─── Inject folder ID into plupload params ────────────── + if ( wp.Uploader ) { + var origUploaderInit = wp.Uploader.prototype.init; + wp.Uploader.prototype.init = function () { + origUploaderInit.apply( this, arguments ); + + var uploader = this.uploader; + if ( ! uploader ) return; + + uploader.bind( 'BeforeUpload', function () { + if ( activeFolderId > 0 ) { + uploader.settings.multipart_params.mfp_folder_id = activeFolderId; + } else { + delete uploader.settings.multipart_params.mfp_folder_id; + } + } ); + + uploader.bind( 'FileUploaded', function () { + document.dispatchEvent( new CustomEvent( 'mfp-folder-changed' ) ); + + // Re-query the library so new file appears. + // Don't use reset() — it clears the grid and shows "no items". + // Instead, toggle the prop to force a fresh AJAX query. + setTimeout( function () { + if ( ! wp.media.frame ) return; + try { + var state = wp.media.frame.state(); + var lib = state && state.get( 'library' ); + if ( lib && lib.props ) { + var folder = lib.props.get( 'media_folder' ) || 0; + lib.props.unset( 'media_folder', { silent: true } ); + lib.props.set( { media_folder: folder } ); + } + } catch ( e ) {} + }, 800 ); + } ); + }; + } + + // ─── Sync on modal open/close ───────────────────────────── + var origModalOpen = wp.media.view.Modal.prototype.open; + wp.media.view.Modal.prototype.open = function () { + var modal = this; + origModalOpen.apply( this, arguments ); + + if ( ! modal._mfpCloseHooked ) { + modal._mfpCloseHooked = true; + var origClose = modal.close; + modal.close = function () { + foldersCache = null; + activeFolderId = 0; + allSelects = []; + document.dispatchEvent( new CustomEvent( 'mfp-folder-changed' ) ); + return origClose.apply( this, arguments ); + }; + } + }; +} )(); diff --git a/wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php b/wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php new file mode 100644 index 0000000..471b548 --- /dev/null +++ b/wp-content/plugins/media-folder-pro/includes/class-ajax-handler.php @@ -0,0 +1,237 @@ +taxonomy = $taxonomy; + } + + public function register_hooks(): void { + $actions = [ + 'mfp_create_folder', + 'mfp_rename_folder', + 'mfp_delete_folder', + 'mfp_move_folder', + 'mfp_get_folders', + 'mfp_assign_media', + 'mfp_upload_to_folder', + ]; + + foreach ( $actions as $action ) { + add_action( "wp_ajax_{$action}", [ $this, $action ] ); + } + } + + private function verify_request(): void { + check_ajax_referer( 'mfp_nonce', 'nonce' ); + + if ( ! current_user_can( 'upload_files' ) ) { + wp_send_json_error( [ 'message' => __( 'Brak uprawnień.', 'media-folder-pro' ) ], 403 ); + } + } + + public function mfp_create_folder(): void { + $this->verify_request(); + + $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); + $parent_id = (int) ( $_POST['parent_id'] ?? 0 ); + + if ( empty( $name ) ) { + wp_send_json_error( [ 'message' => __( 'Nazwa folderu jest wymagana.', 'media-folder-pro' ) ] ); + } + + $result = wp_insert_term( $name, MFP_Taxonomy::TAXONOMY, [ + 'parent' => $parent_id, + ] ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ] ); + } + + $term = get_term( $result['term_id'], MFP_Taxonomy::TAXONOMY ); + + wp_send_json_success( [ + 'folder' => [ + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'count' => 0, + 'children' => [], + ], + ] ); + } + + public function mfp_rename_folder(): void { + $this->verify_request(); + + $folder_id = (int) ( $_POST['folder_id'] ?? 0 ); + $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); + + if ( ! $folder_id || empty( $name ) ) { + wp_send_json_error( [ 'message' => __( 'ID folderu i nowa nazwa są wymagane.', 'media-folder-pro' ) ] ); + } + + $result = wp_update_term( $folder_id, MFP_Taxonomy::TAXONOMY, [ + 'name' => $name, + ] ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ] ); + } + + $term = get_term( $result['term_id'], MFP_Taxonomy::TAXONOMY ); + + wp_send_json_success( [ + 'folder' => [ + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ], + ] ); + } + + public function mfp_delete_folder(): void { + $this->verify_request(); + + $folder_id = (int) ( $_POST['folder_id'] ?? 0 ); + + if ( ! $folder_id ) { + wp_send_json_error( [ 'message' => __( 'ID folderu jest wymagane.', 'media-folder-pro' ) ] ); + } + + if ( $this->taxonomy->folder_has_children( $folder_id ) ) { + wp_send_json_error( [ + 'message' => __( 'Folder nie jest pusty. Usuń najpierw zawartość i podfoldery.', 'media-folder-pro' ), + 'code' => 'not_empty', + ] ); + } + + $result = wp_delete_term( $folder_id, MFP_Taxonomy::TAXONOMY ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ] ); + } + + wp_send_json_success(); + } + + public function mfp_move_folder(): void { + $this->verify_request(); + + $folder_id = (int) ( $_POST['folder_id'] ?? 0 ); + $new_parent_id = (int) ( $_POST['new_parent_id'] ?? 0 ); + + if ( ! $folder_id ) { + wp_send_json_error( [ 'message' => __( 'ID folderu jest wymagane.', 'media-folder-pro' ) ] ); + } + + if ( $this->taxonomy->would_create_cycle( $folder_id, $new_parent_id ) ) { + wp_send_json_error( [ + 'message' => __( 'Nie można przenieść folderu do samego siebie lub swojego podfolderu.', 'media-folder-pro' ), + ] ); + } + + $result = wp_update_term( $folder_id, MFP_Taxonomy::TAXONOMY, [ + 'parent' => $new_parent_id, + ] ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ] ); + } + + $term = get_term( $result['term_id'], MFP_Taxonomy::TAXONOMY ); + + wp_send_json_success( [ + 'folder' => [ + 'id' => $term->term_id, + 'name' => $term->name, + 'parent' => $term->parent, + ], + ] ); + } + + public function mfp_assign_media(): void { + $this->verify_request(); + + $attachment_ids = array_map( 'intval', (array) ( $_POST['attachment_ids'] ?? [] ) ); + $folder_id = (int) ( $_POST['folder_id'] ?? 0 ); + + if ( empty( $attachment_ids ) ) { + wp_send_json_error( [ 'message' => __( 'Brak wybranych mediów.', 'media-folder-pro' ) ] ); + } + + $count = 0; + foreach ( $attachment_ids as $id ) { + if ( $id <= 0 ) { + continue; + } + if ( get_post_type( $id ) !== 'attachment' ) { + continue; + } + + $terms = $folder_id > 0 ? [ $folder_id ] : []; + $result = wp_set_object_terms( $id, $terms, MFP_Taxonomy::TAXONOMY ); + + if ( ! is_wp_error( $result ) ) { + $count++; + } + } + + // Return updated folder counts. + $all_terms = get_terms( [ + 'taxonomy' => MFP_Taxonomy::TAXONOMY, + 'hide_empty' => false, + 'fields' => 'all', + ] ); + + $folder_counts = []; + if ( ! is_wp_error( $all_terms ) ) { + foreach ( $all_terms as $term ) { + $folder_counts[ $term->term_id ] = (int) $term->count; + } + } + + wp_send_json_success( [ + 'count' => $count, + 'folder_counts' => $folder_counts, + ] ); + } + + public function mfp_upload_to_folder(): void { + $this->verify_request(); + + $attachment_id = (int) ( $_POST['attachment_id'] ?? 0 ); + $folder_id = (int) ( $_POST['folder_id'] ?? 0 ); + + if ( ! $attachment_id || ! $folder_id ) { + wp_send_json_error( [ 'message' => __( 'ID załącznika i folderu są wymagane.', 'media-folder-pro' ) ] ); + } + + if ( get_post_type( $attachment_id ) !== 'attachment' ) { + wp_send_json_error( [ 'message' => __( 'Nieprawidłowy załącznik.', 'media-folder-pro' ) ] ); + } + + $result = wp_set_object_terms( $attachment_id, [ $folder_id ], MFP_Taxonomy::TAXONOMY ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ] ); + } + + wp_send_json_success(); + } + + public function mfp_get_folders(): void { + $this->verify_request(); + + wp_send_json_success( [ + 'folders' => $this->taxonomy->get_folder_tree(), + ] ); + } +} diff --git a/wp-content/plugins/media-folder-pro/includes/class-media-query.php b/wp-content/plugins/media-folder-pro/includes/class-media-query.php new file mode 100644 index 0000000..ef597cc --- /dev/null +++ b/wp-content/plugins/media-folder-pro/includes/class-media-query.php @@ -0,0 +1,64 @@ + MFP_Taxonomy::TAXONOMY, + 'operator' => 'NOT EXISTS', + ]; + } elseif ( $folder_id > 0 ) { + $query['tax_query'][] = [ + 'taxonomy' => MFP_Taxonomy::TAXONOMY, + 'terms' => [ $folder_id ], + 'field' => 'term_id', + ]; + } + + return $query; + } +} diff --git a/wp-content/plugins/media-folder-pro/includes/class-taxonomy.php b/wp-content/plugins/media-folder-pro/includes/class-taxonomy.php new file mode 100644 index 0000000..437af44 --- /dev/null +++ b/wp-content/plugins/media-folder-pro/includes/class-taxonomy.php @@ -0,0 +1,122 @@ + [ + 'name' => __( 'Foldery mediów', 'media-folder-pro' ), + 'singular_name' => __( 'Folder mediów', 'media-folder-pro' ), + 'add_new_item' => __( 'Dodaj nowy folder', 'media-folder-pro' ), + 'edit_item' => __( 'Edytuj folder', 'media-folder-pro' ), + ], + 'hierarchical' => true, + 'public' => false, + 'show_ui' => false, + 'show_in_rest' => true, + 'show_admin_column' => false, + 'query_var' => false, + 'rewrite' => false, + ] ); + } + + /** + * @return array + */ + public function get_folder_tree(): array { + $terms = get_terms( [ + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'orderby' => 'name', + 'order' => 'ASC', + ] ); + + if ( is_wp_error( $terms ) || empty( $terms ) ) { + return []; + } + + $flat = []; + foreach ( $terms as $term ) { + $flat[ $term->term_id ] = [ + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'count' => (int) $term->count, + 'children' => [], + ]; + } + + $tree = []; + foreach ( $flat as $id => &$node ) { + if ( $node['parent'] === 0 ) { + $tree[] = &$flat[ $id ]; + } elseif ( isset( $flat[ $node['parent'] ] ) ) { + $flat[ $node['parent'] ]['children'][] = &$flat[ $id ]; + } else { + $tree[] = &$flat[ $id ]; + } + } + unset( $node ); + + return $tree; + } + + public function folder_has_children( int $folder_id ): bool { + $children = get_terms( [ + 'taxonomy' => self::TAXONOMY, + 'parent' => $folder_id, + 'hide_empty' => false, + 'number' => 1, + 'fields' => 'ids', + ] ); + + if ( ! empty( $children ) ) { + return true; + } + + $attachments = get_posts( [ + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'tax_query' => [ + [ + 'taxonomy' => self::TAXONOMY, + 'terms' => $folder_id, + ], + ], + 'posts_per_page' => 1, + 'fields' => 'ids', + ] ); + + return ! empty( $attachments ); + } + + public function would_create_cycle( int $folder_id, int $new_parent_id ): bool { + if ( $new_parent_id === 0 ) { + return false; + } + if ( $folder_id === $new_parent_id ) { + return true; + } + + $current = $new_parent_id; + while ( $current !== 0 ) { + $term = get_term( $current, self::TAXONOMY ); + if ( is_wp_error( $term ) || ! $term ) { + break; + } + if ( $term->parent === $folder_id ) { + return true; + } + $current = $term->parent; + } + + return false; + } +} diff --git a/wp-content/plugins/media-folder-pro/media-folder-pro.php b/wp-content/plugins/media-folder-pro/media-folder-pro.php new file mode 100644 index 0000000..a8f532f --- /dev/null +++ b/wp-content/plugins/media-folder-pro/media-folder-pro.php @@ -0,0 +1,218 @@ +taxonomy = new MFP_Taxonomy(); + $this->ajax = new MFP_Ajax_Handler( $this->taxonomy ); + $this->media_query = new MFP_Media_Query(); + + add_action( 'init', [ $this->taxonomy, 'register' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_modal_assets' ], 20 ); + add_action( 'wp_enqueue_media', [ $this, 'enqueue_modal_on_media_load' ] ); + add_action( 'admin_footer-upload.php', [ $this, 'render_folder_tree_container' ] ); + + $this->ajax->register_hooks(); + $this->media_query->register(); + } + + /** + * Shared localization data for all JS scripts. + */ + private function get_mfp_data(): array { + return [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'mfp_nonce' ), + 'i18n' => [ + 'allMedia' => __( 'Wszystkie media', 'media-folder-pro' ), + 'newFolder' => __( 'Nowy folder', 'media-folder-pro' ), + 'folderName' => __( 'Nazwa folderu:', 'media-folder-pro' ), + 'rename' => __( 'Zmień nazwę', 'media-folder-pro' ), + 'delete' => __( 'Usuń', 'media-folder-pro' ), + 'newSubfolder' => __( 'Nowy podfolder', 'media-folder-pro' ), + 'confirmDelete' => __( 'Czy na pewno usunąć ten folder?', 'media-folder-pro' ), + 'folderNotEmpty' => __( 'Folder nie jest pusty. Usuń najpierw zawartość.', 'media-folder-pro' ), + 'error' => __( 'Wystąpił błąd. Spróbuj ponownie.', 'media-folder-pro' ), + 'assignSuccess' => __( 'Media przypisane do folderu.', 'media-folder-pro' ), + 'assignError' => __( 'Nie udało się przypisać mediów.', 'media-folder-pro' ), + 'dropHere' => __( 'Upuść tutaj', 'media-folder-pro' ), + 'uncategorized' => __( 'Bez folderu', 'media-folder-pro' ), + 'moveToFolder' => __( 'Przenieś do folderu', 'media-folder-pro' ), + 'removeFromFolder' => __( 'Usuń z folderu', 'media-folder-pro' ), + 'noSelection' => __( 'Zaznacz media do przeniesienia', 'media-folder-pro' ), + 'folderCreated' => __( 'Folder utworzony', 'media-folder-pro' ), + 'folderRenamed' => __( 'Nazwa zmieniona', 'media-folder-pro' ), + 'folderDeleted' => __( 'Folder usunięty', 'media-folder-pro' ), + 'folderMoved' => __( 'Folder przeniesiony', 'media-folder-pro' ), + 'emptyState' => __( 'Brak folderów', 'media-folder-pro' ), + 'createFirst' => __( 'Utwórz pierwszy folder', 'media-folder-pro' ), + ], + ]; + } + + /** + * Ensure mfpData is localized (registers inline script if tree JS not loaded). + */ + private function ensure_mfp_data(): void { + if ( wp_script_is( 'media-folder-pro-tree', 'enqueued' ) ) { + return; + } + wp_register_script( 'media-folder-pro-tree', false ); + wp_enqueue_script( 'media-folder-pro-tree' ); + wp_localize_script( 'media-folder-pro-tree', 'mfpData', $this->get_mfp_data() ); + } + + /** + * Full assets for Media Library pages (upload.php, media-new.php). + */ + public function enqueue_admin_assets( string $hook ): void { + if ( ! in_array( $hook, [ 'upload.php', 'media-new.php' ], true ) ) { + return; + } + + wp_enqueue_style( + 'media-folder-pro-admin', + MFP_PLUGIN_URL . 'assets/css/admin.css', + [], + MFP_VERSION + ); + + wp_enqueue_script( + 'media-folder-pro-tree', + MFP_PLUGIN_URL . 'assets/js/folder-tree.js', + [], + MFP_VERSION, + true + ); + + wp_enqueue_script( + 'media-folder-pro-filter', + MFP_PLUGIN_URL . 'assets/js/media-filter.js', + [ 'media-views', 'media-folder-pro-tree' ], + MFP_VERSION, + true + ); + + wp_enqueue_script( + 'media-folder-pro-modal', + MFP_PLUGIN_URL . 'assets/js/modal-integration.js', + [ 'media-views', 'media-folder-pro-tree' ], + MFP_VERSION, + true + ); + + wp_localize_script( 'media-folder-pro-tree', 'mfpData', $this->get_mfp_data() ); + } + + /** + * Modal integration for standard admin pages (post editor, etc.). + */ + public function enqueue_modal_assets( string $hook ): void { + if ( $hook === 'upload.php' || $hook === 'media-new.php' ) { + return; + } + + if ( ! did_action( 'wp_enqueue_media' ) && ! wp_script_is( 'media-views', 'enqueued' ) ) { + return; + } + + if ( wp_script_is( 'media-folder-pro-modal', 'enqueued' ) ) { + return; + } + + wp_enqueue_style( + 'media-folder-pro-admin', + MFP_PLUGIN_URL . 'assets/css/admin.css', + [], + MFP_VERSION + ); + + $this->ensure_mfp_data(); + + wp_enqueue_script( + 'media-folder-pro-modal', + MFP_PLUGIN_URL . 'assets/js/modal-integration.js', + [ 'media-views', 'media-folder-pro-tree' ], + MFP_VERSION, + true + ); + } + + /** + * Hook into wp_enqueue_media — fires whenever any plugin/theme calls wp_enqueue_media(). + * Covers: Elementor, WPBakery, Divi, ACF, and any builder using wp.media. + */ + public function enqueue_modal_on_media_load(): void { + if ( wp_script_is( 'media-folder-pro-modal', 'enqueued' ) ) { + return; + } + + // Skip pages handled by enqueue_admin_assets — wp_enqueue_media() fires + // BEFORE admin_enqueue_scripts on upload.php, which would poison the + // tree script handle with a false (empty) registration. + $screen = get_current_screen(); + if ( $screen && in_array( $screen->base, [ 'upload', 'media' ], true ) ) { + return; + } + + wp_enqueue_style( + 'media-folder-pro-admin', + MFP_PLUGIN_URL . 'assets/css/admin.css', + [], + MFP_VERSION + ); + + $this->ensure_mfp_data(); + + wp_enqueue_script( + 'media-folder-pro-modal', + MFP_PLUGIN_URL . 'assets/js/modal-integration.js', + [ 'media-views', 'media-folder-pro-tree' ], + MFP_VERSION, + true + ); + } + + public function render_folder_tree_container(): void { + echo '
    '; + } +} + +add_action( 'plugins_loaded', [ 'Media_Folder_Pro', 'instance' ] );