| @@ -213,17 +213,24 @@ | ||
| 213 | 213 | } |
| 214 | 214 | if ( '/' !== substr( $rel, -1 ) ) { |
| 215 | 215 | $rel .= '/'; // Whole string is a directory path. |
| 216 | 216 | } |
| 217 | + $created = array(); | |
| 217 | 218 | $folder_id = openstation_files_resolve_relative_path( |
| 218 | 219 | get_current_user_id(), |
| 219 | 220 | (int) $req->get_param( 'parentId' ), |
| 220 | - $rel | |
| 221 | + $rel, | |
| 222 | + $created | |
| 221 | 223 | ); |
| 222 | 224 | if ( is_wp_error( $folder_id ) ) { |
| 223 | 225 | return $folder_id; |
| 224 | 226 | } |
| 225 | - return rest_ensure_response( array( 'folderId' => (int) $folder_id ) ); | |
| 227 | + return rest_ensure_response( | |
| 228 | + array( | |
| 229 | + 'folderId' => (int) $folder_id, | |
| 230 | + 'createdFolders' => openstation_files_shape_created_folders( $created ), | |
| 231 | + ) | |
| 232 | + ); | |
| 226 | 233 | } |
| 227 | 234 | |
| 228 | 235 | /** |
| 229 | 236 | * PATCH /files/uploads/<id> — rename (owner only). Not-found and |
| @@ -323,15 +330,50 @@ | ||
| 323 | 330 | |
| 324 | 331 | $row = openstation_files_get_placement( $registered['placement_id'] ); |
| 325 | 332 | return rest_ensure_response( |
| 326 | 333 | array( |
| 327 | - 'placement' => openstation_files_shape_placement( $row ), | |
| 328 | - 'storedFileId' => (int) $registered['file_id'], | |
| 334 | + 'placement' => openstation_files_shape_placement( $row ), | |
| 335 | + 'storedFileId' => (int) $registered['file_id'], | |
| 336 | + 'createdFolders' => openstation_files_shape_created_folders( $registered['created_folders'] ), | |
| 329 | 337 | ) |
| 330 | 338 | ); |
| 331 | 339 | } |
| 332 | 340 | |
| 333 | 341 | /** |
| 342 | + * Shape the folders a request created mkdir-p style so the client | |
| 343 | + * can paint their tiles the moment they exist. Each entry carries | |
| 344 | + * the folder row AND its placement in the parent the client is | |
| 345 | + * looking at — the per-file `placement` in the same response only | |
| 346 | + * describes the file inside the leaf folder, which is why the | |
| 347 | + * wallpaper tile used to wait for the end-of-batch resync (or the | |
| 348 | + * next Heartbeat delta) after a folder-tree drop. | |
| 349 | + * | |
| 350 | + * @internal | |
| 351 | + * | |
| 352 | + * @param array $created `{ folder_id, placement_id }` rows from | |
| 353 | + * `openstation_files_resolve_relative_path()`. | |
| 354 | + * @return array<int,array{folder:array,placement:array}> | |
| 355 | + */ | |
| 356 | +function openstation_files_shape_created_folders( $created ) { | |
| 357 | + $out = array(); | |
| 358 | + foreach ( (array) $created as $entry ) { | |
| 359 | + if ( empty( $entry['folder_id'] ) || empty( $entry['placement_id'] ) ) { | |
| 360 | + continue; | |
| 361 | + } | |
| 362 | + $folder = openstation_files_get_folder( (int) $entry['folder_id'] ); | |
| 363 | + $placement = openstation_files_get_placement( (int) $entry['placement_id'] ); | |
| 364 | + if ( ! $folder || ! $placement ) { | |
| 365 | + continue; | |
| 366 | + } | |
| 367 | + $out[] = array( | |
| 368 | + 'folder' => openstation_files_shape_folder( $folder ), | |
| 369 | + 'placement' => openstation_files_shape_placement( $placement ), | |
| 370 | + ); | |
| 371 | + } | |
| 372 | + return $out; | |
| 373 | +} | |
| 374 | + | |
| 375 | +/** | |
| 334 | 376 | * Receive step: validate and move the bytes into the owner's |
| 335 | 377 | * storage dir under a fresh UUID disk name. Returns |
| 336 | 378 | * `{ path, disk_name, display_name, size_bytes, mime }` or an |
| 337 | 379 | * error. No DB writes happen here. |
| @@ -476,15 +518,20 @@ | ||
| 476 | 518 | * @param string $relative_path Optional `a/b/c.ext` path; directory |
| 477 | 519 | * segments are resolved under `$parent_id`. |
| 478 | 520 | * @param array|null $coords `x`, `y` for the placement, or null |
| 479 | 521 | * to auto-place at the next free slot. |
| 480 | - * @return array|WP_Error `{ file_id, placement_id }`. | |
| 522 | + * @return array|WP_Error `{ file_id, placement_id, created_folders }` — | |
| 523 | + * `created_folders` lists the `{ folder_id, | |
| 524 | + * placement_id }` pairs the relative path | |
| 525 | + * created (empty for flat uploads and for | |
| 526 | + * segments that already existed). | |
| 481 | 527 | */ |
| 482 | 528 | function openstation_files_upload_register( $user_id, $received, $parent_id, $relative_path = '', $coords = null ) { |
| 483 | 529 | $parent_id = max( 0, (int) $parent_id ); |
| 530 | + $created = array(); | |
| 484 | 531 | |
| 485 | 532 | if ( '' !== (string) $relative_path ) { |
| 486 | - $resolved = openstation_files_resolve_relative_path( (int) $user_id, $parent_id, (string) $relative_path ); | |
| 533 | + $resolved = openstation_files_resolve_relative_path( (int) $user_id, $parent_id, (string) $relative_path, $created ); | |
| 487 | 534 | if ( is_wp_error( $resolved ) ) { |
| 488 | 535 | return $resolved; |
| 489 | 536 | } |
| 490 | 537 | $parent_id = $resolved; |
| @@ -544,10 +591,11 @@ | ||
| 544 | 591 | */ |
| 545 | 592 | do_action( 'openstation_stored_file_uploaded', (int) $file_id, (int) $placement_id, (int) $user_id ); |
| 546 | 593 | |
| 547 | 594 | return array( |
| 548 | - 'file_id' => (int) $file_id, | |
| 549 | - 'placement_id' => (int) $placement_id, | |
| 595 | + 'file_id' => (int) $file_id, | |
| 596 | + 'placement_id' => (int) $placement_id, | |
| 597 | + 'created_folders' => $created, | |
| 550 | 598 | ); |
| 551 | 599 | } |
| 552 | 600 | |
| 553 | 601 | /** |
| @@ -563,11 +611,18 @@ | ||
| 563 | 611 | * @param int $base_parent_id Folder to resolve under (0 = root). |
| 564 | 612 | * @param string $relative_path `a/b/c.ext` or `a/b/` (trailing |
| 565 | 613 | * slash = pure directory path, e.g. |
| 566 | 614 | * an empty folder from a drag). |
| 615 | + * @param array $created Optional, by reference. Receives one | |
| 616 | + * `{ folder_id, placement_id }` entry per | |
| 617 | + * folder this call created, outermost | |
| 618 | + * first, so the caller can hand the new | |
| 619 | + * tiles to the client in the same | |
| 620 | + * response. Reused segments are not | |
| 621 | + * listed. | |
| 567 | 622 | * @return int|WP_Error Folder id to place the file in. |
| 568 | 623 | */ |
| 569 | -function openstation_files_resolve_relative_path( $user_id, $base_parent_id, $relative_path ) { | |
| 624 | +function openstation_files_resolve_relative_path( $user_id, $base_parent_id, $relative_path, &$created = null ) { | |
| 570 | 625 | global $wpdb; |
| 571 | 626 | $user_id = (int) $user_id; |
| 572 | 627 | $parent_id = max( 0, (int) $base_parent_id ); |
| 573 | 628 | $path = str_replace( '\\', '/', (string) $relative_path ); |
| @@ -627,11 +682,21 @@ | ||
| 627 | 682 | $folder_id = openstation_files_create_folder( $user_id, array( 'name' => $name ) ); |
| 628 | 683 | if ( is_wp_error( $folder_id ) ) { |
| 629 | 684 | return $folder_id; |
| 630 | 685 | } |
| 631 | - $placement = openstation_files_place( $user_id, $parent_id, 'folder', (string) $folder_id ); | |
| 686 | + // Next free slot, same as the file placements below: a bare | |
| 687 | + // `openstation_files_place()` pins the tile at 0,0, which the | |
| 688 | + // layer only displaces client-side — the stored coordinates | |
| 689 | + // stay wrong and the tile jumps on the next repaint. | |
| 690 | + $placement = openstation_files_place_at_next_free_slot( $user_id, $parent_id, 'folder', (string) $folder_id ); | |
| 632 | 691 | if ( is_wp_error( $placement ) ) { |
| 633 | 692 | return $placement; |
| 693 | + } | |
| 694 | + if ( is_array( $created ) ) { | |
| 695 | + $created[] = array( | |
| 696 | + 'folder_id' => (int) $folder_id, | |
| 697 | + 'placement_id' => (int) $placement, | |
| 698 | + ); | |
| 634 | 699 | } |
| 635 | 700 | $parent_id = (int) $folder_id; |
| 636 | 701 | } |
| 637 | 702 | return $parent_id; |