From ccf38ef6f961072bc01245efd246c2229cb0a112 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sun, 18 Jan 2026 18:02:42 +0000 Subject: [PATCH] add: image and blog import fix: category field to Entry model feat: EntriesTable with searchable and sortable columns chore: update CHANGELOG with recent additions and API updates --- CHANGELOG.md | 12 ++ app/Console/Commands/ImportBlogs.php | 144 ++++++++++++++++++ app/Console/Commands/ImportImages.php | 61 ++++++++ .../Resources/Entries/Tables/EntriesTable.php | 19 ++- app/Models/Entry.php | 1 + 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/ImportBlogs.php create mode 100644 app/Console/Commands/ImportImages.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cbae33..d1f5186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## 2026-01-08 +added text widgets + +added categories + +updated api to access text widgets and categories + +added url and call to action for entries for use in cards + +added import image and blogs import commands + +## 2026-01-08 + added tags to entry model added text widget and category diff --git a/app/Console/Commands/ImportBlogs.php b/app/Console/Commands/ImportBlogs.php new file mode 100644 index 0000000..eef0702 --- /dev/null +++ b/app/Console/Commands/ImportBlogs.php @@ -0,0 +1,144 @@ +\"\"

\n

In FlowiseAI, applications based on the JavaScript fork of LangChain are modeled in a 'no/low-code' environment. If you are coming from a closed source world, yet trying to implement devops principles this may fill you with fear, dread and uncertainty. FlowiseAI offers the advantage of using plain-text JSON files to represent each workflow. These files are easy to understand, open, and readily backup-able, unlike opaque proprietary binary formats\nThe data used at runtime and other component prerequisites like credentials are stored in the FlowiseAI data volume, which looks like this

\n

bash\nMode LastWriteTime ..... 8ZS1C0ZBB.webp", + "created_at": "2024-12-17 11:24:59", + "updated_at": "2024-12-17 12:53:54", + "category_id": 1, + "blog_date": "2024-02-26 00:00", + "is_featured": 0, + "published": 1 + }, + + */ + + $filePath = '/home/user/projects/laravel/12/media_library/boring-astro-static/imported_database/blogs.json'; + + if (!file_exists($filePath)) { + $this->error("File not found: $filePath"); + return 1; + } + + $jsonContent = file_get_contents($filePath); + $blogs = json_decode($jsonContent, true); + + if (!$blogs) { + $this->error("Could not parse JSON file: $filePath"); + return 1; + } + + foreach ($blogs as $blog) { + // Only process the blog with ID 51 + if (($blog['id'] ?? null) !== 51) { + continue; + } + + $slug = $blog['slug'] ?? null; + $this->info("Processing blog ID: {$blog['id']} with slug: {$slug}"); + // Check if the entry already exists + $existingEntry = Entry::where('slug', $slug)->first(); + + if ($existingEntry) { + // Update existing entry with cleaned content + $existingEntry->update([ + 'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''), + 'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''), + ]); + $this->info("Updated content for: {$existingEntry->title}"); + } else { + // Create new entry + Entry::create([ + 'title' => $blog['title'] ?? null, + 'slug' => $slug, + 'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''), + 'is_published' => $blog['published'] ?? false, + 'is_featured' => $blog['is_featured'] ?? false, + 'published_at' => $blog['blog_date'] ?? null, + 'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''), + 'category_id' => 1, // Default category + ]); + $this->info("Created new entry: " . ($blog['title'] ?? 'Untitled')); + } + } + + $this->info('Blogs imported successfully.'); + } + + /** + * Extract plain text from HTML for description field + */ + private function extractPlainTextFromHtml(string $html): string + { + if (empty($html)) { + return ''; + } + + // Decode HTML entities and strip tags + $text = html_entity_decode(strip_tags($html)); + + // Clean up whitespace + $text = preg_replace('/\s+/', ' ', $text); + + // Limit to reasonable length for description + return trim(substr($text, 0, 500)); + } + + /** + * Clean HTML content for Filament rich editor + */ + private function cleanHtmlForFilament(string $html): string + { + if (empty($html)) { + return ''; + } + + // Convert escaped newlines to actual newlines + $html = str_replace(['\\n', '\\r\\n', '\\r'], "\n", $html); + + // Decode HTML entities + $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Clean up excessive whitespace but preserve paragraph structure + $html = preg_replace('/\s*\n\s*/', ' ', $html); + $html = preg_replace('/[ \t]+/', ' ', $html); + + // Ensure paragraphs have proper spacing + $html = str_replace('

', '

' . "\n" . '

', $html); + + return trim($html); + } +} diff --git a/app/Console/Commands/ImportImages.php b/app/Console/Commands/ImportImages.php new file mode 100644 index 0000000..7c93d12 --- /dev/null +++ b/app/Console/Commands/ImportImages.php @@ -0,0 +1,61 @@ +option('entry-id'); + $entry = Entry::find($entryId); + + if (!$entry) { + $this->error("Entry with ID {$entryId} not found"); + return; + } + + $files = Storage::disk('public')->files('imported_images'); + + if (empty($files)) { + $this->info('No files found in storage/app/public/imported_images/'); + return; + } + + $this->info("Found " . count($files) . " files to import"); + + foreach ($files as $filePath) { + try { + $fullPath = storage_path('app/public/' . $filePath); + $fileName = pathinfo($fullPath, PATHINFO_BASENAME); + + if (!file_exists($fullPath)) { + $this->error("File not found: {$fullPath}"); + continue; + } + + // Check if already exists + $existingMedia = $entry->getMedia()->where('file_name', $fileName)->first(); + if ($existingMedia) { + $this->info("Skipping existing: {$fileName}"); + continue; + } + + $media = $entry->addMedia($fullPath) + ->toMediaCollection('default'); + + $this->info("Imported: {$fileName}"); + } catch (\Exception $e) { + $this->error("Failed to import {$filePath}: " . $e->getMessage()); + } + } + + $this->info('Import completed'); + } +} diff --git a/app/Filament/Resources/Entries/Tables/EntriesTable.php b/app/Filament/Resources/Entries/Tables/EntriesTable.php index eb89fd4..723c1fd 100644 --- a/app/Filament/Resources/Entries/Tables/EntriesTable.php +++ b/app/Filament/Resources/Entries/Tables/EntriesTable.php @@ -17,18 +17,33 @@ class EntriesTable { return $table ->columns([ + TextColumn::make('id') + ->label('ID') + ->sortable(query: function ($query, $direction) { + $query->orderBy('id', $direction); + }), SpatieMediaLibraryImageColumn::make('featured_image') ->collection('featured-image') + ->label('Image') ->circular() ->stacked() ->limit(3), TextColumn::make('title') - ->searchable(), - TextColumn::make('slug') + ->searchable() + ->sortable(), + TextColumn::make('category.name') + ->label('Category') + ->sortable(query: function ($query, $direction) { + $query->join('categories', 'entries.category_id', '=', 'categories.id') + ->orderBy('categories.name', $direction) + ->select('entries.*'); + }) ->searchable(), IconColumn::make('is_published') + ->label('pub') ->boolean(), IconColumn::make('is_featured') + ->label('feat') ->boolean(), TextColumn::make('published_at') ->date() diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 476e4b5..d6d6406 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -32,6 +32,7 @@ class Entry extends Model implements HasMedia, HasRichContent 'content', 'call_to_action_link', 'call_to_action_text', + 'category_id', ]; /**