refactor(pinterest): reuse InputError, extract PinterestBoard type
- PinterestSettings: replace inline error markup with the shared
InputError component (matches the rest of the form patterns)
- Extract PinterestBoard type to @/types and use it across
PinterestSettings, ScheduleTab, PostEditorSidebar, Edit — drops
the duplicated inline {id, name} shape from 4 call sites
- Block publish/schedule when Pinterest is selected but no board is
picked (mirrors tiktokComplianceValid). Tooltip surfaces the same
i18n key the backend uses, so frontend + backend speak the same
language
This commit is contained in:
parent
44d891ef08
commit
1f5a16ef2c
5 changed files with 77 additions and 39 deletions
|
|
@ -1,7 +1,9 @@
|
|||
<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,
|
||||
|
|
@ -16,6 +18,7 @@ import {
|
|||
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;
|
||||
|
|
@ -34,7 +37,7 @@ interface Props {
|
|||
socialAccount: SocialAccount | null;
|
||||
contentType: string;
|
||||
media: MediaItem[];
|
||||
boards: Array<{ id: string; name: string }>;
|
||||
boards: PinterestBoard[];
|
||||
meta: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
@ -71,6 +74,17 @@ 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>
|
||||
|
|
@ -135,40 +149,43 @@ const selectedBoard = computed<BoardOption | undefined>({
|
|||
<IconAlertTriangle class="mt-0.5 size-3.5 shrink-0" />
|
||||
{{ $t('posts.form.pinterest.no_boards') }}
|
||||
</p>
|
||||
<Combobox
|
||||
v-else
|
||||
v-model="selectedBoard"
|
||||
:display-value="(b: any) => 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 border-foreground/30 bg-card px-3 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
: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>
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -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,7 +72,7 @@ const props = defineProps<{
|
|||
labels: { id: string; name: string; color: string }[];
|
||||
selectedLabelIds: string[];
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
pinterestBoards?: Record<string, Array<{ id: string; name: string }>> | null;
|
||||
pinterestBoards?: Record<string, PinterestBoard[]> | null;
|
||||
isReadOnly: boolean;
|
||||
authUserId: string;
|
||||
initialHighlightCommentId: string | null;
|
||||
|
|
|
|||
|
|
@ -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,7 +87,7 @@ const props = defineProps<{
|
|||
platformContentTypes: Record<string, string>;
|
||||
platformIssues?: Record<string, string>;
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
pinterestBoards?: Record<string, Array<{ id: string; name: string }>> | null;
|
||||
pinterestBoards?: Record<string, PinterestBoard[]> | null;
|
||||
media?: MediaItem[];
|
||||
}>();
|
||||
|
||||
|
|
@ -135,7 +136,7 @@ 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): Array<{ id: string; name: string }> =>
|
||||
const getBoards = (pp: PostPlatform): PinterestBoard[] =>
|
||||
pp.social_account_id ? props.pinterestBoards?.[pp.social_account_id] ?? [] : [];
|
||||
|
||||
const videoDurationSec = computed(() => {
|
||||
|
|
|
|||
|
|
@ -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: Record<string, Array<{ id: string; name: string }>>;
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue