Merge branch 'main' into fix/tiktok-app-review
This commit is contained in:
commit
cbb73487f2
17 changed files with 440 additions and 163 deletions
|
|
@ -120,7 +120,9 @@ ## License
|
|||
## Star History
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/?repos=trypostit%2Ftrypost&type=date&legend=top-left">
|
||||
<img src="https://api.star-history.com/chart?repos=trypostit/trypost&type=date&legend=top-left" alt="Star History Chart" width="600">
|
||||
</a>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=trypostit/trypost&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=trypostit/trypost&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=trypostit/trypost&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ enum ContentType: string
|
|||
{
|
||||
// Instagram
|
||||
case InstagramFeed = 'instagram_feed';
|
||||
case InstagramCarousel = 'instagram_carousel';
|
||||
case InstagramReel = 'instagram_reel';
|
||||
case InstagramStory = 'instagram_story';
|
||||
|
||||
|
|
@ -51,11 +50,16 @@ enum ContentType: string
|
|||
// Mastodon
|
||||
case MastodonPost = 'mastodon_post';
|
||||
|
||||
/**
|
||||
* AI generation format for an Instagram carousel. Not a content type —
|
||||
* carousel posts are persisted as InstagramFeed.
|
||||
*/
|
||||
public const CAROUSEL_FORMAT = 'instagram_carousel';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::InstagramFeed => 'Feed Post',
|
||||
self::InstagramCarousel => 'Carousel',
|
||||
self::InstagramReel => 'Reel',
|
||||
self::InstagramStory => 'Story',
|
||||
self::LinkedInPost, self::LinkedInPagePost => 'Post',
|
||||
|
|
@ -84,7 +88,7 @@ public function description(): string
|
|||
public function platform(): SocialPlatform
|
||||
{
|
||||
return match ($this) {
|
||||
self::InstagramFeed, self::InstagramCarousel, self::InstagramReel, self::InstagramStory => SocialPlatform::Instagram,
|
||||
self::InstagramFeed, self::InstagramReel, self::InstagramStory => SocialPlatform::Instagram,
|
||||
self::LinkedInPost, self::LinkedInCarousel => SocialPlatform::LinkedIn,
|
||||
self::LinkedInPagePost, self::LinkedInPageCarousel => SocialPlatform::LinkedInPage,
|
||||
self::FacebookPost, self::FacebookReel, self::FacebookStory => SocialPlatform::Facebook,
|
||||
|
|
@ -109,7 +113,6 @@ public function aiImageDimensions(): array
|
|||
return match ($this) {
|
||||
// Vertical 4:5 (Instagram preferred portrait, Threads mirrors it)
|
||||
self::InstagramFeed,
|
||||
self::InstagramCarousel,
|
||||
self::ThreadsPost => ['width' => 1080, 'height' => 1350],
|
||||
|
||||
// Square 1:1 (LinkedIn, X, Facebook, Bluesky, Mastodon)
|
||||
|
|
@ -135,7 +138,7 @@ public function aiImageDimensions(): array
|
|||
public function aspectRatio(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::InstagramFeed, self::InstagramCarousel => '4:5',
|
||||
self::InstagramFeed => '4:5',
|
||||
self::InstagramReel, self::InstagramStory => '9:16',
|
||||
self::FacebookReel, self::FacebookStory => '9:16',
|
||||
self::TikTokVideo, self::YouTubeShort => '9:16',
|
||||
|
|
@ -149,8 +152,7 @@ public function aspectRatio(): ?string
|
|||
public function maxMediaCount(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::InstagramFeed => 1,
|
||||
self::InstagramCarousel => 10,
|
||||
self::InstagramFeed => 10,
|
||||
self::InstagramReel, self::InstagramStory => 1,
|
||||
self::LinkedInPost, self::LinkedInPagePost => 1,
|
||||
self::LinkedInCarousel, self::LinkedInPageCarousel => 20,
|
||||
|
|
@ -172,7 +174,6 @@ public function supportsVideo(): bool
|
|||
{
|
||||
return match ($this) {
|
||||
self::InstagramFeed, self::InstagramReel, self::InstagramStory => true,
|
||||
self::InstagramCarousel => false,
|
||||
self::LinkedInPost, self::LinkedInPagePost => true,
|
||||
self::LinkedInCarousel, self::LinkedInPageCarousel => false,
|
||||
self::FacebookPost, self::FacebookReel, self::FacebookStory => true,
|
||||
|
|
@ -237,7 +238,6 @@ public static function aiSupported(): array
|
|||
{
|
||||
return [
|
||||
self::InstagramFeed,
|
||||
self::InstagramCarousel,
|
||||
self::InstagramStory,
|
||||
self::LinkedInPost,
|
||||
self::LinkedInPagePost,
|
||||
|
|
|
|||
|
|
@ -20,11 +20,14 @@ public function authorize(): bool
|
|||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$allowedFormats = array_map(fn (ContentType $t) => $t->value, ContentType::aiSupported());
|
||||
$allowedFormats[] = ContentType::CAROUSEL_FORMAT;
|
||||
|
||||
return [
|
||||
'format' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::in(array_map(fn (ContentType $t) => $t->value, ContentType::aiSupported())),
|
||||
Rule::in($allowedFormats),
|
||||
],
|
||||
'social_account_id' => ['nullable', 'uuid'],
|
||||
'image_count' => ['nullable', 'integer', 'min:0', 'max:10'],
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ public function rules(): array
|
|||
'brand_color' => $hex,
|
||||
'background_color' => $hex,
|
||||
'text_color' => $hex,
|
||||
'brand_font' => ['required', 'string', Rule::in(BrandFont::values())],
|
||||
'image_style' => ['required', 'string', Rule::in(ImageStyle::values())],
|
||||
'brand_font' => ['sometimes', 'required', 'string', Rule::in(BrandFont::values())],
|
||||
'image_style' => ['sometimes', 'required', 'string', Rule::in(ImageStyle::values())],
|
||||
'content_language' => ['sometimes', 'string', 'in:en,pt-BR,es'],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public function handle(): void
|
|||
$workspace = Workspace::findOrFail($this->workspaceId);
|
||||
$socialAccount = $this->socialAccountId ? SocialAccount::find($this->socialAccountId) : null;
|
||||
|
||||
$isCarousel = $this->format === 'instagram_carousel';
|
||||
$isCarousel = $this->format === ContentType::CAROUSEL_FORMAT;
|
||||
$agentFormat = $isCarousel ? 'carousel' : 'single';
|
||||
|
||||
$slideCount = $isCarousel && $this->imageCount > 0 ? $this->imageCount : 1;
|
||||
|
|
@ -120,13 +120,26 @@ public function handle(): void
|
|||
*/
|
||||
private function dimensionsForFormat(): array
|
||||
{
|
||||
$type = ContentType::tryFrom($this->format);
|
||||
$type = $this->resolvedContentType();
|
||||
|
||||
return $type
|
||||
? $type->aiImageDimensions()
|
||||
: ['width' => TemplateImageGenerator::DEFAULT_WIDTH, 'height' => TemplateImageGenerator::DEFAULT_HEIGHT];
|
||||
}
|
||||
|
||||
/**
|
||||
* The stored content type for the requested generation format. The carousel
|
||||
* generation format is persisted as an Instagram feed post.
|
||||
*/
|
||||
private function resolvedContentType(): ?ContentType
|
||||
{
|
||||
if ($this->format === ContentType::CAROUSEL_FORMAT) {
|
||||
return ContentType::InstagramFeed;
|
||||
}
|
||||
|
||||
return ContentType::tryFrom($this->format);
|
||||
}
|
||||
|
||||
private function humanize(Workspace $workspace, array $structured, string $format): array
|
||||
{
|
||||
try {
|
||||
|
|
@ -277,23 +290,20 @@ private function createPost(Workspace $workspace, string $content, array $media,
|
|||
'date' => $this->date,
|
||||
]);
|
||||
|
||||
$contentType = ContentType::tryFrom($this->format);
|
||||
$contentType = $this->resolvedContentType();
|
||||
|
||||
if ($contentType && $socialAccount) {
|
||||
$aspectRatio = $this->aspectRatioFor($contentType);
|
||||
$platformContentType = $contentType === ContentType::InstagramCarousel
|
||||
? ContentType::InstagramFeed
|
||||
: $contentType;
|
||||
|
||||
$post->postPlatforms()
|
||||
->where('social_account_id', $socialAccount->id)
|
||||
->each(function ($platform) use ($aspectRatio, $platformContentType): void {
|
||||
->each(function ($platform) use ($aspectRatio, $contentType): void {
|
||||
$meta = $platform->meta ?? [];
|
||||
if ($aspectRatio !== null) {
|
||||
$meta['aspect_ratio'] = $aspectRatio;
|
||||
}
|
||||
$platform->meta = $meta;
|
||||
$platform->content_type = $platformContentType->value;
|
||||
$platform->content_type = $contentType->value;
|
||||
$platform->enabled = true;
|
||||
$platform->save();
|
||||
});
|
||||
|
|
|
|||
264
composer.lock
generated
264
composer.lock
generated
|
|
@ -8062,16 +8062,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.6.0",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -8084,7 +8084,7 @@
|
|||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -8109,7 +8109,7 @@
|
|||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8120,29 +8120,33 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
"time": "2026-04-13T15:52:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dom-crawler",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/dom-crawler.git",
|
||||
"reference": "284ace90732b445b027728b5e0eec6418a17a364"
|
||||
"reference": "77ca351474ea018daba5f2e473cbf1b9b8e72ac6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/284ace90732b445b027728b5e0eec6418a17a364",
|
||||
"reference": "284ace90732b445b027728b5e0eec6418a17a364",
|
||||
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/77ca351474ea018daba5f2e473cbf1b9b8e72ac6",
|
||||
"reference": "77ca351474ea018daba5f2e473cbf1b9b8e72ac6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
|
|
@ -8175,7 +8179,7 @@
|
|||
"description": "Eases DOM navigation for HTML and XML documents",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/dom-crawler/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/dom-crawler/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8195,24 +8199,24 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/error-handler",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/error-handler.git",
|
||||
"reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517"
|
||||
"reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/c1119fe8dcfc3825ec74ec061b96ef0c8f281517",
|
||||
"reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517",
|
||||
"url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5",
|
||||
"reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/polyfill-php85": "^1.32",
|
||||
"symfony/var-dumper": "^7.4|^8.0"
|
||||
|
|
@ -8256,7 +8260,7 @@
|
|||
"description": "Provides tools to manage errors and ease debugging PHP code",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/error-handler/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/error-handler/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8276,24 +8280,25 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v8.0.9",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f"
|
||||
"reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f",
|
||||
"reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102",
|
||||
"reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/event-dispatcher-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
|
|
@ -8341,7 +8346,7 @@
|
|||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8361,20 +8366,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-18T13:51:42+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
"version": "v3.6.0",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
|
||||
"reference": "59eb412e93815df44f05f342958efa9f46b1e586"
|
||||
"reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586",
|
||||
"reference": "59eb412e93815df44f05f342958efa9f46b1e586",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32",
|
||||
"reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -8388,7 +8393,7 @@
|
|||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -8421,7 +8426,7 @@
|
|||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0"
|
||||
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8432,12 +8437,16 @@
|
|||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
"time": "2026-01-05T13:30:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
|
|
@ -8579,20 +8588,21 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "02656f7ebeae5c155d659e946f6b3a33df24051b"
|
||||
"reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/02656f7ebeae5c155d659e946f6b3a33df24051b",
|
||||
"reference": "02656f7ebeae5c155d659e946f6b3a33df24051b",
|
||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/af11474600f06718086c2cda4fa6fa8d0a672e7e",
|
||||
"reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "^1.1"
|
||||
},
|
||||
"conflict": {
|
||||
|
|
@ -8635,7 +8645,7 @@
|
|||
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/http-foundation/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8655,34 +8665,38 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-kernel",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-kernel.git",
|
||||
"reference": "1770f6818d83b2fddc12185025b93f39a90cb628"
|
||||
"reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/1770f6818d83b2fddc12185025b93f39a90cb628",
|
||||
"reference": "1770f6818d83b2fddc12185025b93f39a90cb628",
|
||||
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/cefeb37c82eed3e0c42fa25ba64cd3a908d90f39",
|
||||
"reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/error-handler": "^7.4|^8.0",
|
||||
"symfony/event-dispatcher": "^7.4|^8.0",
|
||||
"symfony/http-foundation": "^7.4|^8.0",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<8.1",
|
||||
"symfony/flex": "<2.10",
|
||||
"symfony/http-client-contracts": "<2.5",
|
||||
"symfony/translation-contracts": "<2.5",
|
||||
"symfony/var-dumper": "<8.1",
|
||||
"symfony/web-profiler-bundle": "<8.1",
|
||||
"twig/twig": "<3.21"
|
||||
},
|
||||
"provide": {
|
||||
|
|
@ -8695,13 +8709,14 @@
|
|||
"symfony/config": "^7.4|^8.0",
|
||||
"symfony/console": "^7.4|^8.0",
|
||||
"symfony/css-selector": "^7.4|^8.0",
|
||||
"symfony/dependency-injection": "^7.4|^8.0",
|
||||
"symfony/dependency-injection": "^8.1",
|
||||
"symfony/dom-crawler": "^7.4|^8.0",
|
||||
"symfony/expression-language": "^7.4|^8.0",
|
||||
"symfony/finder": "^7.4|^8.0",
|
||||
"symfony/http-client-contracts": "^2.5|^3",
|
||||
"symfony/process": "^7.4|^8.0",
|
||||
"symfony/property-access": "^7.4|^8.0",
|
||||
"symfony/rate-limiter": "^7.4|^8.0",
|
||||
"symfony/routing": "^7.4|^8.0",
|
||||
"symfony/serializer": "^7.4|^8.0",
|
||||
"symfony/stopwatch": "^7.4|^8.0",
|
||||
|
|
@ -8709,7 +8724,7 @@
|
|||
"symfony/translation-contracts": "^2.5|^3",
|
||||
"symfony/uid": "^7.4|^8.0",
|
||||
"symfony/validator": "^7.4|^8.0",
|
||||
"symfony/var-dumper": "^7.4|^8.0",
|
||||
"symfony/var-dumper": "^8.1",
|
||||
"symfony/var-exporter": "^7.4|^8.0",
|
||||
"twig/twig": "^3.21"
|
||||
},
|
||||
|
|
@ -8739,7 +8754,7 @@
|
|||
"description": "Provides a structured process for converting a Request into a Response",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/http-kernel/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8759,25 +8774,25 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-31T21:14:05+00:00"
|
||||
"time": "2026-05-29T08:46:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mailer",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mailer.git",
|
||||
"reference": "ca5f6edaf8780ece814404b58a4482b22b509c56"
|
||||
"reference": "9418d772df3a03a142e3bc06f602adb2b8724877"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/ca5f6edaf8780ece814404b58a4482b22b509c56",
|
||||
"reference": "ca5f6edaf8780ece814404b58a4482b22b509c56",
|
||||
"url": "https://api.github.com/repos/symfony/mailer/zipball/9418d772df3a03a142e3bc06f602adb2b8724877",
|
||||
"reference": "9418d772df3a03a142e3bc06f602adb2b8724877",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"egulias/email-validator": "^2.1.10|^3|^4",
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"psr/event-dispatcher": "^1",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/event-dispatcher": "^7.4|^8.0",
|
||||
|
|
@ -8819,7 +8834,7 @@
|
|||
"description": "Helps sending emails",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mailer/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/mailer/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8839,24 +8854,24 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/mime",
|
||||
"version": "v8.0.9",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/mime.git",
|
||||
"reference": "a9fcb293650c054b62a5b406f4e92e7b711ea333"
|
||||
"reference": "b164ae7e3f7915aacfe9ee155f2f358502440664"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/a9fcb293650c054b62a5b406f4e92e7b711ea333",
|
||||
"reference": "a9fcb293650c054b62a5b406f4e92e7b711ea333",
|
||||
"url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664",
|
||||
"reference": "b164ae7e3f7915aacfe9ee155f2f358502440664",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/polyfill-intl-idn": "^1.10",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
|
|
@ -8905,7 +8920,7 @@
|
|||
"mime-type"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/mime/tree/v8.0.9"
|
||||
"source": "https://github.com/symfony/mime/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8925,7 +8940,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-29T15:02:55+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
|
|
@ -9182,16 +9197,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.37.0",
|
||||
"version": "v1.38.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
|
||||
"reference": "dc21118016c039a66235cf93d96b435ffb282412"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
|
||||
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412",
|
||||
"reference": "dc21118016c039a66235cf93d96b435ffb282412",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9245,7 +9260,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9265,20 +9280,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-10T14:38:51+00:00"
|
||||
"time": "2026-05-25T15:22:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.37.0",
|
||||
"version": "v1.38.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
|
||||
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
|
||||
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9330,7 +9345,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9350,20 +9365,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
"time": "2026-05-25T13:48:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.37.0",
|
||||
"version": "v1.38.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
|
||||
"reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92",
|
||||
"reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9415,7 +9430,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9435,7 +9450,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T17:25:58+00:00"
|
||||
"time": "2026-05-26T12:51:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
|
|
@ -9523,16 +9538,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.37.0",
|
||||
"version": "v1.38.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
|
||||
"reference": "8339098cae28673c15cce00d80734af0453054e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2",
|
||||
"reference": "8339098cae28673c15cce00d80734af0453054e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9579,7 +9594,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9599,7 +9614,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T17:25:58+00:00"
|
||||
"time": "2026-05-26T12:51:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php84",
|
||||
|
|
@ -9683,16 +9698,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php85",
|
||||
"version": "v1.37.0",
|
||||
"version": "v1.38.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php85.git",
|
||||
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee"
|
||||
"reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee",
|
||||
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
|
||||
"reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -9739,7 +9754,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0"
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -9759,7 +9774,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-26T13:10:57+00:00"
|
||||
"time": "2026-05-26T02:25:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php86",
|
||||
|
|
@ -10078,20 +10093,20 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/routing",
|
||||
"version": "v8.0.9",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/routing.git",
|
||||
"reference": "75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038"
|
||||
"reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038",
|
||||
"reference": "75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038",
|
||||
"url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3",
|
||||
"reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
|
|
@ -10134,7 +10149,7 @@
|
|||
"url"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/routing/tree/v8.0.9"
|
||||
"source": "https://github.com/symfony/routing/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10154,20 +10169,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-29T15:02:55+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v3.6.1",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
|
||||
"reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a",
|
||||
"reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -10185,7 +10200,7 @@
|
|||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
|
@ -10221,7 +10236,7 @@
|
|||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10241,7 +10256,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-15T11:30:57+00:00"
|
||||
"time": "2026-03-28T09:44:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
|
|
@ -10588,20 +10603,20 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1"
|
||||
"reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1",
|
||||
"reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e",
|
||||
"reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/polyfill-mbstring": "^1.0"
|
||||
},
|
||||
"conflict": {
|
||||
|
|
@ -10651,7 +10666,7 @@
|
|||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10671,31 +10686,32 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-31T07:15:36+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1"
|
||||
"reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/54174ab48c0c0f9e21512b304be17f8150ccf8f1",
|
||||
"reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0",
|
||||
"reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.4.1",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^7.4|^8.0"
|
||||
"symfony/console": "^7.4|^8.0",
|
||||
"yaml/yaml-test-suite": "*"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/yaml-lint"
|
||||
|
|
@ -10726,7 +10742,7 @@
|
|||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/yaml/tree/v8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -10746,7 +10762,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ const emit = defineEmits<{
|
|||
cancel: [];
|
||||
}>();
|
||||
|
||||
const CAROUSEL_FORMAT = 'instagram_carousel' as const;
|
||||
type AiFormat = ContentTypeValue | typeof CAROUSEL_FORMAT;
|
||||
|
||||
// Selections
|
||||
const selectedFormat = ref<ContentTypeValue | null>(null);
|
||||
const selectedFormat = ref<AiFormat | null>(null);
|
||||
const selectedAccountId = ref<string | null>(null);
|
||||
const includeImages = ref(true);
|
||||
const imageCount = ref(2);
|
||||
|
|
@ -59,9 +62,9 @@ const httpStart = useHttp<{
|
|||
date: string | null;
|
||||
}>({ format: null, social_account_id: null, image_count: 0, prompt: '', date: null });
|
||||
|
||||
const AI_FORMATS: Array<{ value: ContentTypeValue; platforms: string[] }> = [
|
||||
const AI_FORMATS: Array<{ value: AiFormat; platforms: string[] }> = [
|
||||
{ value: ContentType.InstagramFeed, platforms: ['instagram', 'instagram-facebook'] },
|
||||
{ value: ContentType.InstagramCarousel, platforms: ['instagram', 'instagram-facebook'] },
|
||||
{ value: CAROUSEL_FORMAT, platforms: ['instagram', 'instagram-facebook'] },
|
||||
{ value: ContentType.InstagramStory, platforms: ['instagram', 'instagram-facebook'] },
|
||||
{ value: ContentType.LinkedInPost, platforms: ['linkedin'] },
|
||||
{ value: ContentType.LinkedInPagePost, platforms: ['linkedin-page'] },
|
||||
|
|
@ -95,7 +98,7 @@ const accountsForFormat = computed(() => {
|
|||
return props.socialAccounts.filter((a) => format.platforms.includes(a.platform));
|
||||
});
|
||||
|
||||
const isCarousel = computed(() => selectedFormat.value === ContentType.InstagramCarousel);
|
||||
const isCarousel = computed(() => selectedFormat.value === CAROUSEL_FORMAT);
|
||||
const requiresImage = computed(() =>
|
||||
selectedFormat.value === ContentType.FacebookPost ||
|
||||
selectedFormat.value === ContentType.PinterestPin ||
|
||||
|
|
@ -140,11 +143,11 @@ watch(accountsForFormat, (accounts) => {
|
|||
}
|
||||
});
|
||||
|
||||
const selectFormat = (format: ContentTypeValue) => {
|
||||
const selectFormat = (format: AiFormat) => {
|
||||
selectedFormat.value = format;
|
||||
// Sensible default per format. Picking a format always pre-selects an
|
||||
// image option so the user sees a chip highlighted on arrival.
|
||||
if (format === ContentType.InstagramCarousel) {
|
||||
if (format === CAROUSEL_FORMAT) {
|
||||
imageCount.value = 5;
|
||||
} else if (format === ContentType.InstagramFeed) {
|
||||
imageCount.value = 1;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export const ContentType = {
|
||||
InstagramFeed: 'instagram_feed',
|
||||
InstagramCarousel: 'instagram_carousel',
|
||||
InstagramStory: 'instagram_story',
|
||||
InstagramReel: 'instagram_reel',
|
||||
LinkedInPost: 'linkedin_post',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { PostStatus } from '@/types/post';
|
|||
interface PostPlatform {
|
||||
id: string;
|
||||
platform: string;
|
||||
content: string;
|
||||
status: string;
|
||||
social_account: {
|
||||
id: string;
|
||||
|
|
@ -31,6 +30,7 @@ interface PostPlatform {
|
|||
interface Post {
|
||||
id: string;
|
||||
status: string;
|
||||
content: string | null;
|
||||
scheduled_at: string;
|
||||
post_platforms: PostPlatform[];
|
||||
}
|
||||
|
|
@ -297,7 +297,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
<div v-if="dayPosts.length > 0" class="space-y-3">
|
||||
<Link v-for="post in dayPosts" :key="post.id" :href="getPostUrl(post)" class="block">
|
||||
<div
|
||||
class="rounded-xl border-2 border-foreground p-4 shadow-2xs transition-all hover:-translate-y-0.5 hover:shadow-md"
|
||||
class="rounded-xl border-2 border-foreground p-4 shadow-2xs transition-all hover:shadow-md"
|
||||
:class="getStatusColor(post.status)"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
|
|
@ -334,7 +334,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
|
||||
<!-- Content Preview -->
|
||||
<p class="line-clamp-2 text-sm font-medium text-foreground/80">
|
||||
{{ post.post_platforms[0]?.content || $t('calendar.no_content') }}
|
||||
{{ post.content?.trim() || $t('calendar.no_content') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -383,7 +383,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
<!-- Posts -->
|
||||
<Link v-for="post in getPostsForDay(day)" :key="post.id" :href="getPostUrl(post)" class="block">
|
||||
<div
|
||||
class="rounded-lg border-2 border-foreground p-2 text-sm shadow-2xs transition-all hover:-translate-y-0.5 hover:shadow-sm"
|
||||
class="rounded-lg border-2 border-foreground p-2 text-sm shadow-2xs transition-all hover:shadow-sm"
|
||||
:class="getStatusColor(post.status)"
|
||||
>
|
||||
<!-- Time -->
|
||||
|
|
@ -418,7 +418,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
|
||||
<!-- Content Preview -->
|
||||
<p class="line-clamp-2 text-xs font-medium text-foreground/80">
|
||||
{{ post.post_platforms[0]?.content || $t('calendar.no_content') }}
|
||||
{{ post.content?.trim() || $t('calendar.no_content') }}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
@ -479,12 +479,13 @@ const formatTime = (scheduledAt: string): string => {
|
|||
</div>
|
||||
|
||||
<!-- Posts -->
|
||||
<div class="min-h-0 flex-1 space-y-1 overflow-y-auto">
|
||||
<div class="min-h-0 flex-1 space-y-1 overflow-y-auto px-1">
|
||||
<Link v-for="post in getPostsForDay(day).slice(0, 3)" :key="post.id" :href="getPostUrl(post)" class="block">
|
||||
<div
|
||||
class="flex items-center justify-between gap-1.5 rounded-md border-2 border-foreground px-2 py-1 text-xs shadow-2xs transition-all hover:-translate-y-0.5 hover:shadow-sm"
|
||||
class="flex flex-col gap-1 rounded-md border-2 border-foreground px-2 py-1 text-xs shadow-2xs transition-all hover:shadow-sm"
|
||||
:class="getStatusColor(post.status)"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-1.5">
|
||||
<span class="shrink-0 font-bold">{{ formatTime(post.scheduled_at) }}</span>
|
||||
<div class="flex shrink-0 -space-x-1">
|
||||
<TooltipProvider
|
||||
|
|
@ -513,6 +514,10 @@ const formatTime = (scheduledAt: string): string => {
|
|||
+{{ post.post_platforms.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="line-clamp-1 font-medium text-foreground/80">
|
||||
{{ post.content?.trim() || $t('calendar.no_content') }}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -50,6 +50,26 @@
|
|||
->assertJsonValidationErrors(['format']);
|
||||
});
|
||||
|
||||
test('start accepts instagram_carousel as a generation format', function () {
|
||||
Bus::fake();
|
||||
|
||||
$account = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Instagram,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson(route('app.posts.ai.create'), [
|
||||
'prompt' => 'Five tips about productivity',
|
||||
'format' => 'instagram_carousel',
|
||||
'social_account_id' => $account->id,
|
||||
'image_count' => 5,
|
||||
])
|
||||
->assertStatus(Response::HTTP_ACCEPTED);
|
||||
|
||||
Bus::assertDispatched(StreamPostCreation::class, fn ($job) => $job->format === 'instagram_carousel');
|
||||
});
|
||||
|
||||
test('start rejects social_account_id from another workspace', function () {
|
||||
Bus::fake();
|
||||
|
||||
|
|
|
|||
91
tests/Feature/Ai/StreamPostCreationTest.php
Normal file
91
tests/Feature/Ai/StreamPostCreationTest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Ai\Agents\PostContentGenerator;
|
||||
use App\Ai\Agents\PostContentHumanizer;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Jobs\Ai\StreamPostCreation;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Ai\Image;
|
||||
|
||||
beforeEach(function () {
|
||||
Bus::fake();
|
||||
Storage::fake();
|
||||
Image::fake();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
|
||||
$this->account = SocialAccount::factory()->instagram()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
]);
|
||||
});
|
||||
|
||||
function runStreamPostCreation(string $format, SocialAccount $account, int $imageCount): void
|
||||
{
|
||||
(new StreamPostCreation(
|
||||
userId: $account->workspace->user_id,
|
||||
creationId: (string) Str::uuid(),
|
||||
workspaceId: $account->workspace_id,
|
||||
format: $format,
|
||||
socialAccountId: $account->id,
|
||||
imageCount: $imageCount,
|
||||
prompt: 'Five tips about productivity',
|
||||
))->handle();
|
||||
}
|
||||
|
||||
test('AI carousel generation stores the post as an instagram feed, never instagram_carousel', function () {
|
||||
// Empty image_keywords make TemplateImageGenerator::render() return null, so
|
||||
// the storage decision is exercised without touching the image pipeline.
|
||||
PostContentGenerator::fake([[
|
||||
'caption' => 'Swipe to see the tips',
|
||||
'slides' => [
|
||||
['title' => 'Tip 1', 'body' => 'First tip', 'image_keywords' => []],
|
||||
['title' => 'Tip 2', 'body' => 'Second tip', 'image_keywords' => []],
|
||||
],
|
||||
]]);
|
||||
PostContentHumanizer::fake([[
|
||||
'caption' => 'Swipe to see the tips',
|
||||
'slides' => [
|
||||
['title' => 'Tip 1', 'body' => 'First tip'],
|
||||
['title' => 'Tip 2', 'body' => 'Second tip'],
|
||||
],
|
||||
]]);
|
||||
|
||||
runStreamPostCreation('instagram_carousel', $this->account, 2);
|
||||
|
||||
$platform = PostPlatform::where('social_account_id', $this->account->id)->firstOrFail();
|
||||
|
||||
expect($platform->content_type)->toBe(ContentType::InstagramFeed);
|
||||
expect($platform->meta['aspect_ratio'] ?? null)->toBe('4:5');
|
||||
});
|
||||
|
||||
test('AI single feed generation stores the post as an instagram feed', function () {
|
||||
PostContentGenerator::fake([[
|
||||
'content' => 'A single productivity tip',
|
||||
'image_title' => 'Tip',
|
||||
'image_body' => 'Do less',
|
||||
'image_keywords' => [],
|
||||
]]);
|
||||
PostContentHumanizer::fake([[
|
||||
'content' => 'A single productivity tip',
|
||||
'image_title' => 'Tip',
|
||||
'image_body' => 'Do less',
|
||||
]]);
|
||||
|
||||
runStreamPostCreation('instagram_feed', $this->account, 0);
|
||||
|
||||
$platform = PostPlatform::where('social_account_id', $this->account->id)->firstOrFail();
|
||||
|
||||
expect($platform->content_type)->toBe(ContentType::InstagramFeed);
|
||||
});
|
||||
|
|
@ -167,6 +167,39 @@
|
|||
->assertOk();
|
||||
});
|
||||
|
||||
it('rejects creating a post with instagram_carousel — carousel is not a stored content_type', function () {
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'instagram_carousel'],
|
||||
],
|
||||
])
|
||||
->assertJsonValidationErrors(['platforms.0.content_type']);
|
||||
});
|
||||
|
||||
it('rejects updating a post with instagram_carousel — carousel is not a stored content_type', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
]);
|
||||
|
||||
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => 'draft',
|
||||
'platforms' => [
|
||||
['id' => $postPlatform->id, 'content_type' => 'instagram_carousel'],
|
||||
],
|
||||
])
|
||||
->assertJsonValidationErrors(['platforms.0.content_type']);
|
||||
});
|
||||
|
||||
it('cannot update post from another workspace', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$otherSocialAccount = SocialAccount::factory()->create([
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
use App\Mcp\Tools\Post\DeletePostTool;
|
||||
use App\Mcp\Tools\Post\GetPostTool;
|
||||
use App\Mcp\Tools\Post\ListPostsTool;
|
||||
use App\Mcp\Tools\Post\UpdatePostTool;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
|
@ -177,6 +179,38 @@
|
|||
$response->assertHasErrors();
|
||||
});
|
||||
|
||||
test('create post rejects instagram_carousel — carousel is not a stored content_type', function () {
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(CreatePostTool::class, [
|
||||
'platforms' => [
|
||||
['social_account_id' => $this->socialAccount->id, 'content_type' => 'instagram_carousel'],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertHasErrors();
|
||||
});
|
||||
|
||||
test('update post rejects instagram_carousel — carousel is not a stored content_type', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(UpdatePostTool::class, [
|
||||
'post_id' => $post->id,
|
||||
'platforms' => [
|
||||
['id' => $platform->id, 'content_type' => 'instagram_carousel'],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertHasErrors();
|
||||
});
|
||||
|
||||
test('create post rejects a content_type that does not match the social account platform', function () {
|
||||
// x_post on a LinkedIn account — ContentTypeMatchesPlatform should reject.
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
|
|
|
|||
|
|
@ -176,6 +176,27 @@
|
|||
);
|
||||
});
|
||||
|
||||
test('calendar payload exposes post content for rendering', function () {
|
||||
$scheduledAt = now('UTC')->startOfWeek()->addDays(2)->setTime(12, 0);
|
||||
|
||||
Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'Caption visible in the calendar',
|
||||
'status' => PostStatus::Scheduled,
|
||||
'scheduled_at' => $scheduledAt,
|
||||
]);
|
||||
|
||||
$dateKey = $scheduledAt->format('Y-m-d');
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.calendar'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where("posts.{$dateKey}.0.content", 'Caption visible in the calendar')
|
||||
);
|
||||
});
|
||||
|
||||
// Create tests
|
||||
test('create requires authentication', function () {
|
||||
$response = $this->get(route('app.posts.create'));
|
||||
|
|
|
|||
|
|
@ -525,3 +525,19 @@
|
|||
expect(data_get($this->post->media, '0.source'))->toBe('ai');
|
||||
expect(data_get($this->post->media, '0.source_meta.title'))->toBe('Fix ECP typo');
|
||||
});
|
||||
|
||||
test('instagram_carousel is rejected as a content_type — carousel is a feed post with multiple images', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Draft->value,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $this->postPlatform->id,
|
||||
'content_type' => 'instagram_carousel',
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.content_type');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -232,6 +232,27 @@
|
|||
expect($this->workspace->refresh()->image_style->value)->toBe('minimalist');
|
||||
});
|
||||
|
||||
test('update workspace settings updates the name when only the name is submitted', function () {
|
||||
$this->actingAs($this->user)
|
||||
->from(route('app.workspace.settings'))
|
||||
->put(route('app.workspace.settings.update'), [
|
||||
'name' => 'Name Only Update',
|
||||
])
|
||||
->assertRedirect(route('app.workspace.settings'))
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
expect($this->workspace->refresh()->name)->toBe('Name Only Update');
|
||||
});
|
||||
|
||||
test('update workspace settings still requires brand_font and image_style when submitted empty', function () {
|
||||
$this->actingAs($this->user)
|
||||
->put(route('app.workspace.settings.update'), [
|
||||
'name' => $this->workspace->name,
|
||||
'brand_font' => '',
|
||||
'image_style' => '',
|
||||
])->assertSessionHasErrors(['brand_font', 'image_style']);
|
||||
});
|
||||
|
||||
test('update workspace settings rejects unknown image_style values', function () {
|
||||
$this->actingAs($this->user)
|
||||
->put(route('app.workspace.settings.update'), [
|
||||
|
|
|
|||
|
|
@ -53,9 +53,12 @@
|
|||
expect(ContentType::XPost->aspectRatio())->toBeNull();
|
||||
});
|
||||
|
||||
test('instagram_carousel is not a content type — it is an AI generation format only', function () {
|
||||
expect(ContentType::tryFrom('instagram_carousel'))->toBeNull();
|
||||
});
|
||||
|
||||
test('content type has correct max media count', function () {
|
||||
expect(ContentType::InstagramFeed->maxMediaCount())->toBe(1);
|
||||
expect(ContentType::InstagramCarousel->maxMediaCount())->toBe(10);
|
||||
expect(ContentType::InstagramFeed->maxMediaCount())->toBe(10);
|
||||
expect(ContentType::InstagramReel->maxMediaCount())->toBe(1);
|
||||
expect(ContentType::LinkedInCarousel->maxMediaCount())->toBe(20);
|
||||
expect(ContentType::XPost->maxMediaCount())->toBe(4);
|
||||
|
|
|
|||
Loading…
Reference in a new issue