diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php index 3040e39..28f6454 100644 --- a/app/Models/PlaylistItem.php +++ b/app/Models/PlaylistItem.php @@ -140,7 +140,7 @@ class PlaylistItem extends Model if (! $this->isMashup()) { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', + 'deviceVariant' => $device?->deviceModel->name ?? 'og', 'scaleLevel' => $device?->deviceModel?->scale_level, 'slot' => $this->plugin instanceof Plugin ? $this->plugin->render('full', false) @@ -164,7 +164,7 @@ class PlaylistItem extends Model return view('trmnl-layouts.mashup', [ 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', + 'deviceVariant' => $device?->deviceModel->name ?? 'og', 'scaleLevel' => $device?->deviceModel?->scale_level, 'mashupLayout' => $this->getMashupLayoutType(), 'slot' => implode('', $pluginMarkups), diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 8f0ec75..7c6d2c1 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -346,7 +346,7 @@ class Plugin extends Model if ($size === 'full') { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', + 'deviceVariant' => $device?->deviceModel->name ?? 'og', 'scaleLevel' => $device?->deviceModel?->scale_level, 'slot' => $renderedContent, ])->render(); @@ -354,7 +354,7 @@ class Plugin extends Model return view('trmnl-layouts.mashup', [ 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', + 'deviceVariant' => $device?->deviceModel->name ?? 'og', 'scaleLevel' => $device?->deviceModel?->scale_level, 'slot' => $renderedContent, ])->render(); @@ -368,7 +368,7 @@ class Plugin extends Model if ($standalone) { return view('trmnl-layouts.single', [ 'colorDepth' => $device?->deviceModel?->color_depth, - 'deviceVariant' => $device?->deviceModel?->name ?? 'og', + 'deviceVariant' => $device?->deviceModel->name ?? 'og', 'scaleLevel' => $device?->deviceModel?->scale_level, 'slot' => view($this->render_markup_view, [ 'size' => $size, diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index 29b5688..c409d99 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -22,11 +22,12 @@ class PluginImportService * * @param UploadedFile $zipFile The uploaded ZIP file * @param User $user The user importing the plugin + * @param string|null $zipEntryPath Optional path to specific plugin in monorepo * @return Plugin The created plugin instance * * @throws Exception If the ZIP file is invalid or required files are missing */ - public function importFromZip(UploadedFile $zipFile, User $user): Plugin + public function importFromZip(UploadedFile $zipFile, User $user, ?string $zipEntryPath = null): Plugin { // Create a temporary directory using Laravel's temporary directory helper $tempDirName = 'temp/'.uniqid('plugin_import_', true); @@ -47,7 +48,7 @@ class PluginImportService $zip->close(); // Find the required files (settings.yml and full.liquid/full.blade.php) - $filePaths = $this->findRequiredFiles($tempDir); + $filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath); // Validate that we found the required files if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { @@ -138,11 +139,12 @@ class PluginImportService * * @param string $zipUrl The URL to the ZIP file * @param User $user The user importing the plugin + * @param string|null $zipEntryPath Optional path to specific plugin in monorepo * @return Plugin The created plugin instance * * @throws Exception If the ZIP file is invalid or required files are missing */ - public function importFromUrl(string $zipUrl, User $user): Plugin + public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null): Plugin { // Download the ZIP file $response = Http::timeout(60)->get($zipUrl); @@ -171,7 +173,7 @@ class PluginImportService $zip->close(); // Find the required files (settings.yml and full.liquid/full.blade.php) - $filePaths = $this->findRequiredFiles($tempDir); + $filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath); // Validate that we found the required files if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { @@ -257,12 +259,57 @@ class PluginImportService } } - private function findRequiredFiles(string $tempDir): array + private function findRequiredFiles(string $tempDir, ?string $zipEntryPath = null): array { $settingsYamlPath = null; $fullLiquidPath = null; $sharedLiquidPath = null; + // If zipEntryPath is specified, look for files in that specific directory first + if ($zipEntryPath) { + $targetDir = $tempDir . '/' . $zipEntryPath; + if (File::exists($targetDir)) { + // Check if files are directly in the target directory + if (File::exists($targetDir . '/settings.yml')) { + $settingsYamlPath = $targetDir . '/settings.yml'; + + if (File::exists($targetDir . '/full.liquid')) { + $fullLiquidPath = $targetDir . '/full.liquid'; + } elseif (File::exists($targetDir . '/full.blade.php')) { + $fullLiquidPath = $targetDir . '/full.blade.php'; + } + + if (File::exists($targetDir . '/shared.liquid')) { + $sharedLiquidPath = $targetDir . '/shared.liquid'; + } + } + + // Check if files are in src subdirectory of target directory + if (!$settingsYamlPath && File::exists($targetDir . '/src/settings.yml')) { + $settingsYamlPath = $targetDir . '/src/settings.yml'; + + if (File::exists($targetDir . '/src/full.liquid')) { + $fullLiquidPath = $targetDir . '/src/full.liquid'; + } elseif (File::exists($targetDir . '/src/full.blade.php')) { + $fullLiquidPath = $targetDir . '/src/full.blade.php'; + } + + if (File::exists($targetDir . '/src/shared.liquid')) { + $sharedLiquidPath = $targetDir . '/src/shared.liquid'; + } + } + + // If we found the required files in the target directory, return them + if ($settingsYamlPath && $fullLiquidPath) { + return [ + 'settingsYamlPath' => $settingsYamlPath, + 'fullLiquidPath' => $fullLiquidPath, + 'sharedLiquidPath' => $sharedLiquidPath, + ]; + } + } + } + // First, check if files are directly in the src folder if (File::exists($tempDir.'/src/settings.yml')) { $settingsYamlPath = $tempDir.'/src/settings.yml'; diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 92bd5a9..5bdae10 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -53,6 +53,7 @@ new class extends Component { 'github' => Arr::get($plugin, 'author.github'), 'license' => Arr::get($plugin, 'license'), 'zip_url' => Arr::get($plugin, 'trmnlp.zip_url'), + 'zip_entry_path' => Arr::get($plugin, 'trmnlp.zip_entry_path'), 'repo_url' => Arr::get($plugin, 'trmnlp.repo'), 'logo_url' => Arr::get($plugin, 'logo_url'), 'screenshot_url' => Arr::get($plugin, 'screenshot_url'), @@ -82,7 +83,7 @@ new class extends Component { $this->installingPlugin = $pluginId; try { - $importedPlugin = $pluginImportService->importFromUrl($plugin['zip_url'], auth()->user()); + $importedPlugin = $pluginImportService->importFromUrl($plugin['zip_url'], auth()->user(), $plugin['zip_entry_path'] ?? null); $this->dispatch('plugin-installed'); Flux::modal('import-from-catalog')->close(); diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php index 25325d2..9a7293d 100644 --- a/tests/Feature/PluginImportTest.php +++ b/tests/Feature/PluginImportTest.php @@ -6,6 +6,7 @@ use App\Models\Plugin; use App\Models\User; use App\Services\PluginImportService; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; beforeEach(function () { @@ -132,6 +133,214 @@ it('handles blade markup language correctly', function () { expect($plugin->markup_language)->toBe('blade'); }); +it('imports plugin from monorepo with zip_entry_path parameter', function () { + $user = User::factory()->create(); + + // Create a mock ZIP file with plugin in a subdirectory + $zipContent = createMockZipFile([ + 'example-plugin/settings.yml' => getValidSettingsYaml(), + 'example-plugin/full.liquid' => getValidFullLiquid(), + ]); + + $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); + + $pluginImportService = new PluginImportService(); + $plugin = $pluginImportService->importFromZip($zipFile, $user); + + expect($plugin)->toBeInstanceOf(Plugin::class) + ->and($plugin->user_id)->toBe($user->id) + ->and($plugin->name)->toBe('Test Plugin'); +}); + +it('imports plugin from monorepo with src subdirectory', function () { + $user = User::factory()->create(); + + // Create a mock ZIP file with plugin in a subdirectory with src folder + $zipContent = createMockZipFile([ + 'example-plugin/src/settings.yml' => getValidSettingsYaml(), + 'example-plugin/src/full.liquid' => getValidFullLiquid(), + ]); + + $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); + + $pluginImportService = new PluginImportService(); + $plugin = $pluginImportService->importFromZip($zipFile, $user); + + expect($plugin)->toBeInstanceOf(Plugin::class) + ->and($plugin->user_id)->toBe($user->id) + ->and($plugin->name)->toBe('Test Plugin'); +}); + +it('imports plugin from monorepo with shared.liquid in subdirectory', function () { + $user = User::factory()->create(); + + $zipContent = createMockZipFile([ + 'example-plugin/settings.yml' => getValidSettingsYaml(), + 'example-plugin/full.liquid' => getValidFullLiquid(), + 'example-plugin/shared.liquid' => '{% comment %}Monorepo shared styles{% endcomment %}', + ]); + + $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); + + $pluginImportService = new PluginImportService(); + $plugin = $pluginImportService->importFromZip($zipFile, $user); + + expect($plugin->render_markup)->toContain('{% comment %}Monorepo shared styles{% endcomment %}') + ->and($plugin->render_markup)->toContain('