mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: implement Plugin duplicate action
This commit is contained in:
parent
e176f2828e
commit
e3bb9ad4e2
3 changed files with 243 additions and 2 deletions
|
|
@ -11,7 +11,6 @@ use App\Liquid\Filters\StandardFilters;
|
||||||
use App\Liquid\Filters\StringMarkup;
|
use App\Liquid\Filters\StringMarkup;
|
||||||
use App\Liquid\Filters\Uniqueness;
|
use App\Liquid\Filters\Uniqueness;
|
||||||
use App\Liquid\Tags\TemplateTag;
|
use App\Liquid\Tags\TemplateTag;
|
||||||
use App\Services\ImageGenerationService;
|
|
||||||
use App\Services\Plugin\Parsers\ResponseParserRegistry;
|
use App\Services\Plugin\Parsers\ResponseParserRegistry;
|
||||||
use App\Services\PluginImportService;
|
use App\Services\PluginImportService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
@ -25,6 +24,7 @@ use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Process;
|
use Illuminate\Support\Facades\Process;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
|
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
|
||||||
use Keepsuit\Liquid\Exceptions\LiquidException;
|
use Keepsuit\Liquid\Exceptions\LiquidException;
|
||||||
use Keepsuit\Liquid\Extensions\StandardExtension;
|
use Keepsuit\Liquid\Extensions\StandardExtension;
|
||||||
|
|
@ -455,7 +455,7 @@ class Plugin extends Model
|
||||||
public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string
|
public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string
|
||||||
{
|
{
|
||||||
if ($this->plugin_type !== 'recipe') {
|
if ($this->plugin_type !== 'recipe') {
|
||||||
throw new \InvalidArgumentException('Render method is only applicable for recipe plugins.');
|
throw new InvalidArgumentException('Render method is only applicable for recipe plugins.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->render_markup) {
|
if ($this->render_markup) {
|
||||||
|
|
@ -606,4 +606,61 @@ class Plugin extends Model
|
||||||
default => '1Tx1B',
|
default => '1Tx1B',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate the plugin, copying all attributes and handling render_markup_view
|
||||||
|
*
|
||||||
|
* @param int|null $userId Optional user ID for the duplicate. If not provided, uses the original plugin's user_id.
|
||||||
|
* @return Plugin The newly created duplicate plugin
|
||||||
|
*/
|
||||||
|
public function duplicate(?int $userId = null): self
|
||||||
|
{
|
||||||
|
// Get all attributes except id and uuid
|
||||||
|
// Use toArray() to get cast values (respects JSON casts)
|
||||||
|
$attributes = $this->toArray();
|
||||||
|
unset($attributes['id'], $attributes['uuid']);
|
||||||
|
|
||||||
|
// Handle render_markup_view - copy file content to render_markup
|
||||||
|
if ($this->render_markup_view) {
|
||||||
|
try {
|
||||||
|
$basePath = resource_path('views/'.str_replace('.', '/', $this->render_markup_view));
|
||||||
|
$paths = [
|
||||||
|
$basePath.'.blade.php',
|
||||||
|
$basePath.'.liquid',
|
||||||
|
];
|
||||||
|
|
||||||
|
$fileContent = null;
|
||||||
|
$markupLanguage = null;
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$fileContent = file_get_contents($path);
|
||||||
|
// Determine markup language based on file extension
|
||||||
|
$markupLanguage = str_ends_with($path, '.liquid') ? 'liquid' : 'blade';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fileContent !== null) {
|
||||||
|
$attributes['render_markup'] = $fileContent;
|
||||||
|
$attributes['markup_language'] = $markupLanguage;
|
||||||
|
$attributes['render_markup_view'] = null;
|
||||||
|
} else {
|
||||||
|
// File doesn't exist, remove the view reference
|
||||||
|
$attributes['render_markup_view'] = null;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If file reading fails, remove the view reference
|
||||||
|
$attributes['render_markup_view'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append " (Copy)" to the name
|
||||||
|
$attributes['name'] = $this->name.' (Copy)';
|
||||||
|
|
||||||
|
// Set user_id - use provided userId or fall back to original plugin's user_id
|
||||||
|
$attributes['user_id'] = $userId ?? $this->user_id;
|
||||||
|
|
||||||
|
// Create and return the new plugin
|
||||||
|
return self::create($attributes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,17 @@ HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function duplicatePlugin(): void
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
||||||
|
// Use the model's duplicate method
|
||||||
|
$newPlugin = $this->plugin->duplicate(auth()->id());
|
||||||
|
|
||||||
|
// Redirect to the new plugin's detail page
|
||||||
|
$this->redirect(route('plugins.recipe', ['plugin' => $newPlugin]));
|
||||||
|
}
|
||||||
|
|
||||||
public function deletePlugin(): void
|
public function deletePlugin(): void
|
||||||
{
|
{
|
||||||
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
|
||||||
|
|
@ -427,6 +438,7 @@ HTML;
|
||||||
<flux:dropdown>
|
<flux:dropdown>
|
||||||
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
||||||
<flux:menu>
|
<flux:menu>
|
||||||
|
<flux:menu.item icon="document-duplicate" wire:click="duplicatePlugin">Duplicate Plugin</flux:menu.item>
|
||||||
<flux:modal.trigger name="delete-plugin">
|
<flux:modal.trigger name="delete-plugin">
|
||||||
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
|
|
|
||||||
|
|
@ -737,3 +737,175 @@ test('plugin model preserves multi_string csv format', function (): void {
|
||||||
|
|
||||||
expect($plugin->fresh()->configuration['tags'])->toBe('laravel,pest,security');
|
expect($plugin->fresh()->configuration['tags'])->toBe('laravel,pest,security');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate copies all attributes except id and uuid', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Original Plugin',
|
||||||
|
'data_stale_minutes' => 30,
|
||||||
|
'data_strategy' => 'polling',
|
||||||
|
'polling_url' => 'https://api.example.com/data',
|
||||||
|
'polling_verb' => 'get',
|
||||||
|
'polling_header' => 'Authorization: Bearer token123',
|
||||||
|
'polling_body' => '{"query": "test"}',
|
||||||
|
'render_markup' => '<div>Test markup</div>',
|
||||||
|
'markup_language' => 'blade',
|
||||||
|
'configuration' => ['api_key' => 'secret123'],
|
||||||
|
'configuration_template' => [
|
||||||
|
'custom_fields' => [
|
||||||
|
[
|
||||||
|
'keyname' => 'api_key',
|
||||||
|
'field_type' => 'string',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'no_bleed' => true,
|
||||||
|
'dark_mode' => true,
|
||||||
|
'data_payload' => ['test' => 'data'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
// Refresh to ensure casts are applied
|
||||||
|
$original->refresh();
|
||||||
|
$duplicate->refresh();
|
||||||
|
|
||||||
|
expect($duplicate->id)->not->toBe($original->id)
|
||||||
|
->and($duplicate->uuid)->not->toBe($original->uuid)
|
||||||
|
->and($duplicate->name)->toBe('Original Plugin (Copy)')
|
||||||
|
->and($duplicate->user_id)->toBe($original->user_id)
|
||||||
|
->and($duplicate->data_stale_minutes)->toBe($original->data_stale_minutes)
|
||||||
|
->and($duplicate->data_strategy)->toBe($original->data_strategy)
|
||||||
|
->and($duplicate->polling_url)->toBe($original->polling_url)
|
||||||
|
->and($duplicate->polling_verb)->toBe($original->polling_verb)
|
||||||
|
->and($duplicate->polling_header)->toBe($original->polling_header)
|
||||||
|
->and($duplicate->polling_body)->toBe($original->polling_body)
|
||||||
|
->and($duplicate->render_markup)->toBe($original->render_markup)
|
||||||
|
->and($duplicate->markup_language)->toBe($original->markup_language)
|
||||||
|
->and($duplicate->configuration)->toBe($original->configuration)
|
||||||
|
->and($duplicate->configuration_template)->toBe($original->configuration_template)
|
||||||
|
->and($duplicate->no_bleed)->toBe($original->no_bleed)
|
||||||
|
->and($duplicate->dark_mode)->toBe($original->dark_mode)
|
||||||
|
->and($duplicate->data_payload)->toBe($original->data_payload)
|
||||||
|
->and($duplicate->render_markup_view)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate copies render_markup_view file content to render_markup', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// Create a test blade file
|
||||||
|
$testViewPath = resource_path('views/recipes/test-duplicate.blade.php');
|
||||||
|
$testContent = '<div class="test-view">Test Content</div>';
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (! is_dir(dirname($testViewPath))) {
|
||||||
|
mkdir(dirname($testViewPath), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($testViewPath, $testContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'View Plugin',
|
||||||
|
'render_markup' => null,
|
||||||
|
'render_markup_view' => 'recipes.test-duplicate',
|
||||||
|
'markup_language' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->render_markup)->toBe($testContent)
|
||||||
|
->and($duplicate->markup_language)->toBe('blade')
|
||||||
|
->and($duplicate->render_markup_view)->toBeNull()
|
||||||
|
->and($duplicate->name)->toBe('View Plugin (Copy)');
|
||||||
|
} finally {
|
||||||
|
// Clean up test file
|
||||||
|
if (file_exists($testViewPath)) {
|
||||||
|
unlink($testViewPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate handles liquid file extension', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// Create a test liquid file
|
||||||
|
$testViewPath = resource_path('views/recipes/test-duplicate-liquid.liquid');
|
||||||
|
$testContent = '<div class="test-view">{{ data.message }}</div>';
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (! is_dir(dirname($testViewPath))) {
|
||||||
|
mkdir(dirname($testViewPath), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($testViewPath, $testContent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Liquid Plugin',
|
||||||
|
'render_markup' => null,
|
||||||
|
'render_markup_view' => 'recipes.test-duplicate-liquid',
|
||||||
|
'markup_language' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->render_markup)->toBe($testContent)
|
||||||
|
->and($duplicate->markup_language)->toBe('liquid')
|
||||||
|
->and($duplicate->render_markup_view)->toBeNull();
|
||||||
|
} finally {
|
||||||
|
// Clean up test file
|
||||||
|
if (file_exists($testViewPath)) {
|
||||||
|
unlink($testViewPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate handles missing view file gracefully', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Missing View Plugin',
|
||||||
|
'render_markup' => null,
|
||||||
|
'render_markup_view' => 'recipes.nonexistent-view',
|
||||||
|
'markup_language' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->render_markup_view)->toBeNull()
|
||||||
|
->and($duplicate->name)->toBe('Missing View Plugin (Copy)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate uses provided user_id', function (): void {
|
||||||
|
$user1 = User::factory()->create();
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user1->id,
|
||||||
|
'name' => 'Original Plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate($user2->id);
|
||||||
|
|
||||||
|
expect($duplicate->user_id)->toBe($user2->id)
|
||||||
|
->and($duplicate->user_id)->not->toBe($original->user_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plugin duplicate falls back to original user_id when no user_id provided', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$original = Plugin::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => 'Original Plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$duplicate = $original->duplicate();
|
||||||
|
|
||||||
|
expect($duplicate->user_id)->toBe($original->user_id);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue