Surface Pinterest board truncation in the web editors.

Share {boards, truncated} via ListPinterestBoards into Inertia and warn in the board picker when pagination stopped early, matching API/MCP.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Paulo Castellano 2026-07-24 23:02:40 -03:00
parent d8e43bcfbb
commit b9194b9c7d
26 changed files with 68 additions and 31 deletions

View file

@ -4,10 +4,10 @@
namespace App\Actions\Automation\Automation;
use App\Actions\SocialAccount\ListPinterestBoards;
use App\Enums\SocialAccount\Platform;
use App\Models\Automation;
use App\Models\SocialAccount;
use App\Services\Social\PinterestPublisher;
use App\Services\Social\TikTokCreatorInfo;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
@ -15,14 +15,13 @@
class GetAutomationEditorData
{
public function __construct(
private PinterestPublisher $pinterestPublisher,
private TikTokCreatorInfo $tikTokCreatorInfo,
) {}
/**
* @return array{
* socialAccounts: Collection<int, SocialAccount>,
* pinterestBoards: SupportCollection<string, array<int, mixed>>,
* pinterestBoards: SupportCollection<string, array{boards: list<array{id: string, name: string}>, truncated: bool}>,
* tiktokCreatorInfos: SupportCollection<string, mixed>,
* }
*/
@ -34,12 +33,8 @@ public function __invoke(Automation $automation): array
->where('platform', Platform::Pinterest)
->mapWithKeys(fn ($account) => [
$account->id => rescue(
fn () => data_get(
$this->pinterestPublisher->getBoards($account),
'boards',
[],
),
[],
fn () => ListPinterestBoards::execute($account),
['boards' => [], 'truncated' => false],
report: false,
),
]);

View file

@ -9,6 +9,7 @@
use App\Actions\Post\DuplicatePost;
use App\Actions\Post\SyncPostPlatforms;
use App\Actions\Post\UpdatePost;
use App\Actions\SocialAccount\ListPinterestBoards;
use App\Ai\Templates\AiContentTemplate;
use App\Ai\Templates\AiTemplateRegistry;
use App\Enums\Post\Action as PostAction;
@ -23,7 +24,6 @@
use App\Models\Post;
use App\Models\PostPlatform;
use App\Services\Post\PostMetricsFetcher;
use App\Services\Social\PinterestPublisher;
use App\Services\Social\TikTokCreatorInfo;
use App\Support\PostStatusRules;
use Carbon\Carbon;
@ -260,12 +260,8 @@ public function edit(Request $request, Post $post): Response|RedirectResponse
->where('platform', Platform::Pinterest)
->mapWithKeys(fn ($account) => [
$account->id => rescue(
fn () => data_get(
app(PinterestPublisher::class)->getBoards($account),
'boards',
[],
),
[],
fn () => ListPinterestBoards::execute($account),
['boards' => [], 'truncated' => false],
report: false,
),
]);

View file

@ -154,6 +154,7 @@
'board' => 'اللوحة',
'select_board' => 'اختر لوحة',
'no_boards' => 'لم يتم العثور على لوحات Pinterest. أنشئ واحدة في حسابك على Pinterest أولًا.',
'boards_truncated' => 'تعذر تحميل بعض اللوحات. إذا كانت لوحتك مفقودة، افتح Pinterest وحاول مرة أخرى.',
'search_board' => 'البحث في اللوحات...',
'no_board_found' => 'لا توجد لوحة مطابقة لبحثك.',
'board_required' => 'اختر لوحة Pinterest لنشر هذا المنشور.',

View file

@ -156,6 +156,7 @@
'board' => 'Pinnwand',
'select_board' => 'Pinnwand auswählen',
'no_boards' => 'Keine Pinterest-Pinnwände gefunden. Erstelle zuerst eine in deinem Pinterest-Konto.',
'boards_truncated' => 'Einige Pinnwände konnten nicht geladen werden. Fehlt deine, öffne Pinterest und versuche es erneut.',
'search_board' => 'Pinnwände suchen...',
'no_board_found' => 'Keine Pinnwand passt zu deiner Suche.',
'board_required' => 'Wähle eine Pinterest-Pinnwand, um diesen Beitrag zu veröffentlichen.',

View file

@ -154,6 +154,7 @@
'board' => 'Πίνακας',
'select_board' => 'Επιλέξτε έναν πίνακα',
'no_boards' => 'Δεν βρέθηκαν πίνακες Pinterest. Δημιουργήστε πρώτα έναν στον λογαριασμό σας Pinterest.',
'boards_truncated' => 'Ορισμένοι πίνακες δεν φορτώθηκαν. Αν λείπει ο δικός σας, ανοίξτε το Pinterest και δοκιμάστε ξανά.',
'search_board' => 'Αναζήτηση πινάκων...',
'no_board_found' => 'Κανένας πίνακας δεν ταιριάζει με την αναζήτησή σας.',
'board_required' => 'Επιλέξτε έναν πίνακα Pinterest για να δημοσιεύσετε αυτή τη δημοσίευση.',

View file

@ -154,6 +154,7 @@
'board' => 'Board',
'select_board' => 'Select a board',
'no_boards' => 'No Pinterest boards found. Create one in your Pinterest account first.',
'boards_truncated' => 'Some boards could not be loaded. If yours is missing, open Pinterest and try again.',
'search_board' => 'Search boards...',
'no_board_found' => 'No board matches your search.',
'board_required' => 'Select a Pinterest board to publish this post.',

View file

@ -154,6 +154,7 @@
'board' => 'Tablero',
'select_board' => 'Selecciona un tablero',
'no_boards' => 'No se encontraron tableros de Pinterest. Crea uno en tu cuenta de Pinterest primero.',
'boards_truncated' => 'No se pudieron cargar algunos tableros. Si falta el tuyo, abre Pinterest e inténtalo de nuevo.',
'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.',

View file

@ -154,6 +154,7 @@
'board' => 'Tableau',
'select_board' => 'Sélectionner un tableau',
'no_boards' => 'Aucun tableau Pinterest trouvé. Créez-en un dans votre compte Pinterest d\'abord.',
'boards_truncated' => 'Certains tableaux nont pas pu être chargés. Sil manque le vôtre, ouvrez Pinterest et réessayez.',
'search_board' => 'Rechercher des tableaux...',
'no_board_found' => 'Aucun tableau ne correspond à votre recherche.',
'board_required' => 'Sélectionnez un tableau Pinterest pour publier cette publication.',

View file

@ -154,6 +154,7 @@
'board' => 'Bacheca',
'select_board' => 'Seleziona una bacheca',
'no_boards' => 'Nessuna bacheca Pinterest trovata. Creane una nel tuo account Pinterest prima.',
'boards_truncated' => 'Alcune bacheche non sono state caricate. Se manca la tua, apri Pinterest e riprova.',
'search_board' => 'Cerca bacheche...',
'no_board_found' => 'Nessuna bacheca corrisponde alla ricerca.',
'board_required' => 'Seleziona una bacheca Pinterest per pubblicare questo post.',

View file

@ -154,6 +154,7 @@
'board' => 'ボード',
'select_board' => 'ボードを選択',
'no_boards' => 'Pinterest ボードが見つかりません。先に Pinterest アカウントで作成してください。',
'boards_truncated' => '一部のボードを読み込めませんでした。見つからない場合は Pinterest を開いて再試行してください。',
'search_board' => 'ボードを検索...',
'no_board_found' => '検索に一致するボードがありません。',
'board_required' => 'この投稿を公開する Pinterest ボードを選択してください。',

View file

@ -154,6 +154,7 @@
'board' => '보드',
'select_board' => '보드 선택',
'no_boards' => 'Pinterest 보드를 찾을 수 없습니다. 먼저 Pinterest 계정에서 보드를 만드세요.',
'boards_truncated' => '일부 보드를 불러오지 못했습니다. 내 보드가 없다면 Pinterest를 연 뒤 다시 시도하세요.',
'search_board' => '보드 검색...',
'no_board_found' => '검색과 일치하는 보드가 없습니다.',
'board_required' => '이 게시물을 게시할 Pinterest 보드를 선택하세요.',

View file

@ -154,6 +154,7 @@
'board' => 'Bord',
'select_board' => 'Selecteer een bord',
'no_boards' => 'Geen Pinterest-borden gevonden. Maak er eerst een aan in je Pinterest-account.',
'boards_truncated' => 'Sommige borden konden niet worden geladen. Ontbreekt die van jou, open Pinterest en probeer opnieuw.',
'search_board' => 'Borden zoeken...',
'no_board_found' => 'Geen bord komt overeen met je zoekopdracht.',
'board_required' => 'Selecteer een Pinterest-bord om deze post te publiceren.',

View file

@ -154,6 +154,7 @@
'board' => 'Tablica',
'select_board' => 'Wybierz tablicę',
'no_boards' => 'Nie znaleziono tablic Pinterest. Najpierw utwórz jedną na swoim koncie Pinterest.',
'boards_truncated' => 'Nie udało się wczytać niektórych tablic. Jeśli brakuje twojej, otwórz Pinterest i spróbuj ponownie.',
'search_board' => 'Szukaj tablic...',
'no_board_found' => 'Brak tablic pasujących do wyszukiwania.',
'board_required' => 'Wybierz tablicę Pinterest, aby opublikować ten post.',

View file

@ -154,6 +154,7 @@
'board' => 'Quadro',
'select_board' => 'Selecione um quadro',
'no_boards' => 'Nenhum quadro do Pinterest encontrado. Crie um na sua conta do Pinterest primeiro.',
'boards_truncated' => 'Alguns boards não puderam ser carregados. Se o seu não aparecer, abra o Pinterest e tente de novo.',
'search_board' => 'Pesquisar quadros...',
'no_board_found' => 'Nenhum quadro encontrado.',
'board_required' => 'Selecione um quadro do Pinterest para publicar este post.',

View file

@ -154,6 +154,7 @@
'board' => 'Доска',
'select_board' => 'Выберите доску',
'no_boards' => 'Доски Pinterest не найдены. Сначала создайте доску в своём аккаунте Pinterest.',
'boards_truncated' => 'Некоторые доски не удалось загрузить. Если вашей нет, откройте Pinterest и попробуйте снова.',
'search_board' => 'Поиск досок...',
'no_board_found' => 'Нет досок по вашему запросу.',
'board_required' => 'Выберите доску Pinterest, чтобы опубликовать этот пост.',

View file

@ -156,6 +156,7 @@
'board' => 'Pano',
'select_board' => 'Bir pano seçin',
'no_boards' => 'Pinterest panosu bulunamadı. Önce Pinterest hesabınızda bir tane oluşturun.',
'boards_truncated' => 'Bazı panolar yüklenemedi. Sizinki eksikse Pinteresti açıp tekrar deneyin.',
'search_board' => 'Pano ara...',
'no_board_found' => 'Aramanızla eşleşen pano yok.',
'board_required' => 'Bu gönderiyi yayınlamak için bir Pinterest panosu seçin.',

View file

@ -154,6 +154,7 @@
'board' => '图板',
'select_board' => '选择一个图板',
'no_boards' => '未找到 Pinterest 图板。请先在你的 Pinterest 账号中创建一个。',
'boards_truncated' => '部分图板未能加载。如果没有看到你的图板,请打开 Pinterest 后重试。',
'search_board' => '搜索图板…',
'no_board_found' => '没有与搜索匹配的图板。',
'board_required' => '请选择一个 Pinterest 图板以发布此帖子。',

View file

@ -153,6 +153,7 @@ const selectedChannels = computed(() => props.channels.filter((channel) => isSel
:content-type="channel.contentType"
:media="media"
:boards="channel.boards ?? []"
:boards-truncated="channel.boardsTruncated ?? false"
:meta="channel.meta"
:disabled="disabled"
:preview-only="previewOnly"

View file

@ -13,7 +13,7 @@ import { Switch } from '@/components/ui/switch';
import { useExpandedEditor } from '@/composables/useExpandedEditor';
import { getMediaRulesForContentType } from '@/composables/useMediaRules';
import { getMediaIncompatibilityReason, getPlatformMetaIssue } from '@/composables/usePostCompliance';
import type { PinterestBoard } from '@/types';
import type { PinterestBoard, PinterestBoardsPayload } from '@/types';
import type { Channel } from '@/types/channel';
import { ContentType } from '@/types/content-type';
import type { MediaItem } from '@/types/media';
@ -91,8 +91,8 @@ const platformConfigs = computed<Record<string, any>>(() => {
return raw ?? {};
});
const pinterestBoards = computed<Record<string, PinterestBoard[]>>(() => {
const raw = page.props.pinterestBoards as Record<string, PinterestBoard[]> | undefined;
const pinterestBoards = computed<Record<string, PinterestBoardsPayload>>(() => {
const raw = page.props.pinterestBoards as Record<string, PinterestBoardsPayload> | undefined;
return raw ?? {};
});
@ -209,7 +209,10 @@ const getCreatorInfo = (account: SocialAccount): TikTokCreatorInfo | null =>
tiktokCreatorInfos.value[account.id] ?? null;
const getBoards = (account: SocialAccount): PinterestBoard[] =>
pinterestBoards.value[account.id] ?? [];
pinterestBoards.value[account.id]?.boards ?? [];
const boardsTruncated = (account: SocialAccount): boolean =>
pinterestBoards.value[account.id]?.truncated ?? false;
// Image-capability is derived from the SAME media rules the post editor uses
// (per content type), never a hardcoded list facebook_post, tiktok_photo,
@ -279,6 +282,7 @@ const channels = computed<Channel[]>(() =>
publishConfig: getPublishConfig(account),
creatorInfo: getCreatorInfo(account),
boards: getBoards(account),
boardsTruncated: boardsTruncated(account),
};
}),
);

View file

@ -39,6 +39,7 @@ interface Props {
contentType: string;
media: MediaItem[];
boards: PinterestBoard[];
boardsTruncated?: boolean;
meta: Record<string, any>;
disabled?: boolean;
previewOnly?: boolean;
@ -47,6 +48,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
disabled: false,
previewOnly: false,
boardsTruncated: false,
});
const emit = defineEmits<{
@ -187,6 +189,13 @@ const boardError = computed<string | undefined>(() => {
</ComboboxList>
</Combobox>
<InputError :message="boardError" />
<p
v-if="boardsTruncated"
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.boards_truncated') }}
</p>
</template>
</div>

View file

@ -5,7 +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';
import type { PinterestBoardsPayload } from '@/types';
import type { MediaItem } from '@/types/media';
interface SocialAccount {
@ -62,7 +62,7 @@ const props = defineProps<{
labels: { id: string; name: string; color: string }[];
selectedLabelIds: string[];
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
pinterestBoards?: Record<string, PinterestBoard[]> | null;
pinterestBoards?: Record<string, PinterestBoardsPayload> | null;
isReadOnly: boolean;
authUserId: string;
initialHighlightCommentId: string | null;

View file

@ -8,7 +8,7 @@ import { Badge } from '@/components/ui/badge';
import { usePageErrors } from '@/composables/usePageErrors';
import { getPlatformLogo } from '@/composables/usePlatformLogo';
import { isVideo } from '@/lib/mediaType';
import type { PinterestBoard } from '@/types';
import type { PinterestBoard, PinterestBoardsPayload } from '@/types';
import type { Channel } from '@/types/channel';
import type { MediaItem } from '@/types/media';
import { PostPlatformStatus } from '@/types/post';
@ -77,7 +77,7 @@ const props = defineProps<{
platformContentTypes: Record<string, string>;
platformIssues?: Record<string, string>;
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
pinterestBoards?: Record<string, PinterestBoard[]> | null;
pinterestBoards?: Record<string, PinterestBoardsPayload> | null;
media?: MediaItem[];
}>();
@ -94,8 +94,14 @@ 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 boardsPayload = (pp: PostPlatform): PinterestBoardsPayload =>
pp.social_account_id
? props.pinterestBoards?.[pp.social_account_id] ?? { boards: [], truncated: false }
: { boards: [], truncated: false };
const getBoards = (pp: PostPlatform): PinterestBoard[] => boardsPayload(pp).boards;
const boardsTruncated = (pp: PostPlatform): boolean => boardsPayload(pp).truncated;
const videoDurationSec = computed(() => {
const video = props.media?.find((m) => isVideo(m));
@ -143,6 +149,7 @@ const channels = computed<Channel[]>(() =>
publishConfig: getPublishConfig(pp),
creatorInfo: getCreatorInfo(pp),
boards: getBoards(pp),
boardsTruncated: boardsTruncated(pp),
})),
);
</script>

View file

@ -25,7 +25,7 @@ import dayjs from '@/dayjs';
import debounce from '@/debounce';
import AppLayout from '@/layouts/AppLayout.vue';
import { destroy as destroyPost, update as updatePost } from '@/routes/app/posts';
import type { PinterestBoard } from '@/types';
import type { PinterestBoardsPayload } from '@/types';
import type { MediaItem } from '@/types/media';
import { PostStatus } from '@/types/post';
@ -86,7 +86,7 @@ const props = defineProps<{
post: Post;
socialAccounts: SocialAccount[];
platformConfigs: Record<string, any>;
pinterestBoards: Record<string, PinterestBoard[]>;
pinterestBoards: Record<string, PinterestBoardsPayload>;
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
labels: { id: string; name: string; color: string }[];
signatures: { id: string; name: string; content: string }[];

View file

@ -40,4 +40,5 @@ export interface Channel {
publishConfig?: Record<string, any> | null;
creatorInfo?: ChannelTikTokCreatorInfo | null;
boards?: PinterestBoard[];
boardsTruncated?: boolean;
}

View file

@ -116,6 +116,12 @@ export interface PinterestBoard {
name: string;
}
/** Per-account payload from ListPinterestBoards (Inertia + API/MCP). */
export interface PinterestBoardsPayload {
boards: PinterestBoard[];
truncated: boolean;
}
export interface ContentLanguageOption {
value: string;
label: string;

View file

@ -136,8 +136,8 @@
it('maps pinterest boards for pinterest accounts', function () {
$this->mock(PinterestPublisher::class, fn ($mock) => $mock->shouldReceive('getBoards')->andReturn([
'boards' => [['id' => 'b1']],
'truncated' => false,
'boards' => [['id' => 'b1', 'name' => 'Ideas']],
'truncated' => true,
]));
$this->mock(TikTokCreatorInfo::class);
@ -147,5 +147,8 @@
$result = app(GetAutomationEditorData::class)($automation);
expect($result['pinterestBoards']->get($pinterest->id))->toBe([['id' => 'b1']]);
expect($result['pinterestBoards']->get($pinterest->id))->toBe([
'boards' => [['id' => 'b1', 'name' => 'Ideas']],
'truncated' => true,
]);
});