Merge pull request #39 from trypostit/fix/pinterest-board-picker
fix(pinterest): restore board picker + require board_id in validation
This commit is contained in:
commit
4cd0b642e1
15 changed files with 465 additions and 13 deletions
|
|
@ -234,15 +234,15 @@ public function edit(Request $request, Post $post): Response|RedirectResponse
|
|||
$account->id => new PlatformConfigResource($account),
|
||||
]);
|
||||
|
||||
$pinterestBoards = [];
|
||||
$pinterestAccount = $socialAccounts->firstWhere('platform', Platform::Pinterest);
|
||||
if ($pinterestAccount) {
|
||||
try {
|
||||
$pinterestBoards = app(PinterestPublisher::class)->getBoards($pinterestAccount);
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail - boards will be empty
|
||||
}
|
||||
}
|
||||
$pinterestBoards = $socialAccounts
|
||||
->where('platform', Platform::Pinterest)
|
||||
->mapWithKeys(fn ($account) => [
|
||||
$account->id => rescue(
|
||||
fn () => app(PinterestPublisher::class)->getBoards($account),
|
||||
[],
|
||||
report: false,
|
||||
),
|
||||
]);
|
||||
|
||||
$tiktokCreatorInfos = $socialAccounts
|
||||
->where('platform', Platform::TikTok)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ public function rules(): array
|
|||
'platforms.*.meta.disclose' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.brand_content_toggle' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.brand_organic_toggle' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.board_id' => ['sometimes', 'nullable', 'string'],
|
||||
'label_ids' => ['sometimes', 'array'],
|
||||
'label_ids.*' => ['uuid', Rule::exists('workspace_labels', 'id')->where('workspace_id', $this->user()->currentWorkspace->id)],
|
||||
];
|
||||
|
|
@ -106,6 +107,14 @@ public function withValidator(Validator $validator): void
|
|||
trans('posts.form.tiktok.privacy_required'),
|
||||
);
|
||||
}
|
||||
|
||||
if ($platformEnum === Platform::Pinterest
|
||||
&& blank(data_get($platform, 'meta.board_id'))) {
|
||||
$validator->errors()->add(
|
||||
"platforms.{$i}.meta.board_id",
|
||||
trans('posts.form.pinterest.board_required'),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,12 @@
|
|||
'video_pin' => 'Video Pin',
|
||||
'carousel' => 'Carousel',
|
||||
],
|
||||
'board' => 'Board',
|
||||
'select_board' => 'Select a board',
|
||||
'no_boards' => 'No Pinterest boards found. Create one in your Pinterest account first.',
|
||||
'search_board' => 'Search boards...',
|
||||
'no_board_found' => 'No board matches your search.',
|
||||
'board_required' => 'Select a Pinterest board to publish this post.',
|
||||
],
|
||||
'warnings' => [
|
||||
'no_variant' => 'Pick a post type to continue.',
|
||||
|
|
|
|||
|
|
@ -147,6 +147,12 @@
|
|||
'video_pin' => 'Video Pin',
|
||||
'carousel' => 'Carrusel',
|
||||
],
|
||||
'board' => 'Tablero',
|
||||
'select_board' => 'Selecciona un tablero',
|
||||
'no_boards' => 'No se encontraron tableros de Pinterest. Crea uno en tu cuenta de Pinterest primero.',
|
||||
'search_board' => 'Buscar tableros...',
|
||||
'no_board_found' => 'Ningún tablero coincide con tu búsqueda.',
|
||||
'board_required' => 'Selecciona un tablero de Pinterest para publicar este post.',
|
||||
],
|
||||
'warnings' => [
|
||||
'no_variant' => 'Elige un tipo de publicación para continuar.',
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -147,6 +147,12 @@
|
|||
'video_pin' => 'Video Pin',
|
||||
'carousel' => 'Carrossel',
|
||||
],
|
||||
'board' => 'Quadro',
|
||||
'select_board' => 'Selecione um quadro',
|
||||
'no_boards' => 'Nenhum quadro do Pinterest encontrado. Crie um na sua conta do Pinterest primeiro.',
|
||||
'search_board' => 'Pesquisar quadros...',
|
||||
'no_board_found' => 'Nenhum quadro encontrado.',
|
||||
'board_required' => 'Selecione um quadro do Pinterest para publicar este post.',
|
||||
],
|
||||
'warnings' => [
|
||||
'no_variant' => 'Escolha um tipo de publicação para continuar.',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { IconAlertTriangle, IconChevronDown, IconChevronUp } from '@tabler/icons-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxAnchor,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from '@/components/ui/combobox';
|
||||
import { getMediaValidationWarning, type MediaItem } from '@/composables/useMedia';
|
||||
import { getPlatformLogo } from '@/composables/usePlatformLogo';
|
||||
import { ContentType } from '@/enums/content-type';
|
||||
import type { PinterestBoard } from '@/types';
|
||||
|
||||
interface SocialAccount {
|
||||
id: string;
|
||||
|
|
@ -15,10 +28,17 @@ interface SocialAccount {
|
|||
avatar_url: string | null;
|
||||
}
|
||||
|
||||
interface BoardOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
socialAccount: SocialAccount | null;
|
||||
contentType: string;
|
||||
media: MediaItem[];
|
||||
boards: PinterestBoard[];
|
||||
meta: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +48,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
const emit = defineEmits<{
|
||||
'update:contentType': [value: string];
|
||||
'update:meta': [value: Record<string, any>];
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
|
|
@ -44,6 +65,26 @@ const pickVariant = (value: string) => {
|
|||
};
|
||||
|
||||
const warning = computed(() => getMediaValidationWarning(props.contentType, props.media));
|
||||
|
||||
const boardOptions = computed<BoardOption[]>(() =>
|
||||
props.boards.map((b) => ({ value: b.id, label: b.name })),
|
||||
);
|
||||
|
||||
const selectedBoard = computed<BoardOption | undefined>({
|
||||
get: () => boardOptions.value.find((b) => b.value === props.meta?.board_id),
|
||||
set: (board) => emit('update:meta', { ...props.meta, board_id: board?.value ?? null }),
|
||||
});
|
||||
|
||||
// Surface the backend validation error keyed by platform index
|
||||
// (`platforms.0.meta.board_id`). Suffix match avoids threading the index
|
||||
// through props. Cleared as soon as a board is picked locally so the user
|
||||
// doesn't see a stale error after fixing the issue.
|
||||
const page = usePage();
|
||||
const boardError = computed<string | undefined>(() => {
|
||||
if (props.meta?.board_id) return undefined;
|
||||
const errors = (page.props.errors as Record<string, string> | undefined) ?? {};
|
||||
return Object.entries(errors).find(([key]) => key.endsWith('.meta.board_id'))?.[1];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -99,6 +140,54 @@ const warning = computed(() => getMediaValidationWarning(props.contentType, prop
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.form.pinterest.board') }}</p>
|
||||
<p
|
||||
v-if="boards.length === 0"
|
||||
class="flex items-start gap-2 rounded-lg border-2 border-foreground/30 bg-foreground/5 p-2 text-xs font-semibold text-foreground/60"
|
||||
>
|
||||
<IconAlertTriangle class="mt-0.5 size-3.5 shrink-0" />
|
||||
{{ $t('posts.form.pinterest.no_boards') }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<Combobox
|
||||
v-model="selectedBoard"
|
||||
:display-value="(b: BoardOption | undefined) => b?.label ?? ''"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<ComboboxAnchor class="w-full">
|
||||
<ComboboxTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between rounded-lg border-2 bg-card px-3 py-2 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="boardError ? 'border-rose-500' : 'border-foreground/30 hover:border-foreground'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span :class="selectedBoard ? 'text-foreground' : 'text-foreground/50'">
|
||||
{{ selectedBoard ? selectedBoard.label : $t('posts.form.pinterest.select_board') }}
|
||||
</span>
|
||||
<IconChevronDown class="size-4 shrink-0 text-foreground/60" />
|
||||
</button>
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
<ComboboxList>
|
||||
<ComboboxInput :placeholder="$t('posts.form.pinterest.search_board')" />
|
||||
<ComboboxEmpty>{{ $t('posts.form.pinterest.no_board_found') }}</ComboboxEmpty>
|
||||
<ComboboxGroup>
|
||||
<ComboboxItem
|
||||
v-for="board in boardOptions"
|
||||
:key="board.value"
|
||||
:value="board"
|
||||
>
|
||||
{{ board.label }}
|
||||
</ComboboxItem>
|
||||
</ComboboxGroup>
|
||||
</ComboboxList>
|
||||
</Combobox>
|
||||
<InputError :message="boardError" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="warning"
|
||||
class="flex items-start gap-2 rounded-lg border-2 border-foreground bg-rose-50 p-2 text-xs font-semibold text-rose-700"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import CommentsTab from '@/components/posts/editor/CommentsTab.vue';
|
|||
import PreviewTab from '@/components/posts/editor/PreviewTab.vue';
|
||||
import ScheduleTab from '@/components/posts/editor/ScheduleTab.vue';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import type { PinterestBoard } from '@/types';
|
||||
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
|
|
@ -71,6 +72,7 @@ const props = defineProps<{
|
|||
labels: { id: string; name: string; color: string }[];
|
||||
selectedLabelIds: string[];
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
pinterestBoards?: Record<string, PinterestBoard[]> | null;
|
||||
isReadOnly: boolean;
|
||||
authUserId: string;
|
||||
initialHighlightCommentId: string | null;
|
||||
|
|
@ -127,6 +129,7 @@ defineExpose({
|
|||
:platform-content-types="platformContentTypes"
|
||||
:platform-issues="platformIssues"
|
||||
:tiktok-creator-infos="tiktokCreatorInfos"
|
||||
:pinterest-boards="pinterestBoards"
|
||||
:media="media"
|
||||
@toggle-platform="(id) => emit('toggle-platform', id)"
|
||||
@toggle-label="(id) => emit('toggle-label', id)"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Badge } from '@/components/ui/badge';
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { getPlatformLabel, getPlatformLogo } from '@/composables/usePlatformLogo';
|
||||
import { Platform } from '@/enums/platform';
|
||||
import type { PinterestBoard } from '@/types';
|
||||
|
||||
interface SocialAccount {
|
||||
id: string;
|
||||
|
|
@ -86,6 +87,7 @@ const props = defineProps<{
|
|||
platformContentTypes: Record<string, string>;
|
||||
platformIssues?: Record<string, string>;
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
pinterestBoards?: Record<string, PinterestBoard[]> | null;
|
||||
media?: MediaItem[];
|
||||
}>();
|
||||
|
||||
|
|
@ -134,6 +136,9 @@ const getPublishConfig = (pp: PostPlatform): Record<string, any> | null =>
|
|||
const getCreatorInfo = (pp: PostPlatform): TikTokCreatorInfo | null =>
|
||||
pp.social_account_id ? props.tiktokCreatorInfos?.[pp.social_account_id] ?? null : null;
|
||||
|
||||
const getBoards = (pp: PostPlatform): PinterestBoard[] =>
|
||||
pp.social_account_id ? props.pinterestBoards?.[pp.social_account_id] ?? [] : [];
|
||||
|
||||
const videoDurationSec = computed(() => {
|
||||
const video = props.media?.find((m) => m.type === 'video' || m.mime_type?.startsWith('video/'));
|
||||
const duration = video?.meta?.duration;
|
||||
|
|
@ -321,8 +326,11 @@ const getPlatformAvatar = (pp: PostPlatform): string | null =>
|
|||
:social-account="pp.social_account"
|
||||
:content-type="platformContentTypes[pp.id] ?? ''"
|
||||
:media="media ?? []"
|
||||
:boards="getBoards(pp)"
|
||||
:meta="platformMeta[pp.id] ?? {}"
|
||||
:disabled="isReadOnly"
|
||||
@update:content-type="emit('update:platformContentType', pp.id, $event)"
|
||||
@update:meta="emit('update:platformMeta', pp.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import debounce from '@/debounce';
|
|||
import { Platform } from '@/enums/platform';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { destroy as destroyPost, update as updatePost } from '@/routes/app/posts';
|
||||
import type { PinterestBoard } from '@/types';
|
||||
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
|
|
@ -88,7 +89,7 @@ const props = defineProps<{
|
|||
post: Post;
|
||||
socialAccounts: SocialAccount[];
|
||||
platformConfigs: Record<string, any>;
|
||||
pinterestBoards: any[];
|
||||
pinterestBoards: Record<string, PinterestBoard[]>;
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
labels: { id: string; name: string; color: string }[];
|
||||
signatures: { id: string; name: string; content: string }[];
|
||||
|
|
@ -276,6 +277,14 @@ const tiktokComplianceValid = computed(() => {
|
|||
});
|
||||
});
|
||||
|
||||
// Pinterest needs a board_id selected to publish (the API rejects without it).
|
||||
const pinterestComplianceValid = computed(() => {
|
||||
const pinterestPlatforms = post.value.post_platforms.filter(
|
||||
(pp) => pp.platform === Platform.Pinterest && selectedPlatformIds.value.includes(pp.id),
|
||||
);
|
||||
return pinterestPlatforms.every((pp) => Boolean(platformMeta.value[pp.id]?.board_id));
|
||||
});
|
||||
|
||||
const contentLengthOverflows = computed(() => {
|
||||
const len = content.value.length;
|
||||
return platformLimits.value
|
||||
|
|
@ -286,6 +295,7 @@ const contentLengthOverflows = computed(() => {
|
|||
const canSchedule = computed(
|
||||
() => mediaCompliancePerPlatformValid.value
|
||||
&& tiktokComplianceValid.value
|
||||
&& pinterestComplianceValid.value
|
||||
&& contentLengthOverflows.value.length === 0,
|
||||
);
|
||||
|
||||
|
|
@ -321,6 +331,10 @@ const postActionTooltip = computed(() => {
|
|||
return trans('posts.form.tiktok.compliance_incomplete');
|
||||
}
|
||||
|
||||
if (!pinterestComplianceValid.value) {
|
||||
return trans('posts.form.pinterest.board_required');
|
||||
}
|
||||
|
||||
return trans('posts.edit.compliance_incomplete');
|
||||
});
|
||||
|
||||
|
|
@ -576,6 +590,7 @@ usePostEcho(post.value.id, '.post.comment.created', (e: any) => {
|
|||
:labels="labels"
|
||||
:selected-label-ids="selectedLabelIds"
|
||||
:tiktok-creator-infos="tiktokCreatorInfos"
|
||||
:pinterest-boards="pinterestBoards"
|
||||
:is-read-only="isLocked"
|
||||
:auth-user-id="authUserId"
|
||||
:initial-highlight-comment-id="initialHighlightCommentId"
|
||||
|
|
|
|||
5
resources/js/types/index.d.ts
vendored
5
resources/js/types/index.d.ts
vendored
|
|
@ -90,3 +90,8 @@ export type BreadcrumbItem = {
|
|||
href?: string;
|
||||
};
|
||||
|
||||
export interface PinterestBoard {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,41 @@
|
|||
->toThrow(Exception::class, 'Pinterest board_id is required');
|
||||
});
|
||||
|
||||
test('pinterest publisher throws exception for carousel when no board id', function () {
|
||||
$this->postPlatform->update([
|
||||
'content_type' => ContentType::PinterestCarousel,
|
||||
'meta' => [],
|
||||
]);
|
||||
$this->socialAccount->update(['meta' => []]);
|
||||
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
['id' => 'm1', 'path' => 'media/img1.jpg', 'url' => 'https://example.com/img1.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'img1.jpg'],
|
||||
['id' => 'm2', 'path' => 'media/img2.jpg', 'url' => 'https://example.com/img2.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'img2.jpg'],
|
||||
],
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Pinterest board_id is required');
|
||||
});
|
||||
|
||||
test('pinterest publisher throws exception for video pin when no board id', function () {
|
||||
$this->postPlatform->update([
|
||||
'content_type' => ContentType::PinterestVideoPin,
|
||||
'meta' => [],
|
||||
]);
|
||||
$this->socialAccount->update(['meta' => []]);
|
||||
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
['id' => 'm1', 'path' => 'media/video.mp4', 'url' => 'https://example.com/video.mp4', 'mime_type' => 'video/mp4', 'original_filename' => 'video.mp4'],
|
||||
],
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Pinterest board_id is required');
|
||||
});
|
||||
|
||||
test('pinterest publisher uses default board id from account', function () {
|
||||
$this->postPlatform->update(['meta' => []]); // No board_id in post meta
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,276 @@
|
|||
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.privacy_level']);
|
||||
});
|
||||
|
||||
test('publishing a pinterest post without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('publishing a pinterest post with board_id passes board validation', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => ['board_id' => '123456789'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.board_id']);
|
||||
});
|
||||
|
||||
test('scheduling a pinterest post without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Scheduled->value,
|
||||
'scheduled_at' => now()->addDay()->toIso8601String(),
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('publishing a pinterest carousel without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterestCarousel()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'img-1',
|
||||
'path' => 'media/2026-01/img1.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/img1.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'img1.jpg',
|
||||
],
|
||||
[
|
||||
'id' => 'img-2',
|
||||
'path' => 'media/2026-01/img2.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/img2.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'img2.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestCarousel->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('publishing a pinterest video pin without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterestVideoPin()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $this->mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestVideoPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('pinterest board error does not block other platforms in multi-platform publish', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$linkedinAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
]);
|
||||
$linkedinPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $linkedinAccount->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
'content_type' => ContentType::LinkedInPost,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
[
|
||||
'id' => $linkedinPlatform->id,
|
||||
'content_type' => ContentType::LinkedInPost->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
$response->assertSessionDoesntHaveErrors(['platforms.1.meta.board_id']);
|
||||
});
|
||||
|
||||
test('saving a pinterest post as draft without board_id skips the board rule', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Draft->value,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.board_id']);
|
||||
});
|
||||
|
||||
test('saving a tiktok post as draft without privacy_level skips the privacy_level rule', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
|
|
|
|||
Loading…
Reference in a new issue