Skip to content

feat(categories): migrate Categories portlet from Dojo to Angular#35146

Open
hmoreras wants to merge 18 commits intomainfrom
issue-34731-category-portlet
Open

feat(categories): migrate Categories portlet from Dojo to Angular#35146
hmoreras wants to merge 18 commits intomainfrom
issue-34731-category-portlet

Conversation

@hmoreras
Copy link
Copy Markdown
Member

Summary

This PR migrates the Categories portlet from the legacy Dojo/Java-server implementation to a modern Angular portlet using PrimeNG and NgRx Signals. It is extracted from the broader PrimeNG migration branch (dot-categories) and rebased on top of main.

Closes #34731

What changed

  • New portlet at libs/portlets/dot-categories/ replacing the old implementation in apps/dotcms-ui/src/app/portlets/dot-categories/
  • Service moved to libs/data-access/src/lib/dot-categories/ and registered in the shared data-access index
  • Features implemented:
    • TreeTable-style list with pagination, sorting, and search/filter with debounce
    • Breadcrumb navigation — click a row to drill into children; breadcrumbs update in-memory state
    • Create/Edit dialog (single component, two modes via DynamicDialogConfig.data)
    • Context menu per row (Edit, Permissions, Delete) — visible only on hover
    • Bulk delete with confirmation dialog
    • CSV import via drag-and-drop upload dialog
    • CSV export
    • Permissions placeholder dialog (wired to future implementation)
  • All user-facing text uses i18n keys via DotMessagePipe
  • Tests included for store, list component, create component, and import component

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

fmontes and others added 17 commits March 27, 2026 11:31
Replace the legacy categories portlet in dotcms-ui with a new standalone
portlet library following the dot-tags canonical pattern. Includes API
service, SignalStore with breadcrumb navigation, CRUD dialog, and full
test coverage (52 tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show only 4 fields: name (required), variable (readonly, auto-generated
  from name in camelCase), key, and keywords (textarea)
- Variable field is always visible and auto-generates on create mode
- Remove sortOrder, active, description fields from the dialog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use Angular Router query params (?inode=xxx) as source of truth for
category navigation. Breadcrumbs are rebuilt from the API parentList
field on each load. Row click now navigates to children while a
dedicated edit icon button opens the edit dialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…te form

Remove childrenCount guard so users can navigate into any category to
create children. Add parentName to store state and URL query params so
the create dialog shows an info hint indicating the parent category.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the children API returns an empty list, keep the existing
breadcrumbs from the store instead of wiping them out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API parentList may not include the current parent when navigating
deeper. Append the current parent from query params when the last
parentList item does not match the current parentInode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop Router/ActivatedRoute query param sync and parentList API param.
Breadcrumbs are now built purely from navigation steps in the store:
navigateToChildren pushes, navigateToBreadcrumb truncates. This avoids
issues with the API parentList not matching the full navigation depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a row-level popup menu with ellipsis icon containing edit,
permissions, and delete actions. Edit opens the existing edit dialog,
delete confirms and removes a single category, permissions opens an
empty placeholder dialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace per-row p-menu with a single shared menu appended to body.
Menu items are set imperatively on button click before toggling,
avoiding the change detection issue where template-bound items were
recreated on every cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use group/group-hover Tailwind pattern to show the ellipsis button
only when hovering the row, matching the content-drive portlet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CSS grid layout to the list component host so toolbar, breadcrumb,
and table fill the available height. Enable scrollable flex mode on the
table so it scrolls within its container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Passing null to DialogService.open causes NG0919. Created a minimal
DotCategoriesPermissionsComponent as a placeholder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The separate file import caused a runtime circular dependency error.
Define the placeholder component in the same file as the list component
to avoid the import resolution issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CSV export and import capabilities to the categories service, store,
and list component. Export triggers a blob download; import uses a dialog
with file upload and merge/replace mode selection. The Add button is now
a PrimeNG SplitButton with Import as a dropdown option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hide FileUpload header via PT, use large download icon in bg-primary-50
circle, bold drag-drop text, and info-circle icon on CSV subtitle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Mar 27, 2026
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 27, 2026

Claude finished @hmoreras's task in 2m 42s —— View job


PR Review — feat(categories): migrate Categories portlet from Dojo to Angular

  • Read store implementation
  • Read service layer
  • Read list component
  • Read create/import components
  • Review tests
  • Post findings

Issues Found

1. Dead code: store.importCategories() is never called

DotCategoriesListStore exposes an importCategories(file, exportType) method (store.ts:192-197), but it's never used. DotCategoriesImportComponent calls DotCategoriesService directly (import component:48). The list component then calls store.loadCategories() on dialog close. This violates the portlet architecture rule ("Store MUST NOT open dialogs" / component passes data to store), creates two HTTP paths for the same operation, and the store method is dead code. Either remove store.importCategories() or have the import dialog just return the file+type and let the list component pass them to the store, consistent with how create/edit dialogs work. Fix this →

2. confirmDeleteSingle silently overwrites existing selection

list component:196-199:

accept: () => {
    this.store.setSelectedCategories([category]);  // ← clobbers any existing selection
    this.store.deleteCategories();
}

If a user selects 5 categories via checkboxes, then right-clicks a different row and picks Delete from the context menu, the 5-item selection is silently replaced. The bulk-delete path (confirmDelete()) correctly uses the existing selection. Single-delete should call the service with a targeted inode instead. Fix this →

3. updateSortOrder response is ignored; partial delete failures are swallowed

  • Sort order: updateSortOrder API returns DotCMSAPIResponse<DotCategory[]> (the refreshed page), but the success callback only shows a toast (store.ts:229-234). If the server reorders items differently than the optimistic update, the UI is stale until next page change.

  • Bulk delete: deleteCategories API returns { successCount, fails }, but handleCategoryAction discards the response. If some categories fail to delete (partial failure), the user gets no feedback. Fix this →

4. navigateToBreadcrumb unsafe cast hides potential undefined parentInode

store.ts:150:

const parentInode = breadcrumbs[breadcrumbs.length - 1]?.id as string;

If index is out of bounds (e.g., breadcrumbs.length - 1 after slicing produces an empty array), parentInode becomes undefined as string. This gets passed to getChildrenPaginated as the inode, producing a bad API call. The optional chain ?.id correctly evaluates to undefined but the as string cast hides it from TypeScript. Fix this →

5. Breadcrumb click uses indexOf (reference equality) which can silently return -1

template line 47:

(onItemClick)="onBreadcrumbClick($any($event).item ? store.breadcrumbs().indexOf($any($event).item) : -1)"

indexOf uses reference equality. If PrimeNG creates a new object for the event's item (which it may do depending on how it passes the model), it won't match and -1 is returned, navigating to root instead of the intended level. The $any() also bypasses type safety on a critical navigation path. Use the breadcrumb's id field to find the index instead. Fix this →

6. MessageService injected in store without being in component providers

store.ts:53 injects MessageService from PrimeNG. The list component's providers array has [DotCategoriesListStore, DialogService, ConfirmationService] (list.ts:69) — MessageService is absent. If a parent or root doesn't provide it, the sort-order save toast will throw a NullInjectorError at runtime. Add MessageService to the component's providers array or verify it's always root-provided.

7. updateSortOrder not tested in the store spec

The store spec (dot-categories-list.store.spec.ts) has no describe('updateSortOrder') block. It's one of the more complex methods (optimistic update, error rollback, toast on success) and is not covered at all. Fix this →

8. exportCategories URL built with string concatenation instead of HttpParams

service.ts:107-110:

let url = '/api/v1/categories/_export';
if (contextInode) {
    url += `?contextInode=${contextInode}`;
}

Inconsistent with every other method in the same file that uses #buildParams() / HttpParams. Inodes are UUIDs so XSS risk is low, but the inconsistency is worth fixing. Use HttpClient.get with params like the rest of the service.


Minor

  • toCamelCaseVarName('') returns "" — if the backend enforces a non-empty velocity var name, submitting a category whose name is all special characters (e.g., !!!) will fail silently at the API level.
  • importType in DotCategoriesImportComponent is a plain mutable property ('replace' | 'merge' = 'merge'), not a signal. With ChangeDetectionStrategy.OnPush this should work via template binding events, but mixing mutation styles in a signals-first component is worth flagging.
  • The DotCategoriesPermissionsPlaceholderComponent is defined in the same file as the list component (non-exported, private to the file) — acceptable but unusual; a comment explaining it's intentionally temporary would help.

@hmoreras hmoreras mentioned this pull request Mar 27, 2026
3 tasks
… order editing

- Add 32 i18n keys for all category UI labels (toolbar, table, empty state, dialogs, toasts)
- Replace right-click p-menu with p-contextMenu so it opens at cursor position
- Add row hover state and pointer cursor matching other portlets (content-drive pattern)
- Add breadcrumb cursor-pointer via PrimeNG passthrough API
- Add empty state matching tags portlet (full-height, centered icon + message)
- Add p-inputNumber per row for inline sort order editing (save on blur/Enter)
- Optimistic sort order update avoids table reload/flickering; reverts on error
- Show success toast after sort order save via MessageService at shell level
- Fix toast: move MessageService provider + p-toast to shell component so it's visible
- Fix children count lost after sort update: use optimistic patch instead of _sort response
- Add updateSortOrder() to DotCategoriesService (PUT /api/v1/categories/_sort)
- Center-align Children, Sort Order and Actions columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added the Area : Backend PR changes Java/Maven backend code label Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Dojo to Angular: Categories Portlet

2 participants