| @@ -118,11 +118,20 @@ | ||
| 118 | 118 | // `<div class="wpdberror">…</div>` to the REST response body and |
| 119 | 119 | // break `await response.json()` on the client. `$wpdb->last_error` |
| 120 | 120 | // still holds the message, so genuine DB failures surface via the |
| 121 | 121 | // `WP_Error` we return when no existing row is found. |
| 122 | - $prev_suppress = $wpdb->suppress_errors( true ); | |
| 123 | - $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) ); | |
| 124 | - $wpdb->suppress_errors( $prev_suppress ); | |
| 122 | + $insert = static function () use ( $tables, $row ) { | |
| 123 | + global $wpdb; | |
| 124 | + $prev_suppress = $wpdb->suppress_errors( true ); | |
| 125 | + $ok = $wpdb->insert( $tables['placements'], $row, array( '%d', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ) ); | |
| 126 | + $wpdb->suppress_errors( $prev_suppress ); | |
| 127 | + return array( $ok, (int) $wpdb->insert_id ); | |
| 128 | + }; | |
| 129 | + $result = 'upload' === $type ? openstation_stored_files_place_insert( $ref, $insert ) : $insert(); | |
| 130 | + if ( is_wp_error( $result ) ) { | |
| 131 | + return $result; | |
| 132 | + } | |
| 133 | + list( $ok, $id ) = $result; | |
| 125 | 134 | if ( false === $ok ) { |
| 126 | 135 | // Disambiguate the two cases hidden behind a generic `false`: |
| 127 | 136 | // (a) The `placement_unique` index collided |
| 128 | 137 | // with an existing row for this (user, parent, type, |
| @@ -187,10 +196,8 @@ | ||
| 187 | 196 | } |
| 188 | 197 | |
| 189 | 198 | return $existing_id; |
| 190 | 199 | } |
| 191 | - $id = (int) $wpdb->insert_id; | |
| 192 | - | |
| 193 | 200 | $row['id'] = $id; |
| 194 | 201 | |
| 195 | 202 | /** |
| 196 | 203 | * Fires after a placement is created. |
| @@ -530,13 +537,14 @@ | ||
| 530 | 537 | $args = (array) apply_filters( 'openstation_files_query_args', $args, $user_id, $parent_id ); |
| 531 | 538 | |
| 532 | 539 | // Active queries always exclude trashed rows. Recycle-bin |
| 533 | 540 | // callers reach for the dedicated trash store. |
| 534 | - if ( ! empty( $args['share_view'] ) ) { | |
| 535 | - // Shared sub-folder — return every placement in the folder | |
| 536 | - // regardless of which user originally placed it. The icons | |
| 537 | - // are part of the folder; the owner_id column is audit info, | |
| 538 | - // not a permission gate. | |
| 541 | + if ( ! empty( $args['share_view'] ) || (int) $args['parent_id'] > 0 ) { | |
| 542 | + // A sub-folder, the viewer's own or shared with them: return | |
| 543 | + // every placement in it regardless of which user placed it, | |
| 544 | + // so the owner of a shared folder sees what a writer added. | |
| 545 | + // The icons are part of the folder; the owner_id column is | |
| 546 | + // audit info, not a permission gate. | |
| 539 | 547 | $rows = $wpdb->get_results( |
| 540 | 548 | $wpdb->prepare( |
| 541 | 549 | "SELECT * FROM {$tables['placements']} |
| 542 | 550 | WHERE parent_id = %d |
| @@ -567,11 +575,11 @@ | ||
| 567 | 575 | foreach ( $rows as $row ) { |
| 568 | 576 | $normalized = openstation_files_normalize_placement_row( $row ); |
| 569 | 577 | $file = openstation_resolve_file( $normalized['file_type'], $normalized['file_ref'] ); |
| 570 | 578 | if ( empty( $args['share_view'] ) ) { |
| 571 | - // Private folder / desktop root — keep the existing | |
| 572 | - // per-row read filter so stale/inaccessible entities | |
| 573 | - // don't clutter the user's own view. | |
| 579 | + // The viewer's own folder or desktop root: keep the | |
| 580 | + // per-row read filter so stale or inaccessible entities, | |
| 581 | + // a writer's included, don't clutter the user's own view. | |
| 574 | 582 | if ( $file && ! $file->can_read( $user_id ) ) { |
| 575 | 583 | continue; |
| 576 | 584 | } |
| 577 | 585 | } elseif ( $file && ! $file->can_read( $user_id ) ) { |
| @@ -604,10 +612,12 @@ | ||
| 604 | 612 | * on first hydrate so plugin shortcuts behave like any |
| 605 | 613 | * other tile (drag, sort, right-click, clean up). |
| 606 | 614 | * |
| 607 | 615 | * Idempotent on both axes: a folder/shortcut that already has |
| 608 | - * any placement is left alone. Coordinates use the column-major | |
| 609 | - * grid that `src/desktop-files/grid.ts` mirrors on the JS side. | |
| 616 | + * any placement is left alone. Coordinates come from | |
| 617 | + * `includes/desktop-files/grid.php`, which mirrors | |
| 618 | + * `src/desktop-files/grid.ts` — pitch, reading order, and the | |
| 619 | + * assumed canvas the scan wraps at. | |
| 610 | 620 | * |
| 611 | 621 | * Called by the placements list endpoint when the requested |
| 612 | 622 | * folder is the root (`parent_id=0`). |
| 613 | 623 | * |
| @@ -682,10 +692,11 @@ | ||
| 682 | 692 | } |
| 683 | 693 | |
| 684 | 694 | // Build an occupied set from EXISTING root placements so |
| 685 | 695 | // we never drop an orphan on top of a tile the user |
| 686 | - // already has. Cell math mirrors `src/desktop-files/grid.ts` | |
| 687 | - // (padding 16 + col 96 + row 110). | |
| 696 | + // already has. Cell math lives in | |
| 697 | + // `includes/desktop-files/grid.php`, the mirror of | |
| 698 | + // `src/desktop-files/grid.ts`. | |
| 688 | 699 | $existing = $wpdb->get_results( |
| 689 | 700 | $wpdb->prepare( |
| 690 | 701 | "SELECT x, y FROM {$tables['placements']} |
| 691 | 702 | WHERE owner_id = %d |
| @@ -694,30 +705,19 @@ | ||
| 694 | 705 | $user_id |
| 695 | 706 | ), |
| 696 | 707 | ARRAY_A |
| 697 | 708 | ); |
| 698 | - $occupied = array(); | |
| 699 | - foreach ( (array) $existing as $row ) { | |
| 700 | - $col = max( 0, (int) round( ( (int) $row['x'] - 16 ) / 96 ) ); | |
| 701 | - $row_idx = max( 0, (int) round( ( (int) $row['y'] - 16 ) / 110 ) ); | |
| 702 | - $occupied[ "$col,$row_idx" ] = true; | |
| 703 | - } | |
| 709 | + $occupied = openstation_files_grid_occupied( $existing ); | |
| 704 | 710 | |
| 705 | - $find_next = function () use ( &$occupied ) { | |
| 706 | - for ( $col = 0; $col < 999; $col++ ) { | |
| 707 | - for ( $row = 0; $row < 999; $row++ ) { | |
| 708 | - $key = "$col,$row"; | |
| 709 | - if ( ! isset( $occupied[ $key ] ) ) { | |
| 710 | - $occupied[ $key ] = true; | |
| 711 | - return array( $col, $row ); | |
| 712 | - } | |
| 713 | - } | |
| 714 | - } | |
| 715 | - return array( 0, 0 ); | |
| 716 | - }; | |
| 711 | + // The desktop reads in columns, and the scan wraps to the next one | |
| 712 | + // at the assumed canvas height rather than running a column 999 | |
| 713 | + // cells deep. The server has no viewport; a slot it invents below | |
| 714 | + // the fold is a tile the user cannot reach, because the layer that | |
| 715 | + // renders it does not scroll. | |
| 716 | + $order = openstation_files_grid_order( 0 ); | |
| 717 | 717 | |
| 718 | - $placed = 0; | |
| 719 | - $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) { | |
| 718 | + $placed = 0; | |
| 719 | + $emit_at = function ( $type, $ref, $col, $row ) use ( $user_id, &$occupied, &$placed ) { | |
| 720 | 720 | $occupied[ "$col,$row" ] = true; |
| 721 | 721 | $result = openstation_files_place( |
| 722 | 722 | $user_id, |
| 723 | 723 | 0, |
| @@ -722,19 +722,16 @@ | ||
| 722 | 722 | $user_id, |
| 723 | 723 | 0, |
| 724 | 724 | $type, |
| 725 | 725 | (string) $ref, |
| 726 | - array( | |
| 727 | - 'x' => 16 + $col * 96, | |
| 728 | - 'y' => 16 + $row * 110, | |
| 729 | - ) | |
| 726 | + openstation_files_grid_cell_to_point( $col, $row ) | |
| 730 | 727 | ); |
| 731 | 728 | if ( ! is_wp_error( $result ) ) { |
| 732 | 729 | ++$placed; |
| 733 | 730 | } |
| 734 | 731 | }; |
| 735 | - $emit_next = function ( $type, $ref ) use ( $find_next, $emit_at ) { | |
| 736 | - list( $col, $row ) = $find_next(); | |
| 732 | + $emit_next = function ( $type, $ref ) use ( &$occupied, $order, $emit_at ) { | |
| 733 | + list( $col, $row ) = openstation_files_grid_next_free( $occupied, $order ); | |
| 737 | 734 | $emit_at( $type, $ref, $col, $row ); |
| 738 | 735 | }; |
| 739 | 736 | |
| 740 | 737 | // Pinned shortcuts get reserved top-left slots. Anchored to |