refactor(posts): extract Edit.vue validation logic into usePostCompliance
Pulls ~200 lines of media/platform/length compliance and tooltip derivation into composables/usePostCompliance.ts. Edit.vue is now focused on state, save logic and event handlers; PLATFORM_VARIANTS and the per-content-type media-rule check are exposed from the composable for togglePlatform.
This commit is contained in:
parent
cad89a0a97
commit
2584257ee6
2 changed files with 272 additions and 212 deletions
251
resources/js/composables/usePostCompliance.ts
Normal file
251
resources/js/composables/usePostCompliance.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { computed, type ComputedRef, type Ref } from 'vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
|
||||
import { getMediaItemIssue, type MediaItem } from '@/composables/useMedia';
|
||||
import { getMediaRulesForContentType } from '@/composables/useMediaRules';
|
||||
import { getPlatformLabel } from '@/composables/usePlatformLogo';
|
||||
import { ContentType } from '@/enums/content-type';
|
||||
import { Platform } from '@/enums/platform';
|
||||
|
||||
export interface CompliancePostPlatform {
|
||||
id: string;
|
||||
platform: string;
|
||||
platform_name: string | null;
|
||||
social_account_id: string | null;
|
||||
content_type: string | null;
|
||||
}
|
||||
|
||||
export interface CompliancePost {
|
||||
post_platforms: CompliancePostPlatform[];
|
||||
}
|
||||
|
||||
export const PLATFORM_VARIANTS: Record<string, string[]> = {
|
||||
[Platform.Facebook]: [ContentType.FacebookPost, ContentType.FacebookReel, ContentType.FacebookStory],
|
||||
[Platform.Instagram]: [ContentType.InstagramFeed, ContentType.InstagramReel, ContentType.InstagramStory],
|
||||
[Platform.InstagramFacebook]: [ContentType.InstagramFeed, ContentType.InstagramReel, ContentType.InstagramStory],
|
||||
[Platform.LinkedIn]: [ContentType.LinkedInPost, ContentType.LinkedInCarousel],
|
||||
[Platform.LinkedInPage]: [ContentType.LinkedInPagePost, ContentType.LinkedInPageCarousel],
|
||||
[Platform.TikTok]: [ContentType.TikTokVideo, ContentType.TikTokPhoto],
|
||||
[Platform.Pinterest]: [ContentType.PinterestPin, ContentType.PinterestVideoPin, ContentType.PinterestCarousel],
|
||||
};
|
||||
|
||||
export const getMediaIncompatibilityReason = (
|
||||
contentType: string,
|
||||
mediaItems: MediaItem[],
|
||||
): string | null => {
|
||||
const rules = getMediaRulesForContentType(contentType);
|
||||
const videos = mediaItems.filter((m) => m.type === 'video' || m.mime_type?.startsWith('video/'));
|
||||
const images = mediaItems.filter((m) => m.type === 'image' || m.mime_type?.startsWith('image/'));
|
||||
const gifs = mediaItems.filter((m) => m.mime_type === 'image/gif');
|
||||
const total = mediaItems.length;
|
||||
|
||||
if (rules.requiresMedia && total === 0) return trans('posts.edit.compliance.requires_media');
|
||||
if (!rules.acceptVideos && videos.length > 0) return trans('posts.edit.compliance.no_videos');
|
||||
if (!rules.acceptImages && images.length > 0) return trans('posts.edit.compliance.no_images');
|
||||
if (!rules.acceptsGif && gifs.length > 0) return trans('posts.edit.compliance.no_gifs');
|
||||
if (total > rules.maxFiles) return trans('posts.edit.compliance.too_many_files', { max: String(rules.maxFiles) });
|
||||
if (rules.minFiles && total < rules.minFiles) return trans('posts.edit.compliance.too_few_files', { min: String(rules.minFiles) });
|
||||
|
||||
for (const m of mediaItems) {
|
||||
const isVideo = m.type === 'video' || m.mime_type?.startsWith('video/');
|
||||
const size = m.size ?? 0;
|
||||
const duration = m.meta?.duration ?? 0;
|
||||
const width = m.meta?.width ?? 0;
|
||||
const height = m.meta?.height ?? 0;
|
||||
|
||||
if (isVideo) {
|
||||
if (rules.maxVideoBytes && size > 0 && size > rules.maxVideoBytes) return trans('posts.edit.compliance.video_too_large');
|
||||
if (rules.maxVideoDurationSec && duration > 0 && duration > rules.maxVideoDurationSec) {
|
||||
return trans('posts.edit.compliance.video_too_long', { seconds: String(rules.maxVideoDurationSec) });
|
||||
}
|
||||
} else if (rules.maxImageBytes && size > 0 && size > rules.maxImageBytes) {
|
||||
return trans('posts.edit.compliance.image_too_large');
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0 && (rules.aspectRatioMin || rules.aspectRatioMax)) {
|
||||
const ratio = width / height;
|
||||
if (rules.aspectRatioMin && ratio < rules.aspectRatioMin) return trans('posts.edit.compliance.aspect_ratio_invalid');
|
||||
if (rules.aspectRatioMax && ratio > rules.aspectRatioMax) return trans('posts.edit.compliance.aspect_ratio_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const firstCompatibleVariant = (
|
||||
platform: string,
|
||||
mediaItems: MediaItem[],
|
||||
): string | null => {
|
||||
const variants = PLATFORM_VARIANTS[platform];
|
||||
if (!variants) return null;
|
||||
return variants.find((ct) => !getMediaIncompatibilityReason(ct, mediaItems)) ?? null;
|
||||
};
|
||||
|
||||
interface UsePostComplianceOptions {
|
||||
post: ComputedRef<CompliancePost>;
|
||||
content: Ref<string>;
|
||||
media: Ref<MediaItem[]>;
|
||||
selectedPlatformIds: Ref<string[]>;
|
||||
platformContentTypes: Ref<Record<string, string>>;
|
||||
platformMeta: Ref<Record<string, Record<string, any>>>;
|
||||
platformConfigs: Record<string, { maxContentLength?: number | null }>;
|
||||
}
|
||||
|
||||
export const usePostCompliance = (opts: UsePostComplianceOptions) => {
|
||||
const { post, content, media, selectedPlatformIds, platformContentTypes, platformMeta, platformConfigs } = opts;
|
||||
|
||||
const platformLimits = computed(() => {
|
||||
const seen = new Set<string>();
|
||||
const result: { platform: string; maxLength: number }[] = [];
|
||||
for (const pp of post.value.post_platforms) {
|
||||
if (!selectedPlatformIds.value.includes(pp.id)) continue;
|
||||
if (seen.has(pp.platform)) continue;
|
||||
const accountId = pp.social_account_id;
|
||||
const max = accountId ? platformConfigs[accountId]?.maxContentLength : null;
|
||||
if (typeof max === 'number' && max > 0) {
|
||||
seen.add(pp.platform);
|
||||
result.push({ platform: pp.platform, maxLength: max });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const mediaIssues = computed<Record<string, { platform: string; reason: string }[]>>(() => {
|
||||
const result: Record<string, { platform: string; reason: string }[]> = {};
|
||||
for (const item of media.value) {
|
||||
const issues: { platform: string; reason: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const pp of post.value.post_platforms) {
|
||||
if (!selectedPlatformIds.value.includes(pp.id)) continue;
|
||||
if (seen.has(pp.platform)) continue;
|
||||
const contentType = platformContentTypes.value[pp.id] ?? pp.content_type ?? '';
|
||||
const reason = getMediaItemIssue(item, contentType);
|
||||
if (reason) {
|
||||
seen.add(pp.platform);
|
||||
issues.push({ platform: pp.platform, reason });
|
||||
}
|
||||
}
|
||||
if (issues.length > 0) result[item.id] = issues;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const platformIssues = computed<Record<string, string>>(() => {
|
||||
const issues: Record<string, string> = {};
|
||||
|
||||
for (const pp of post.value.post_platforms) {
|
||||
const contentType = platformContentTypes.value[pp.id];
|
||||
if (!contentType) {
|
||||
issues[pp.id] = trans('posts.edit.compliance.no_content_type');
|
||||
continue;
|
||||
}
|
||||
|
||||
const reason = getMediaIncompatibilityReason(contentType, media.value);
|
||||
if (!reason) continue;
|
||||
|
||||
const isSelected = selectedPlatformIds.value.includes(pp.id);
|
||||
if (!isSelected && firstCompatibleVariant(pp.platform, media.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
issues[pp.id] = reason;
|
||||
}
|
||||
|
||||
return issues;
|
||||
});
|
||||
|
||||
const mediaCompliancePerPlatformValid = computed(
|
||||
() => selectedPlatformIds.value.every((id) => !platformIssues.value[id]),
|
||||
);
|
||||
|
||||
// TikTok requires explicit privacy_level; when disclosure is on, at least
|
||||
// one brand sub-toggle (organic/content) must also be selected.
|
||||
const tiktokComplianceValid = computed(() => {
|
||||
const tiktokPlatforms = post.value.post_platforms.filter(
|
||||
(pp) => pp.platform === Platform.TikTok && selectedPlatformIds.value.includes(pp.id),
|
||||
);
|
||||
return tiktokPlatforms.every((pp) => {
|
||||
const meta = platformMeta.value[pp.id] ?? {};
|
||||
if (!meta.privacy_level) return false;
|
||||
if (meta.disclose && !meta.brand_organic_toggle && !meta.brand_content_toggle) return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
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 hasContentOrMedia = computed(
|
||||
() => content.value.trim().length > 0 || media.value.length > 0,
|
||||
);
|
||||
|
||||
const contentLengthOverflows = computed(() => {
|
||||
const len = content.value.length;
|
||||
return platformLimits.value
|
||||
.filter((p) => len > p.maxLength)
|
||||
.map((p) => ({ platform: p.platform, limit: p.maxLength, over: len - p.maxLength }));
|
||||
});
|
||||
|
||||
const canSchedule = computed(
|
||||
() => mediaCompliancePerPlatformValid.value
|
||||
&& tiktokComplianceValid.value
|
||||
&& pinterestComplianceValid.value
|
||||
&& hasContentOrMedia.value
|
||||
&& contentLengthOverflows.value.length === 0,
|
||||
);
|
||||
|
||||
const postActionTooltip = computed(() => {
|
||||
if (canSchedule.value) return '';
|
||||
|
||||
const mediaReasons = post.value.post_platforms
|
||||
.filter((pp) => selectedPlatformIds.value.includes(pp.id) && platformIssues.value[pp.id])
|
||||
.map((pp) => `${pp.platform_name ?? pp.platform}: ${platformIssues.value[pp.id]}`);
|
||||
|
||||
const lengthReasons = contentLengthOverflows.value.map((overflow) => trans('posts.form.content_exceeds_platform', {
|
||||
platform: getPlatformLabel(overflow.platform),
|
||||
limit: String(overflow.limit),
|
||||
over: String(overflow.over),
|
||||
}));
|
||||
|
||||
const reasons = [...mediaReasons, ...lengthReasons];
|
||||
if (reasons.length > 0) return reasons.join('\n');
|
||||
|
||||
const tiktokDisclosureIncomplete = post.value.post_platforms.some((pp) => {
|
||||
if (pp.platform !== Platform.TikTok) return false;
|
||||
if (!selectedPlatformIds.value.includes(pp.id)) return false;
|
||||
const meta = platformMeta.value[pp.id] ?? {};
|
||||
return Boolean(meta.disclose) && !meta.brand_organic_toggle && !meta.brand_content_toggle;
|
||||
});
|
||||
|
||||
if (tiktokDisclosureIncomplete) {
|
||||
return trans('posts.form.tiktok.compliance_incomplete');
|
||||
}
|
||||
|
||||
if (!pinterestComplianceValid.value) {
|
||||
return trans('posts.form.pinterest.board_required');
|
||||
}
|
||||
|
||||
if (!hasContentOrMedia.value) {
|
||||
return trans('posts.edit.compliance.requires_content_or_media');
|
||||
}
|
||||
|
||||
return trans('posts.edit.compliance_incomplete');
|
||||
});
|
||||
|
||||
return {
|
||||
platformLimits,
|
||||
mediaIssues,
|
||||
platformIssues,
|
||||
mediaCompliancePerPlatformValid,
|
||||
tiktokComplianceValid,
|
||||
pinterestComplianceValid,
|
||||
hasContentOrMedia,
|
||||
contentLengthOverflows,
|
||||
canSchedule,
|
||||
postActionTooltip,
|
||||
};
|
||||
};
|
||||
|
|
@ -11,17 +11,17 @@ import PostEditorComposer from '@/components/posts/editor/PostEditorComposer.vue
|
|||
import PostEditorHeader from '@/components/posts/editor/PostEditorHeader.vue';
|
||||
import PostEditorSidebar from '@/components/posts/editor/PostEditorSidebar.vue';
|
||||
import { usePostEcho } from '@/composables/echo/usePostEcho';
|
||||
import { getMediaItemIssue } from '@/composables/useMedia';
|
||||
import { getMediaRulesForContentType } from '@/composables/useMediaRules';
|
||||
import { getPlatformLabel } from '@/composables/usePlatformLogo';
|
||||
import {
|
||||
firstCompatibleVariant,
|
||||
getMediaIncompatibilityReason,
|
||||
usePostCompliance,
|
||||
} from '@/composables/usePostCompliance';
|
||||
import dayjs from '@/dayjs';
|
||||
import debounce from '@/debounce';
|
||||
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 type { PinterestBoard } from '@/types';
|
||||
import { PostStatus } from '@/types/post';
|
||||
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
|
|
@ -139,212 +139,21 @@ const updatePlatformContentType = (platformId: string, contentType: string) => {
|
|||
platformContentTypes.value = { ...platformContentTypes.value, [platformId]: contentType };
|
||||
};
|
||||
|
||||
const platformLimits = computed(() => {
|
||||
const seen = new Set<string>();
|
||||
const result: { platform: string; maxLength: number }[] = [];
|
||||
for (const pp of post.value.post_platforms) {
|
||||
if (! selectedPlatformIds.value.includes(pp.id)) continue;
|
||||
if (seen.has(pp.platform)) continue;
|
||||
const accountId = pp.social_account_id;
|
||||
const max = accountId ? props.platformConfigs[accountId]?.maxContentLength : null;
|
||||
if (typeof max === 'number' && max > 0) {
|
||||
seen.add(pp.platform);
|
||||
result.push({ platform: pp.platform, maxLength: max });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const mediaIssues = computed(() => {
|
||||
const result: Record<string, { platform: string; reason: string }[]> = {};
|
||||
for (const item of media.value) {
|
||||
const issues: { platform: string; reason: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const pp of post.value.post_platforms) {
|
||||
if (! selectedPlatformIds.value.includes(pp.id)) continue;
|
||||
if (seen.has(pp.platform)) continue;
|
||||
const contentType = platformContentTypes.value[pp.id] ?? pp.content_type ?? '';
|
||||
const reason = getMediaItemIssue(item, contentType);
|
||||
if (reason) {
|
||||
seen.add(pp.platform);
|
||||
issues.push({ platform: pp.platform, reason });
|
||||
}
|
||||
}
|
||||
if (issues.length > 0) result[item.id] = issues;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const getMediaIncompatibilityReason = (contentType: string, mediaItems: MediaItem[]): string | null => {
|
||||
const rules = getMediaRulesForContentType(contentType);
|
||||
const videos = mediaItems.filter((m) => m.type === 'video' || m.mime_type?.startsWith('video/'));
|
||||
const images = mediaItems.filter((m) => m.type === 'image' || m.mime_type?.startsWith('image/'));
|
||||
const gifs = mediaItems.filter((m) => m.mime_type === 'image/gif');
|
||||
const total = mediaItems.length;
|
||||
|
||||
// Order matters: type-mismatch errors (image vs video vs gif) are more
|
||||
// fundamental than count errors. Telling the user "YouTube doesn't accept
|
||||
// images" is more actionable than "too many files" when they're mixing types.
|
||||
if (rules.requiresMedia && total === 0) return trans('posts.edit.compliance.requires_media');
|
||||
if (!rules.acceptVideos && videos.length > 0) return trans('posts.edit.compliance.no_videos');
|
||||
if (!rules.acceptImages && images.length > 0) return trans('posts.edit.compliance.no_images');
|
||||
if (!rules.acceptsGif && gifs.length > 0) return trans('posts.edit.compliance.no_gifs');
|
||||
if (total > rules.maxFiles) return trans('posts.edit.compliance.too_many_files', { max: String(rules.maxFiles) });
|
||||
if (rules.minFiles && total < rules.minFiles) return trans('posts.edit.compliance.too_few_files', { min: String(rules.minFiles) });
|
||||
|
||||
for (const m of mediaItems) {
|
||||
const isVideo = m.type === 'video' || m.mime_type?.startsWith('video/');
|
||||
const size = m.size ?? 0;
|
||||
const duration = m.meta?.duration ?? 0;
|
||||
const width = m.meta?.width ?? 0;
|
||||
const height = m.meta?.height ?? 0;
|
||||
|
||||
if (isVideo) {
|
||||
if (rules.maxVideoBytes && size > 0 && size > rules.maxVideoBytes) return trans('posts.edit.compliance.video_too_large');
|
||||
if (rules.maxVideoDurationSec && duration > 0 && duration > rules.maxVideoDurationSec) {
|
||||
return trans('posts.edit.compliance.video_too_long', { seconds: String(rules.maxVideoDurationSec) });
|
||||
}
|
||||
} else if (rules.maxImageBytes && size > 0 && size > rules.maxImageBytes) {
|
||||
return trans('posts.edit.compliance.image_too_large');
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0 && (rules.aspectRatioMin || rules.aspectRatioMax)) {
|
||||
const ratio = width / height;
|
||||
if (rules.aspectRatioMin && ratio < rules.aspectRatioMin) return trans('posts.edit.compliance.aspect_ratio_invalid');
|
||||
if (rules.aspectRatioMax && ratio > rules.aspectRatioMax) return trans('posts.edit.compliance.aspect_ratio_invalid');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const PLATFORM_VARIANTS: Record<string, string[]> = {
|
||||
[Platform.Facebook]: [ContentType.FacebookPost, ContentType.FacebookReel, ContentType.FacebookStory],
|
||||
[Platform.Instagram]: [ContentType.InstagramFeed, ContentType.InstagramReel, ContentType.InstagramStory],
|
||||
[Platform.InstagramFacebook]: [ContentType.InstagramFeed, ContentType.InstagramReel, ContentType.InstagramStory],
|
||||
[Platform.LinkedIn]: [ContentType.LinkedInPost, ContentType.LinkedInCarousel],
|
||||
[Platform.LinkedInPage]: [ContentType.LinkedInPagePost, ContentType.LinkedInPageCarousel],
|
||||
[Platform.TikTok]: [ContentType.TikTokVideo, ContentType.TikTokPhoto],
|
||||
[Platform.Pinterest]: [ContentType.PinterestPin, ContentType.PinterestVideoPin, ContentType.PinterestCarousel],
|
||||
};
|
||||
|
||||
const firstCompatibleVariant = (platform: string, mediaItems: MediaItem[]): string | null => {
|
||||
const variants = PLATFORM_VARIANTS[platform];
|
||||
if (!variants) return null;
|
||||
return variants.find((ct) => !getMediaIncompatibilityReason(ct, mediaItems)) ?? null;
|
||||
};
|
||||
|
||||
const platformIssues = computed<Record<string, string>>(() => {
|
||||
const issues: Record<string, string> = {};
|
||||
|
||||
for (const pp of post.value.post_platforms) {
|
||||
const contentType = platformContentTypes.value[pp.id];
|
||||
if (!contentType) {
|
||||
issues[pp.id] = trans('posts.edit.compliance.no_content_type');
|
||||
continue;
|
||||
}
|
||||
|
||||
const reason = getMediaIncompatibilityReason(contentType, media.value);
|
||||
if (!reason) continue;
|
||||
|
||||
const isSelected = selectedPlatformIds.value.includes(pp.id);
|
||||
if (!isSelected && firstCompatibleVariant(pp.platform, media.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
issues[pp.id] = reason;
|
||||
}
|
||||
|
||||
return issues;
|
||||
});
|
||||
|
||||
const mediaCompliancePerPlatformValid = computed(
|
||||
() => selectedPlatformIds.value.every((id) => !platformIssues.value[id]),
|
||||
);
|
||||
|
||||
// TikTok compliance per docs:
|
||||
// - privacy_level must be explicitly selected
|
||||
// - if disclosure toggle is ON, at least one sub-toggle must be selected
|
||||
const tiktokComplianceValid = computed(() => {
|
||||
const tiktokPlatforms = post.value.post_platforms.filter(
|
||||
(pp) => pp.platform === Platform.TikTok && selectedPlatformIds.value.includes(pp.id),
|
||||
);
|
||||
return tiktokPlatforms.every((pp) => {
|
||||
const meta = platformMeta.value[pp.id] ?? {};
|
||||
if (!meta.privacy_level) return false;
|
||||
if (meta.disclose && !meta.brand_organic_toggle && !meta.brand_content_toggle) return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// 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 hasContentOrMedia = computed(
|
||||
() => content.value.trim().length > 0 || media.value.length > 0,
|
||||
);
|
||||
|
||||
const contentLengthOverflows = computed(() => {
|
||||
const len = content.value.length;
|
||||
return platformLimits.value
|
||||
.filter((p) => len > p.maxLength)
|
||||
.map((p) => ({ platform: p.platform, limit: p.maxLength, over: len - p.maxLength }));
|
||||
});
|
||||
|
||||
const canSchedule = computed(
|
||||
() => mediaCompliancePerPlatformValid.value
|
||||
&& tiktokComplianceValid.value
|
||||
&& pinterestComplianceValid.value
|
||||
&& hasContentOrMedia.value
|
||||
&& contentLengthOverflows.value.length === 0,
|
||||
);
|
||||
|
||||
const postActionTooltip = computed(() => {
|
||||
if (canSchedule.value) return '';
|
||||
|
||||
const mediaReasons = post.value.post_platforms
|
||||
.filter((pp) => selectedPlatformIds.value.includes(pp.id) && platformIssues.value[pp.id])
|
||||
.map((pp) => `${pp.platform_name ?? pp.platform}: ${platformIssues.value[pp.id]}`);
|
||||
|
||||
const lengthReasons = contentLengthOverflows.value.map((overflow) => trans('posts.form.content_exceeds_platform', {
|
||||
platform: getPlatformLabel(overflow.platform),
|
||||
limit: String(overflow.limit),
|
||||
over: String(overflow.over),
|
||||
}));
|
||||
|
||||
const reasons = [...mediaReasons, ...lengthReasons];
|
||||
|
||||
if (reasons.length > 0) return reasons.join('\n');
|
||||
|
||||
// No media issues — the only remaining blocker is TikTok compliance.
|
||||
// Surface the exact TikTok-required tooltip when the disclosure toggle
|
||||
// is on without a sub-selection (UX Guideline Point 3a). For other TikTok
|
||||
// cases (e.g. missing privacy_level), fall back to the generic message.
|
||||
const tiktokDisclosureIncomplete = post.value.post_platforms.some((pp) => {
|
||||
if (pp.platform !== Platform.TikTok) return false;
|
||||
if (!selectedPlatformIds.value.includes(pp.id)) return false;
|
||||
const meta = platformMeta.value[pp.id] ?? {};
|
||||
return Boolean(meta.disclose) && !meta.brand_organic_toggle && !meta.brand_content_toggle;
|
||||
});
|
||||
|
||||
if (tiktokDisclosureIncomplete) {
|
||||
return trans('posts.form.tiktok.compliance_incomplete');
|
||||
}
|
||||
|
||||
if (!pinterestComplianceValid.value) {
|
||||
return trans('posts.form.pinterest.board_required');
|
||||
}
|
||||
|
||||
if (!hasContentOrMedia.value) {
|
||||
return trans('posts.edit.compliance.requires_content_or_media');
|
||||
}
|
||||
|
||||
return trans('posts.edit.compliance_incomplete');
|
||||
const {
|
||||
platformLimits,
|
||||
mediaIssues,
|
||||
platformIssues,
|
||||
contentLengthOverflows,
|
||||
canSchedule,
|
||||
postActionTooltip,
|
||||
} = usePostCompliance({
|
||||
post,
|
||||
content,
|
||||
media,
|
||||
selectedPlatformIds,
|
||||
platformContentTypes,
|
||||
platformMeta,
|
||||
platformConfigs: props.platformConfigs,
|
||||
});
|
||||
|
||||
// Schedule
|
||||
|
|
|
|||
Loading…
Reference in a new issue