['name' => 'Markup', 'flux_icon_name' => 'code-bracket', 'detail_view_route' => 'plugins.markup'], 'api' => ['name' => 'API', 'flux_icon_name' => 'braces', 'detail_view_route' => 'plugins.api'], 'image-webhook' => ['name' => 'Image Webhook', 'flux_icon_name' => 'photo', 'detail_view_route' => 'plugins.image-webhook'], ]; protected $rules = [ 'name' => 'required|string|max:255', 'data_stale_minutes' => 'required|integer|min:1', 'data_strategy' => 'required|string|in:polling,webhook,static', 'polling_url' => 'required_if:data_strategy,polling|nullable|url', 'polling_verb' => 'required|string|in:get,post', 'polling_header' => 'nullable|string|max:255', 'polling_body' => 'nullable|string', ]; public function refreshPlugins(): void { // Only show recipe plugins in the main list (image_webhook has its own management page) $userPlugins = auth()->user()?->plugins() ->where('plugin_type', 'recipe') ->get() ->makeHidden(['render_markup', 'data_payload']) ->toArray(); $allPlugins = array_merge($this->native_plugins, $userPlugins ?? []); $allPlugins = array_values($allPlugins); $allPlugins = $this->sortPlugins($allPlugins); $this->plugins = $allPlugins; } protected function sortPlugins(array $plugins): array { $pluginsToSort = array_values($plugins); switch ($this->sortBy) { case 'name_asc': usort($pluginsToSort, function($a, $b) { return strcasecmp($a['name'] ?? '', $b['name'] ?? ''); }); break; case 'name_desc': usort($pluginsToSort, function($a, $b) { return strcasecmp($b['name'] ?? '', $a['name'] ?? ''); }); break; case 'date_desc': usort($pluginsToSort, function($a, $b) { $aDate = $a['created_at'] ?? '1970-01-01'; $bDate = $b['created_at'] ?? '1970-01-01'; return strcmp($bDate, $aDate); }); break; case 'date_asc': usort($pluginsToSort, function($a, $b) { $aDate = $a['created_at'] ?? '1970-01-01'; $bDate = $b['created_at'] ?? '1970-01-01'; return strcmp($aDate, $bDate); }); break; } return $pluginsToSort; } public function mount(): void { $this->refreshPlugins(); } public function updatedSortBy(): void { $this->refreshPlugins(); } public function getListeners(): array { return [ 'plugin-installed' => 'refreshPlugins', ]; } public function addPlugin(): void { abort_unless(auth()->user() !== null, 403); $this->validate(); \App\Models\Plugin::create([ 'uuid' => Str::uuid(), 'user_id' => auth()->id(), 'name' => $this->name, 'data_stale_minutes' => $this->data_stale_minutes, 'data_strategy' => $this->data_strategy, 'polling_url' => $this->polling_url ?? null, 'polling_verb' => $this->polling_verb, 'polling_header' => $this->polling_header, 'polling_body' => $this->polling_body, ]); $this->reset(['name', 'data_stale_minutes', 'data_strategy', 'polling_url', 'polling_verb', 'polling_header', 'polling_body']); $this->refreshPlugins(); Flux::modal('add-plugin')->close(); } public function seedExamplePlugins(): void { Artisan::call(ExampleRecipesSeederCommand::class, ['user_id' => auth()->id()]); $this->refreshPlugins(); } public function importZip(PluginImportService $pluginImportService): void { abort_unless(auth()->user() !== null, 403); $this->validate([ 'zipFile' => 'required|file|mimes:zip|max:10240', // 10MB max ]); try { $plugin = $pluginImportService->importFromZip($this->zipFile, auth()->user()); $this->refreshPlugins(); $this->reset(['zipFile']); Flux::modal('import-zip')->close(); } catch (\Exception $e) { $this->addError('zipFile', 'Error installing plugin: ' . $e->getMessage()); } } }; ?>
settings.yml and full.liquid files.The ZIP file should contain the following structure:
--}} {{----}}
{{--.--}}
{{--├── src--}}
{{--│ ├── full.liquid (required)--}}
{{--│ ├── settings.yml (required)--}}
{{--│ └── ...--}}
{{--└── ...--}}
{{-- --}}
date: "%N" is unsupported. Use date: "u" instead