- Plan 10-02: Added explicit UserMailService wiring in Account.scala onRegister handler for activation emails when activateMode == User - Plan 10-04: Split into backend-only (models, services, admin controller) reducing from 16 to 15 files, estimated context ~50% - Plan 10-05: New plan for frontend components (Posts, Post, Categories, RelatedPosts), Wave 3 depends on 10-04 - Updated ROADMAP.md to reflect 5 plans for Phase 10
365 lines
13 KiB
Markdown
365 lines
13 KiB
Markdown
---
|
|
phase: 10-core-plugins
|
|
plan: 04
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["10-03"]
|
|
files_modified:
|
|
- plugins/golem15/blog/models/Category.scala
|
|
- plugins/golem15/blog/models/Tag.scala
|
|
- plugins/golem15/blog/models/PostCategory.scala
|
|
- plugins/golem15/blog/models/PostTag.scala
|
|
- plugins/golem15/blog/repositories/CategoryRepository.scala
|
|
- plugins/golem15/blog/repositories/TagRepository.scala
|
|
- plugins/golem15/blog/services/CategoryService.scala
|
|
- plugins/golem15/blog/services/TagService.scala
|
|
- plugins/golem15/blog/services/NestedTree.scala
|
|
- plugins/golem15/blog/services/BlogPostService.scala
|
|
- plugins/golem15/blog/repositories/BlogPostRepository.scala
|
|
- plugins/golem15/blog/controllers/Posts.scala
|
|
- plugins/golem15/blog/controllers/Categories.scala
|
|
- plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql
|
|
- plugins/golem15/blog/resources/controllers/posts/fields.yaml
|
|
- plugins/golem15/blog/resources/controllers/categories/fields.yaml
|
|
- plugins/golem15/blog/resources/controllers/categories/columns.yaml
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Blog posts can be assigned to categories"
|
|
- "Categories are hierarchical (parent/child)"
|
|
- "Blog posts can have multiple tags"
|
|
- "Admin can manage categories in tree view"
|
|
- "Related posts calculated by shared categories/tags"
|
|
artifacts:
|
|
- path: "plugins/golem15/blog/models/Category.scala"
|
|
provides: "Hierarchical category model with nested set"
|
|
contains: "case class Category"
|
|
- path: "plugins/golem15/blog/models/Tag.scala"
|
|
provides: "Tag model for free-form labeling"
|
|
contains: "case class Tag"
|
|
- path: "plugins/golem15/blog/services/CategoryService.scala"
|
|
provides: "Category CRUD with tree operations"
|
|
exports: ["CategoryService", "getTree", "getAllChildrenAndSelf"]
|
|
- path: "plugins/golem15/blog/controllers/Categories.scala"
|
|
provides: "Admin controller for category management"
|
|
exports: ["CategoriesController"]
|
|
- path: "plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql"
|
|
provides: "Categories, tags, and pivot tables"
|
|
contains: "CREATE TABLE blog_categories"
|
|
key_links:
|
|
- from: "plugins/golem15/blog/services/BlogPostService.scala"
|
|
to: "CategoryService"
|
|
via: "category filtering"
|
|
pattern: "categoryService\\.getAllChildrenAndSelf"
|
|
- from: "plugins/golem15/blog/services/CategoryService.scala"
|
|
to: "NestedTree"
|
|
via: "tree operations"
|
|
pattern: "NestedTree\\."
|
|
- from: "plugins/golem15/blog/controllers/Categories.scala"
|
|
to: "CategoryService"
|
|
via: "CRUD operations"
|
|
pattern: "categoryService\\."
|
|
---
|
|
|
|
<objective>
|
|
Add categories and tags to the Blog plugin with admin management.
|
|
|
|
Purpose: Enable content organization with hierarchical categories and free-form tags, providing the data layer for frontend components.
|
|
|
|
Output: Blog plugin extended with:
|
|
- Hierarchical categories using nested set model
|
|
- Tags for free-form labeling
|
|
- Admin controller for category management
|
|
- BlogPostService updated with category/tag relations
|
|
- Related posts algorithm based on shared categories/tags
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/10-core-plugins/10-CONTEXT.md
|
|
@.planning/phases/10-core-plugins/10-RESEARCH.md
|
|
|
|
# Prior plan in this phase
|
|
@.planning/phases/10-core-plugins/10-03-SUMMARY.md
|
|
|
|
# Reference: Phase 7 admin controller patterns
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Category and tag models with services</name>
|
|
<files>
|
|
plugins/golem15/blog/models/Category.scala
|
|
plugins/golem15/blog/models/Tag.scala
|
|
plugins/golem15/blog/models/PostCategory.scala
|
|
plugins/golem15/blog/models/PostTag.scala
|
|
plugins/golem15/blog/repositories/CategoryRepository.scala
|
|
plugins/golem15/blog/repositories/TagRepository.scala
|
|
plugins/golem15/blog/services/CategoryService.scala
|
|
plugins/golem15/blog/services/TagService.scala
|
|
plugins/golem15/blog/services/NestedTree.scala
|
|
plugins/golem15/blog/resources/db/migration/V10_2_2__blog_categories_tags.sql
|
|
</files>
|
|
<action>
|
|
Create category and tag models with nested tree support:
|
|
|
|
**Category.scala (research Pattern 6):**
|
|
- Case class with fields:
|
|
id, name, slug (unique), description (Option), parentId (Option),
|
|
nestLeft (Int), nestRight (Int), nestDepth (Int),
|
|
published, publishedAt, createdAt, updatedAt
|
|
- Quill table mapping to `blog_categories`
|
|
- CategoryTreeNode case class: category, children (List), postCount
|
|
|
|
**Tag.scala (research Pattern 7):**
|
|
- Case class: id, name, slug (unique), createdAt, updatedAt
|
|
- Quill table mapping to `blog_tags`
|
|
- TagWithCount case class: tag, postCount
|
|
|
|
**PostCategory.scala & PostTag.scala:**
|
|
- Pivot case classes for many-to-many relationships
|
|
- PostCategory: postId, categoryId
|
|
- PostTag: postId, tagId
|
|
|
|
**NestedTree.scala (utility object):**
|
|
- Functions for nested set operations:
|
|
- rebuild(categories: List[Category]): List[Category] - Recalculate left/right
|
|
- descendants(category, all): List[Category] - Get all children
|
|
- ancestors(category, all): List[Category] - Get all parents
|
|
- moveNode(category, newParent, all): List[Category] - Move category in tree
|
|
- insertNode(category, parent, all): List[Category] - Add new category
|
|
- deleteNode(category, all): List[Category] - Remove category and adjust tree
|
|
|
|
**CategoryRepository.scala:**
|
|
- Trait:
|
|
- findById(id): Option[Category]
|
|
- findBySlug(slug): Option[Category]
|
|
- listAll: List[Category]
|
|
- create(category): Category
|
|
- update(category): Category
|
|
- delete(id): Unit
|
|
- getPostCount(categoryId): Int
|
|
- getNestedPostCount(categoryId): Int (includes children)
|
|
- Live implementation using QuillContext
|
|
|
|
**TagRepository.scala:**
|
|
- Trait:
|
|
- findById(id): Option[Tag]
|
|
- findBySlug(slug): Option[Tag]
|
|
- findByName(name): Option[Tag]
|
|
- listAll: List[Tag]
|
|
- create(tag): Tag
|
|
- listForPost(postId): List[Tag]
|
|
- getPopular(limit): List[TagWithCount]
|
|
- Live implementation
|
|
|
|
**CategoryService.scala:**
|
|
- Trait:
|
|
- create(data): Category (auto-generate slug, rebuild tree)
|
|
- update(id, data): Category (rebuild tree if parent changed)
|
|
- delete(id): Unit (rebuild tree after)
|
|
- findBySlug(slug): Option[Category]
|
|
- listAll: List[Category]
|
|
- getTree: List[CategoryTreeNode]
|
|
- getChildren(parentId): List[Category]
|
|
- getAllChildrenAndSelf(categoryId): List[Category] (for nested queries)
|
|
- getPostCount(categoryId): Int
|
|
- getNestedPostCount(categoryId): Int
|
|
- Live implementation with NestedTree for tree operations
|
|
|
|
**TagService.scala:**
|
|
- Trait:
|
|
- findOrCreate(name): Tag (create with auto-slug if not exists)
|
|
- findBySlug(slug): Option[Tag]
|
|
- listAll: List[Tag]
|
|
- listForPost(postId): List[Tag]
|
|
- getPopularTags(limit): List[TagWithCount]
|
|
- syncPostTags(postId, tagNames): List[Tag] (update pivot table)
|
|
- Live implementation
|
|
|
|
**V10_2_2__blog_categories_tags.sql:**
|
|
- CREATE TABLE blog_categories (all fields, nested set indexes)
|
|
- CREATE TABLE blog_tags (id, name, slug unique, timestamps)
|
|
- CREATE TABLE blog_posts_categories (post_id, category_id, composite PK, FKs)
|
|
- CREATE TABLE blog_posts_tags (post_id, tag_id, composite PK, FKs)
|
|
- ALTER TABLE blog_posts ADD CONSTRAINT fk_main_category REFERENCES blog_categories
|
|
- Indexes on slugs, nested set columns, pivot table lookups
|
|
</action>
|
|
<verify>
|
|
./mill summercms.compile succeeds
|
|
NestedTree rebuild produces valid left/right sequence
|
|
SQL migration syntax valid
|
|
</verify>
|
|
<done>
|
|
Category model with nested set tree support.
|
|
Tag model for free-form labeling.
|
|
Services handle CRUD with automatic tree maintenance.
|
|
Pivot tables link posts to categories and tags.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Update BlogPostService and admin for relations</name>
|
|
<files>
|
|
plugins/golem15/blog/services/BlogPostService.scala (modify)
|
|
plugins/golem15/blog/repositories/BlogPostRepository.scala (modify)
|
|
plugins/golem15/blog/controllers/Posts.scala (modify)
|
|
plugins/golem15/blog/controllers/Categories.scala
|
|
plugins/golem15/blog/resources/controllers/posts/fields.yaml (modify)
|
|
plugins/golem15/blog/resources/controllers/categories/fields.yaml
|
|
plugins/golem15/blog/resources/controllers/categories/columns.yaml
|
|
</files>
|
|
<action>
|
|
Integrate categories and tags with blog posts:
|
|
|
|
**BlogPostRepository.scala additions:**
|
|
- findBySlugWithRelations(slug): Option[BlogPostWithRelations]
|
|
- listFrontendWithRelations(options): PaginatedResult[BlogPostWithRelations]
|
|
- listByCategory(categoryId, options): PaginatedResult[BlogPostWithRelations]
|
|
- listByTag(tagSlug, options): PaginatedResult[BlogPostWithRelations]
|
|
- getRelatedPosts(postId, limit): List[BlogPostWithRelations]
|
|
- Join queries to load categories, tags, author in single query
|
|
|
|
**BlogPostService.scala modifications:**
|
|
- BlogPostWithRelations case class:
|
|
post: BlogPost, categories: List[Category], tags: List[Tag],
|
|
featuredImages: List[MediaItem], author: Option[BackendUser],
|
|
readingTime: Int
|
|
- Update create/update to handle categories and tags:
|
|
- PostCreateData/UpdateData add: categoryIds (List[Long]), tagNames (List[String])
|
|
- On save: Sync pivot tables via services
|
|
- Set mainCategoryId if categories provided
|
|
- Update listFrontend to use listFrontendWithRelations
|
|
- Add listByCategory(categoryId, options)
|
|
- Add listByTag(tagSlug, options)
|
|
- Add getRelatedPosts(postId, limit):
|
|
- Score = 2 * shared_categories + 1 * shared_tags
|
|
- Exclude current post
|
|
- Return top N by score
|
|
|
|
**Posts.scala controller modifications:**
|
|
- Update store/update actions to handle categories and tags from form
|
|
- Add category/tag selection to form context
|
|
|
|
**Categories.scala controller (new):**
|
|
- Admin controller for category management
|
|
- Actions: index, create, store, edit, update, delete, reorder (tree)
|
|
- HTMX handler for drag-drop tree reordering
|
|
|
|
**fields.yaml modifications (Posts):**
|
|
- Add categories tab fields:
|
|
```yaml
|
|
_categories:
|
|
type: relation
|
|
label: Categories
|
|
nameFrom: name
|
|
list: true
|
|
tree: true
|
|
tab: Categories
|
|
_tags:
|
|
type: taglist
|
|
label: Tags
|
|
mode: array
|
|
separator: ','
|
|
tab: Tags
|
|
```
|
|
|
|
**categories/fields.yaml:**
|
|
```yaml
|
|
fields:
|
|
name:
|
|
type: text
|
|
label: Name
|
|
required: true
|
|
|
|
slug:
|
|
type: text
|
|
label: Slug
|
|
preset:
|
|
field: name
|
|
type: slug
|
|
|
|
parent_id:
|
|
type: relation
|
|
label: Parent Category
|
|
emptyOption: -- None --
|
|
nameFrom: name
|
|
|
|
description:
|
|
type: textarea
|
|
label: Description
|
|
size: small
|
|
|
|
published:
|
|
type: checkbox
|
|
label: Published
|
|
```
|
|
|
|
**categories/columns.yaml:**
|
|
```yaml
|
|
columns:
|
|
name:
|
|
label: Name
|
|
sortable: true
|
|
tree: true
|
|
|
|
slug:
|
|
label: Slug
|
|
|
|
published:
|
|
label: Published
|
|
type: switch
|
|
|
|
post_count:
|
|
label: Posts
|
|
type: number
|
|
```
|
|
|
|
**Register Categories controller in Plugin.scala**
|
|
</action>
|
|
<verify>
|
|
./mill summercms.compile succeeds
|
|
Related posts algorithm returns posts with shared categories/tags
|
|
Category tree drag-drop works in admin
|
|
</verify>
|
|
<done>
|
|
Posts can be assigned categories and tags.
|
|
Admin can manage categories in tree view.
|
|
Related posts calculated by shared categories/tags.
|
|
Post queries include relations efficiently.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After all tasks complete:
|
|
1. Blog plugin compiles: `./mill summercms.compile`
|
|
2. Migration creates all tables: blog_categories, blog_tags, pivot tables
|
|
3. Category nested tree rebuilds correctly on insert/delete
|
|
4. Posts admin form shows category tree selector and tag input
|
|
5. Related posts algorithm returns relevant content
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Categories form hierarchical tree structure
|
|
- Nested set left/right values correct after operations
|
|
- Tags can be created on-the-fly when typing
|
|
- Post admin form shows category tree selector and tag input
|
|
- Admin can manage categories with tree view and drag-drop
|
|
- Related posts calculated by shared categories/tags
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/10-core-plugins/10-04-SUMMARY.md`
|
|
</output>
|