| 1 |
<?php |
| 2 |
/** |
| 3 |
* Trash — the Recycle Bin, as an OpenStation app. |
| 4 |
* |
| 5 |
* THE Recycle Bin: the App Framework rebuild replaced the legacy |
| 6 |
* native window whole and claims its id — `desktop-mode-recycle-bin` |
| 7 |
* is a frozen identifier (see AGENTS.md), so every desktop shortcut, |
| 8 |
* dock placement, drag-to-trash drop target, theme icon slot and |
| 9 |
* Apps & Plugins row keeps working unchanged. The window is this |
| 10 |
* file; the body is `trash.os.ts`, a client view painting the same |
| 11 |
* toolbar, `<os-table>` and empty state through its own cell |
| 12 |
* renderers (`parts/table-visuals.ts`). All data and mutations run |
| 13 |
* through the store (`includes/recycle-bin/`), which also keeps |
| 14 |
* capture, realtime and REST exactly as they were; the closed-window |
| 15 |
* tile art stays shell-side (`src/desktop-files/recycle-bin-icon-state.ts`). |
| 16 |
* |
| 17 |
* What the framework replaced: the window registration and template |
| 18 |
* (`window.php` keeps only the tile art, the gate and the shell |
| 19 |
* config), the per-window REST client and config blob (actions + |
| 20 |
* `data()` + `ctx.fetch`), the broadcast subscriptions |
| 21 |
* (`watch( '*' )`), and the imperative toolbar wiring. |
| 22 |
* |
| 23 |
* (Header kept short on purpose: Plugin Check's direct-access scan |
| 24 |
* reads only the first 50 raw lines, and the guard below must land |
| 25 |
* inside that window.) |
| 26 |
* |
| 27 |
* @package OpenStation |
| 28 |
*/ |
| 29 |
|
| 30 |
namespace OpenStation\Apps\Trash; |
| 31 |
|
| 32 |
use OpenStation\App; |
| 33 |
use OpenStation\App\Os; |
| 34 |
use OpenStation\App\State; |
| 35 |
|
| 36 |
// Direct access, unless a standalone host is booting on bare PHP. |
| 37 |
if ( ! defined( 'ABSPATH' ) ) { |
| 38 |
defined( 'OPENSTATION_STANDALONE' ) || exit; |
| 39 |
} |
| 40 |
|
| 41 |
/** |
| 42 |
* Normalise the dispatched `items` argument into `{id, type}` refs. |
| 43 |
* |
| 44 |
* @param array<string,mixed> $args Action args. |
| 45 |
* @return array<int,array{id:int,type:string}> |
| 46 |
*/ |
| 47 |
function refs( array $args ) { |
| 48 |
$out = array(); |
| 49 |
foreach ( (array) ( $args['items'] ?? array() ) as $entry ) { |
| 50 |
if ( ! is_array( $entry ) ) { |
| 51 |
continue; |
| 52 |
} |
| 53 |
$id = isset( $entry['id'] ) ? (int) $entry['id'] : 0; |
| 54 |
if ( $id <= 0 ) { |
| 55 |
continue; |
| 56 |
} |
| 57 |
$out[] = array( |
| 58 |
'id' => $id, |
| 59 |
'type' => isset( $entry['type'] ) ? sanitize_key( (string) $entry['type'] ) : '', |
| 60 |
); |
| 61 |
} |
| 62 |
return $out; |
| 63 |
} |
| 64 |
|
| 65 |
/** |
| 66 |
* Run one of the store's bulk callbacks over the dispatched refs, |
| 67 |
* announce the content change per affected type (the same |
| 68 |
* `os.<type>.changed` broadcasts the legacy bin emits, which is also |
| 69 |
* what repaints iframes and the legacy window), and surface errors |
| 70 |
* as a toast — the legacy bin only logged them to the console. |
| 71 |
* |
| 72 |
* @param Os $os Host handle. |
| 73 |
* @param array<int,array{id:int,type:string}> $items Refs. |
| 74 |
* @param callable $callback Store bulk callback. |
| 75 |
* @param string $action `untrashed` | `deleted`. |
| 76 |
* @return void |
| 77 |
*/ |
| 78 |
function run_bulk( Os $os, array $items, $callback, $action ) { |
| 79 |
if ( array() === $items ) { |
| 80 |
return; |
| 81 |
} |
| 82 |
$result = openstation_recycle_bin_apply_bulk( $items, $callback ); |
| 83 |
$ok = array_map( 'intval', (array) $result['ok'] ); |
| 84 |
if ( array() !== $ok ) { |
| 85 |
$by_type = array(); |
| 86 |
foreach ( $items as $item ) { |
| 87 |
if ( in_array( $item['id'], $ok, true ) ) { |
| 88 |
$by_type[ '' !== $item['type'] ? $item['type'] : 'post' ][] = $item['id']; |
| 89 |
} |
| 90 |
} |
| 91 |
foreach ( $by_type as $type => $ids ) { |
| 92 |
$os->announce( (string) $type, $action, $ids ); |
| 93 |
} |
| 94 |
} |
| 95 |
$errors = (array) $result['errors']; |
| 96 |
if ( array() !== $errors ) { |
| 97 |
$os->toast( |
| 98 |
sprintf( |
| 99 |
/* translators: %d: number of items that could not be processed. */ |
| 100 |
__( '%d item(s) could not be processed.', 'desktop-mode' ), |
| 101 |
count( $errors ) |
| 102 |
) |
| 103 |
); |
| 104 |
} |
| 105 |
} |
| 106 |
|
| 107 |
return App::define( 'desktop-mode-recycle-bin' ) |
| 108 |
->title( __( 'Trash', 'desktop-mode' ) ) |
| 109 |
// The same outlined-vessel mark the legacy dock tile draws — its |
| 110 |
// empty state; the client view swaps in the full-bin art through |
| 111 |
// `ctx.host.setIcon()` when the count crosses zero, and both |
| 112 |
// drawings travel in the config extra below so the swap is a |
| 113 |
// local operation, never a round trip. Deliberately NO badge: a |
| 114 |
// count on the tile reads as update notifications, and a bin that |
| 115 |
// changes shape carries the same signal without shouting. |
| 116 |
->icon( function_exists( 'openstation_recycle_bin_icon_svg' ) ? openstation_recycle_bin_icon_svg() : 'dashicons-trash' ) |
| 117 |
->config( function_exists( 'openstation_recycle_bin_icon_uris' ) ? openstation_recycle_bin_icon_uris() : array() ) |
| 118 |
->size( 880, 560 ) |
| 119 |
->min_size( 520, 360 ) |
| 120 |
// The legacy bin's rail furniture, inherited whole: a control |
| 121 |
// (not an app), last on the dock after the shell's own cluster |
| 122 |
// (Mio 10, Overview 20, System 30) — Trash is where things END |
| 123 |
// UP, and a dock reads left to right. |
| 124 |
->nav_kind( 'control' ) |
| 125 |
->dock_order( 40 ) |
| 126 |
->placeable() |
| 127 |
->can( |
| 128 |
static function () { |
| 129 |
return function_exists( 'openstation_recycle_bin_user_can_use' ) |
| 130 |
? openstation_recycle_bin_user_can_use() |
| 131 |
: current_user_can( 'edit_posts' ); |
| 132 |
} |
| 133 |
) |
| 134 |
// The whole interaction surface is two mutations; the filter and |
| 135 |
// the search dispatch the built-in `refresh` (the bound value |
| 136 |
// rides up with the state, `data()` re-queries). |
| 137 |
->state( |
| 138 |
array( |
| 139 |
'filter' => '', |
| 140 |
'search' => '', |
| 141 |
) |
| 142 |
) |
| 143 |
// Returning to the bin must re-query even if a change notification |
| 144 |
// was missed while it was hidden or another admin tab was active. |
| 145 |
->action( |
| 146 |
'show', |
| 147 |
static function () { |
| 148 |
// The dispatch recomputes data below. |
| 149 |
} |
| 150 |
) |
| 151 |
->action( |
| 152 |
'reopen', |
| 153 |
static function () { |
| 154 |
// The dispatch recomputes data below. |
| 155 |
} |
| 156 |
) |
| 157 |
->action( |
| 158 |
'restore', |
| 159 |
static function ( State $state, Os $os, array $args ) { |
| 160 |
run_bulk( $os, refs( $args ), 'openstation_recycle_bin_restore', 'untrashed' ); |
| 161 |
} |
| 162 |
) |
| 163 |
->action( |
| 164 |
'purge', |
| 165 |
static function ( State $state, Os $os, array $args ) { |
| 166 |
run_bulk( $os, refs( $args ), 'openstation_recycle_bin_purge', 'deleted' ); |
| 167 |
} |
| 168 |
) |
| 169 |
// Anything trashed or restored ANYWHERE on the desktop repaints |
| 170 |
// the bin — the framework's replacement for the legacy window's |
| 171 |
// hand-wired per-type broadcast subscriptions. |
| 172 |
->watch( '*' ) |
| 173 |
->data( |
| 174 |
static function ( State $state ) { |
| 175 |
$payload = openstation_recycle_bin_get_items( |
| 176 |
array( |
| 177 |
'type' => (string) $state->get( 'filter' ), |
| 178 |
'search' => (string) $state->get( 'search' ), |
| 179 |
'per_page' => 200, |
| 180 |
) |
| 181 |
); |
| 182 |
return array( |
| 183 |
'items' => $payload['items'], |
| 184 |
// The GLOBAL bin count (every type, unfiltered) — what |
| 185 |
// decides toolbar-vs-empty-state and the dock badge. |
| 186 |
'total' => (int) $payload['total'], |
| 187 |
// Whether WordPress routes attachment deletions through |
| 188 |
// Trash at all — gates the Media filter segment, same |
| 189 |
// as the legacy template. |
| 190 |
'mediaTrash' => defined( 'MEDIA_TRASH' ) && MEDIA_TRASH, |
| 191 |
); |
| 192 |
} |
| 193 |
); |
| 194 |
|