fix(posts): preserve gallery media metadata and polish LinkedIn PDF settings

Selecting media in the gallery dropped original_filename, size, and meta when building the picked item, so they never reached the post. Since the gallery is the only way to add media, this left PDF documents without a filename (empty title field, 'PDF' thumbnail label, 'Document' fallback on publish) and skipped size/duration validation for all gallery media. Carry those fields through (guarded for unsplash/giphy items that lack them).

LinkedIn settings module: render only when a PDF is attached (its only relevant config now that format is inferred from media), pre-fill the document title from the PDF filename (editable), drop the redundant media warning (already surfaced on the channel) and the unused contentType prop/binding.

LinkedInSelect: add Person/Organization tag icons. i18n: clearer document-not-alone message in all locales.
This commit is contained in:
Paulo Castellano 2026-06-25 09:45:15 -03:00
parent 6184807aba
commit 428b8a5b27
7 changed files with 32 additions and 29 deletions

View file

@ -187,7 +187,7 @@
'no_image_allowed' => 'This post type accepts only videos.',
'no_document_allowed' => 'This post type does not accept PDF documents.',
'no_mixed_media' => "Images and a video can't be combined in the same post.",
'document_not_alone' => 'A PDF must be the only attachment.',
'document_not_alone' => 'A PDF must be posted on its own, without other images or videos.',
'gif_not_allowed' => 'This platform does not accept GIF. Remove the GIF or choose a different network.',
'image_too_large' => 'Image exceeds the :max limit for this post type (yours is :current).',
'video_too_large' => 'Video exceeds the :max limit for this post type (yours is :current).',
@ -346,7 +346,7 @@
'no_mixed_media' => "Images and videos can't be combined in the same post.",
'no_gifs' => 'GIFs are not supported here.',
'no_documents' => 'PDF documents are not supported by this format.',
'document_not_alone' => 'A PDF must be the only attachment.',
'document_not_alone' => 'A PDF must be posted on its own, without other images or videos.',
'video_too_large' => 'Video exceeds the size limit for this platform.',
'video_too_long' => 'Video must be under :seconds seconds for this format.',
'image_too_large' => 'Image exceeds the size limit for this platform.',

View file

@ -187,7 +187,7 @@
'no_image_allowed' => 'Este tipo acepta solo videos.',
'no_document_allowed' => 'Este tipo no acepta documentos PDF.',
'no_mixed_media' => 'No se pueden combinar imágenes y un video en la misma publicación.',
'document_not_alone' => 'Un PDF debe ser el único archivo adjunto.',
'document_not_alone' => 'Un PDF debe publicarse solo, sin otras imágenes ni videos.',
'gif_not_allowed' => 'Esta red no acepta GIF. Elimínalo o selecciona otra red.',
'image_too_large' => 'La imagen supera el límite de :max (la tuya es :current).',
'video_too_large' => 'El video supera el límite de :max (el tuyo es :current).',
@ -346,7 +346,7 @@
'no_mixed_media' => 'No se pueden combinar imágenes y videos en la misma publicación.',
'no_gifs' => 'Los GIFs no son compatibles aquí.',
'no_documents' => 'Los documentos PDF no son compatibles con este formato.',
'document_not_alone' => 'Un PDF debe ser el único archivo adjunto.',
'document_not_alone' => 'Un PDF debe publicarse solo, sin otras imágenes ni videos.',
'video_too_large' => 'El video supera el límite de tamaño de esta plataforma.',
'video_too_long' => 'El video debe durar menos de :seconds segundos en este formato.',
'image_too_large' => 'La imagen supera el límite de tamaño de esta plataforma.',

View file

@ -187,7 +187,7 @@
'no_image_allowed' => 'Este tipo aceita apenas vídeos.',
'no_document_allowed' => 'Este tipo não aceita documentos PDF.',
'no_mixed_media' => 'Não dá pra combinar imagens e um vídeo no mesmo post.',
'document_not_alone' => 'Um PDF deve ser o único anexo.',
'document_not_alone' => 'Um PDF precisa ser publicado sozinho, sem outras imagens ou vídeos.',
'gif_not_allowed' => 'Esta rede não aceita GIF. Remova o GIF ou escolha outra rede.',
'image_too_large' => 'A imagem passa do limite de :max (a sua tem :current).',
'video_too_large' => 'O vídeo passa do limite de :max (o seu tem :current).',
@ -346,7 +346,7 @@
'no_mixed_media' => 'Não é possível combinar imagem e vídeo na mesma publicação.',
'no_gifs' => 'GIFs não são suportados aqui.',
'no_documents' => 'Documentos PDF não são suportados por este formato.',
'document_not_alone' => 'Um PDF deve ser o único anexo.',
'document_not_alone' => 'Um PDF precisa ser publicado sozinho, sem outras imagens ou vídeos.',
'video_too_large' => 'Vídeo excede o limite de tamanho desta plataforma.',
'video_too_long' => 'Vídeo deve ter menos de :seconds segundos neste formato.',
'image_too_large' => 'Imagem excede o limite de tamanho desta plataforma.',

View file

@ -163,7 +163,6 @@ const selectedChannels = computed(() => props.channels.filter((channel) => isSel
v-else-if="channel.platform === Platform.LinkedIn || channel.platform === Platform.LinkedInPage"
:social-account="channel.socialAccount"
:platform="channel.platform"
:content-type="channel.contentType"
:media="media"
:meta="channel.meta"
:disabled="disabled"

View file

@ -140,6 +140,9 @@ const toggleSelect = (asset: AssetMedia | SavedMedia, extra?: Partial<PickedMedi
url: asset.url,
type: asset.type,
mime_type: asset.mime_type,
original_filename: 'original_filename' in asset ? asset.original_filename : undefined,
size: 'size' in asset ? asset.size : undefined,
meta: 'meta' in asset ? (asset.meta ?? undefined) : undefined,
...extra,
},
];

View file

@ -1,10 +1,10 @@
<script setup lang="ts">
import { IconAlertTriangle, IconChevronDown, IconChevronUp } from '@tabler/icons-vue';
import { IconChevronDown, IconChevronUp } from '@tabler/icons-vue';
import { computed, ref } from 'vue';
import { Avatar } from '@/components/ui/avatar';
import { Input } from '@/components/ui/input';
import { getMediaValidationWarning, isDocumentMedia } from '@/composables/useMedia';
import { isDocumentMedia } from '@/composables/useMedia';
import { getPlatformLogo } from '@/composables/usePlatformLogo';
import type { MediaItem } from '@/types/media';
import { Platform } from '@/types/platform';
@ -20,7 +20,6 @@ interface SocialAccount {
interface Props {
socialAccount: SocialAccount | null;
platform: string;
contentType: string;
media: MediaItem[];
meta?: Record<string, any>;
disabled?: boolean;
@ -41,20 +40,23 @@ const open = ref(false);
const isPage = computed(() => props.platform === Platform.LinkedInPage);
// The publish format follows the media; the only thing to configure is the
// title shown on a document (PDF) post, so we only expose it when a PDF is set.
const hasPdf = computed(() => props.media.some((item) => isDocumentMedia(item)));
const pdfDocument = computed(
() => props.media.find((item) => isDocumentMedia(item)) ?? null,
);
const hasPdf = computed(() => pdfDocument.value !== null);
const documentTitle = computed({
get: () => (props.meta?.document_title as string | undefined) ?? '',
get: () =>
(props.meta?.document_title as string | undefined) ||
pdfDocument.value?.original_filename ||
'',
set: (value: string) => emit('update:meta', { ...props.meta, document_title: value || null }),
});
const warning = computed(() => getMediaValidationWarning(props.contentType, props.media));
</script>
<template>
<div class="rounded-xl border-2 border-foreground bg-card shadow-2xs">
<div v-if="hasPdf" class="rounded-xl border-2 border-foreground bg-card shadow-2xs">
<button
type="button"
class="flex w-full cursor-pointer items-center justify-between gap-3 p-4 text-sm"
@ -87,7 +89,7 @@ const warning = computed(() => getMediaValidationWarning(props.contentType, prop
</div>
</div>
<div v-if="hasPdf" class="space-y-2">
<div class="space-y-2">
<p class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.form.linkedin.document_title') }}</p>
<Input
v-model="documentTitle"
@ -96,14 +98,6 @@ const warning = computed(() => getMediaValidationWarning(props.contentType, prop
:disabled="disabled || previewOnly"
/>
</div>
<p
v-if="warning && !previewOnly"
class="flex items-start gap-2 rounded-lg border-2 border-foreground bg-rose-50 p-2 text-xs font-semibold text-rose-700"
>
<IconAlertTriangle class="mt-0.5 size-3.5 shrink-0" />
{{ $t(`posts.form.warnings.${warning.key}`, warning.params) }}
</p>
</div>
</div>
</template>

View file

@ -1,5 +1,10 @@
<script setup lang="ts">
import { IconBuilding, IconExternalLink, IconUser } from '@tabler/icons-vue';
import {
IconBuilding,
IconExternalLink,
IconUser,
IconUsers,
} from '@tabler/icons-vue';
import { computed, ref } from 'vue';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
@ -115,8 +120,9 @@ const organizationUrl = (vanity: string | null): string | null =>
<div class="flex items-center gap-2">
<h3 class="truncate font-semibold">{{ person.name }}</h3>
<span
class="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground"
class="inline-flex shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground"
>
<IconUser class="h-3 w-3" />
{{ $t('accounts.linkedin.person_tag') }}
</span>
</div>
@ -156,8 +162,9 @@ const organizationUrl = (vanity: string | null): string | null =>
<div class="flex items-center gap-2">
<h3 class="truncate font-semibold">{{ org.name }}</h3>
<span
class="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground"
class="inline-flex shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground"
>
<IconUsers class="h-3 w-3" />
{{ $t('accounts.linkedin.organization_tag') }}
</span>
</div>