Skip to content

feat(structure): slice priority on add + new /move endpoint#50

Open
marcohanke wants to merge 3 commits into
mainfrom
feature/slice-priority-and-move
Open

feat(structure): slice priority on add + new /move endpoint#50
marcohanke wants to merge 3 commits into
mainfrom
feature/slice-priority-and-move

Conversation

@marcohanke

Copy link
Copy Markdown
Member

Summary

  • Add priority body param to POST /api/structure/articles/{id}/slices. Optional; when omitted, behaviour is unchanged (slice appended at end). When set (e.g. priority: 1), the slice is inserted at that position within its (article, clang, ctype) group. The value is forwarded into the $data array of rex_content_service::addSlice(), which already understands priority (content_service.php:17-24) and re-normalises the sequence via rex_sql_util::organizePriorities(). No new SQL, no new EPs.
  • New POST /api/structure/articles/{id}/slices/{slice_id}/move endpoint with body { "direction": "moveup" | "movedown" }. Mirrors REDAXO core rex_api_content_move_slice::execute() 1:1 (addons/structure/plugins/content/lib/api_functions/api_content.php). clang_id and module_id are derived from the slice row. Permission cascade for session-cookie callers: moveSlice[] perm → structure category perm → modules complex-perm. Admins bypass moveSlice[]. Bearer-token callers pass through scope-based auth (structure/articles/slices/move). On boundary (moveup on first slice etc.) rex_api_exception surfaces as 422 with the original i18n message.
  • Backend mirror is automatic via RoutePackage/Backend/Structure.phpPOST /api/backend/structure/articles/{id}/slices/{slice_id}/move is available with session-cookie auth without a separate Backend implementation.
  • Out of scope by design (CLAUDE.md "exaktes Spiegeln des Core-Verhaltens"): updating priority via PATCH /slices/{id} (backend edit page also doesn't), absolute-position moves, and convenience move-to-top/move-to-bottom endpoints (use priority: 1 on add instead).

Test plan

  • ./vendor/bin/phpunit tests/StructureApiTest.php against a configured tests/.env instance — covers testCreateArticleSliceAtTop, testCreateArticleSliceWithoutPriorityAppends, testMoveArticleSliceUp, testMoveArticleSliceDown, testMoveArticleSliceInvalidDirection, testMoveArticleSliceNotFound.
  • ./vendor/bin/phpunit tests/BackendApiTest.phptestAdminCreateArticleSliceAtTop, testAdminMoveArticleSlice, testRestrictedUserCannotMoveArticleSlice.
  • Manual smoke against a local REDAXO instance:
    • POST /api/structure/articles/{id}/slices with priority: 1 → 201, slice appears at top of its ctype in the list.
    • POST /api/structure/articles/{id}/slices/{slice_id}/move with direction: "movedown" → 200, list reflects new order.
    • POST .../move with direction: "moveup" on the top-most slice → 422.
    • POST .../move with direction: "sideways" → 400.
  • Swagger UI (/redaxo/index.php?page=api/openapi): new priority field appears on the add-slice request schema, new /move endpoint is listed under structure.
  • Token-Scope dropdown in the backend shows structure/articles/slices/move and backend/structure/articles/slices/move; a token without the scope returns 401 on the new endpoint.

POST /api/structure/articles/{id}/slices now accepts an optional `priority`
integer in the body. When omitted, the slice is appended at the end (default
behaviour, unchanged). When set to 1 (or any positive integer), the slice is
inserted at that priority within its (article, clang, ctype) group; the core
service's rex_sql_util::organizePriorities() pass renormalises the sequence.

The handler now forwards `priority` into the $data array that
rex_content_service::addSlice() already understands (content_service.php:17-24)
— no new SQL, no new EPs, no behavioural change for callers that don't send
the param.
New POST /api/structure/articles/{id}/slices/{slice_id}/move endpoint with
body { "direction": "moveup" | "movedown" }. Mirrors REDAXO core's
rex_api_content_move_slice exactly:

- Scope: structure/articles/slices/move (Bearer) and the automatically
  generated backend/structure/articles/slices/move (session-cookie auth).
- clang_id and module_id are derived from the slice row (no extra body params).
- Permission cascade for session-cookie callers: moveSlice[] perm,
  structure category perm, modules complex-perm. Admins bypass moveSlice[].
- Delegates to rex_content_service::moveSlice() which fires SLICE_MOVE,
  swaps priority, runs rex_sql_util::organizePriorities(),
  calls rex_article_cache::deleteContent() and fires art_content_updated.
  No extra EPs or cache calls from the API layer — same as the core API
  function.
- rex_api_exception (e.g. first slice tried to moveup) is surfaced as 422
  with the original i18n message; any other Exception falls back to 500.
Bearer-scope tests in StructureApiTest.php:
- testCreateArticleSliceAtTop: POST with priority=1, then verify the new
  slice is first in its ctype with priority=1.
- testCreateArticleSliceWithoutPriorityAppends: regression — without the
  param, the slice keeps the previous append-at-end behaviour.
- testMoveArticleSliceUp / testMoveArticleSliceDown: two-slice setups,
  swap order via the new /move endpoint and verify priorities flip.
- testMoveArticleSliceInvalidDirection: direction=sideways → 400.
- testMoveArticleSliceNotFound: 404 on non-existent slice id.

Backend-session tests in BackendApiTest.php:
- testAdminCreateArticleSliceAtTop: admin session can create with priority=1.
- testAdminMoveArticleSlice: admin session moves a slice via /move.
- testRestrictedUserCannotMoveArticleSlice: restricted user (no moveSlice[]
  perm, no category perm) must get 403/404, not 200.

Tests gracefully skip when the test article's template lacks the configured
module — same pattern as the existing slice tests.
Copilot AI review requested due to automatic review settings June 11, 2026 08:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds slice ordering controls to the Structure API by allowing explicit insertion position on slice creation and introducing a dedicated slice move endpoint (up/down) that mirrors REDAXO core behavior, along with new API/Backend tests.

Changes:

  • Extend POST /structure/articles/{id}/slices to accept optional priority to insert a slice at a specific position.
  • Add POST /structure/articles/{id}/slices/{slice_id}/move with { direction: "moveup" | "movedown" } and permission checks.
  • Add/extend PHPUnit coverage for priority insertion and slice moving (bearer + backend).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/StructureApiTest.php Adds bearer-auth tests for priority insertion and slice move behavior (contains assertion ordering bugs that need fixing).
tests/BackendApiTest.php Adds backend-admin + restricted-user tests for priority insertion and slice moving (contains an assertion ordering bug that needs fixing).
lib/RoutePackage/Structure.php Adds priority handling in add-slice body and registers/implements the new /move endpoint with 422 mapping for boundary moves.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/BackendApiTest.php
Comment on lines +818 to +824
$getSecond = $this->adminGet('structure/articles/' . $articleId . '/slices/' . $secondId);
$getFirst = $this->adminGet('structure/articles/' . $articleId . '/slices/' . $firstId);
$this->assertLessThan(
(int) $getFirst['data']['priority'],
(int) $getSecond['data']['priority'],
'After moveup the second slice should now have a lower priority than the first',
);

$getSecond = $this->get('structure/articles/' . $articleId . '/slices/' . $secondId);
$getFirst = $this->get('structure/articles/' . $articleId . '/slices/' . $firstId);
$this->assertLessThan((int) $getFirst['data']['priority'], (int) $getSecond['data']['priority']);

$getFirst = $this->get('structure/articles/' . $articleId . '/slices/' . $firstId);
$getSecond = $this->get('structure/articles/' . $articleId . '/slices/' . $secondId);
$this->assertGreaterThan((int) $getSecond['data']['priority'], (int) $getFirst['data']['priority']);
Comment on lines +1529 to +1537
} catch (rex_api_exception $e) {
// moveSlice throws when the slice is already at the boundary (top can't moveup, etc.)
// — surface as 422 rather than 500 since the request was structurally valid but the
// requested state transition is not possible.
return new JsonResponse([
'error' => $e->getMessage(),
'slice_id' => $sliceId,
'direction' => $direction,
], 422);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants