fix(posts): allow deleting Failed posts + misc terminal-state polish

- Add separate canDelete predicate in Index.vue that includes Failed —
  the previous EDITABLE_STATUSES gating hid the delete button for
  failed posts even though the backend allows deleting them.
- Edit.vue Echo handler navigates to /show when an in-page real-time
  status update transitions the post into a read-only state, instead
  of leaving the user on a stuck readonly editor.
- FacebookPublisher: stop using empty() for content checks (treats
  literal "0" as empty) — compare explicitly against null and "".
- Update terminal-state error messages in API + MCP update tool to
  reflect the broadened guard (no longer Published-only). Adjust the
  matching test assertion.
This commit is contained in:
Paulo Castellano 2026-05-19 14:54:04 -03:00
parent 31db72638b
commit e4f8833608
6 changed files with 20 additions and 15 deletions

View file

@ -69,7 +69,7 @@ public function update(UpdatePostRequest $request, Post $post): PostResource|Jso
if (data_get($result, 'action') === PostAction::Finalized) {
return response()->json(
['message' => 'Cannot edit a published post.'],
['message' => 'Cannot edit a post in a terminal state.'],
Response::HTTP_UNPROCESSABLE_ENTITY
);
}

View file

@ -55,7 +55,7 @@ public function handle(Request $request): Response|ResponseFactory
$result = UpdatePost::execute($workspace, $post, $payload);
if (data_get($result, 'action') === PostAction::Finalized) {
return Response::error('Cannot edit a published post.');
return Response::error('Cannot edit a post in a terminal state.');
}
/** @var Post $updated */

View file

@ -49,7 +49,7 @@ private function publishPost(string $pageId, string $accessToken, ?string $conte
{
// Text only post
if ($media->isEmpty()) {
if (empty($content)) {
if ($content === null || $content === '') {
throw new \Exception('Facebook text posts require content. Please add text to your post.');
}
@ -107,7 +107,7 @@ private function publishSingleImagePost(string $pageId, string $accessToken, ?st
'access_token' => $accessToken,
];
if (! empty($content)) {
if ($content !== null && $content !== '') {
$payload['message'] = $content;
}
@ -167,7 +167,7 @@ private function publishMultiImagePost(string $pageId, string $accessToken, ?str
'access_token' => $accessToken,
];
if (! empty($content)) {
if ($content !== null && $content !== '') {
$postData['message'] = $content;
}
@ -201,7 +201,7 @@ private function publishVideoPost(string $pageId, string $accessToken, ?string $
'access_token' => $accessToken,
];
if (! empty($content)) {
if ($content !== null && $content !== '') {
$payload['description'] = $content;
}
@ -306,7 +306,7 @@ private function publishReel(string $pageId, string $accessToken, ?string $conte
'access_token' => $accessToken,
];
if (! empty($content)) {
if ($content !== null && $content !== '') {
$finishPayload['description'] = $content;
}

View file

@ -20,7 +20,7 @@ import { ContentType } from '@/enums/content-type';
import { Platform } from '@/enums/platform';
import { PostStatus } from '@/types/post';
import AppLayout from '@/layouts/AppLayout.vue';
import { destroy as destroyPost, update as updatePost } from '@/routes/app/posts';
import { destroy as destroyPost, show as showPost, update as updatePost } from '@/routes/app/posts';
import type { PinterestBoard } from '@/types';
interface MediaItem {
@ -514,12 +514,15 @@ const unschedulePost = () => {
submit(PostStatus.Draft);
};
// Echo: listen for real-time platform status updates.
// Event fires when any post_platform completes publishing (success or fail).
// Full reload of the post prop so the new status + post_platforms propagate and
// the overlay dismisses.
usePostEcho(post.value.id, '.post.platform.status.updated', () => {
router.reload({ only: ['post'] });
router.reload({
only: ['post'],
onSuccess: () => {
if (READONLY_STATUSES.includes(post.value.status)) {
router.visit(showPost.url(post.value.id));
}
},
});
});

View file

@ -138,7 +138,9 @@ const getPostPreview = (post: Post): string =>
post.content?.trim() || trans('calendar.no_content');
const EDITABLE_STATUSES: readonly string[] = [PostStatus.Draft, PostStatus.Scheduled];
const DELETABLE_STATUSES: readonly string[] = [PostStatus.Draft, PostStatus.Scheduled, PostStatus.Failed];
const canEdit = (post: Post): boolean => EDITABLE_STATUSES.includes(post.status);
const canDelete = (post: Post): boolean => DELETABLE_STATUSES.includes(post.status);
const postUrl = (post: Post): string =>
canEdit(post) ? editPost.url(post.id) : showPost.url(post.id);
@ -303,7 +305,7 @@ useWorkspaceEcho(
<IconCopy class="size-4" />
{{ $t('posts.actions.copy_id') }}
</DropdownMenuItem>
<template v-if="canEdit(post)">
<template v-if="canDelete(post)">
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"

View file

@ -116,7 +116,7 @@
$response = TryPostServer::actingAs($this->user)
->tool(UpdatePostTool::class, ['post_id' => $post->id, 'content' => 'x']);
$response->assertHasErrors(['Cannot edit a published post.']);
$response->assertHasErrors(['Cannot edit a post in a terminal state.']);
})->with([
PostStatus::Published,
PostStatus::PartiallyPublished,