Skip to content

FEATURE: Add visual doc category index editor#87

Draft
megothss wants to merge 62 commits intomainfrom
doc-index-editor-improvements
Draft

FEATURE: Add visual doc category index editor#87
megothss wants to merge 62 commits intomainfrom
doc-index-editor-improvements

Conversation

@megothss
Copy link
Copy Markdown
Contributor

Adds a visual editor for building doc category sidebar indexes directly in the admin UI, as an alternative to the existing topic-based index approach.

  • New "Doc index" tab in the category edit dialog with three modes: Editor (visual), Index topic, and Disabled
  • Visual editor supports sections with draggable links (both topic-based and manual URL links), including drag-and-drop reordering across sections
  • Batch edit mode for multi-select operations (bulk delete, bulk reorder via drag)
  • Per-link icon picker, inline topic chooser, auto-populate from category topics, and duplicate detection warnings
  • Editor state persists across tab switches via FormKit transient data
  • Standalone "Apply" button saves the index without requiring a full category save
  • New IndexesController with topics and update endpoints
  • New IndexSaver service for persisting editor-built sections
  • index_topic_id made nullable with sentinel value of -1 for editor mode; partial unique index excludes NULL and -1
  • Sidebar links now support custom icons
  • Category serializer always includes doc_category_index key so MessageBus updates clear stale values
  • Fix editor willDestroy overwriting disabled mode form data

SamSaffron and others added 2 commits March 18, 2026 14:13
Removes a lot of the visual noise from docs
Instead of hiding reply posts with CSS display:none and fighting the
cloaking system, filter the post stream's ID array so replies are never
rendered. Uses the topic-post-stream transformer to monkey-patch
updateFromJson on doc category PostStream instances.

All posts are stored in the identity map on initial load, then the
stream and posts arrays are trimmed to the OP only. Toggling comments
restores posts from the identity map synchronously with no network
request.
@megothss megothss marked this pull request as draft March 27, 2026 01:52
SamSaffron and others added 27 commits March 30, 2026 14:55
When simple mode is enabled, the topic list should show only what
matters for documentation: topic titles and last updated date. Three
remaining visual elements were adding noise across all themes.

- Delete the "views" column (was kept alongside posters/replies removal)
- Hide inline category badge via CSS scoped to `.doc-simple-mode`
  (redundant when browsing within a single doc category)
- Hide topic excerpts on pinned topics (documentation index should be
  a scannable list, not a reading view)

Ref - t/179937

Requires discourse/discourse#38970
Adds a direct-edit index editor with auto-index, drag-and-drop,
atomic save with category, and tab-switch state persistence.
Introduce a batch editing mode to `doc-category-index-editor`, allowing
multiple items or sections to be selected, managed, and deleted at once.
Batch actions include selecting all links in a section, inverting/clearing
selection, and bulk deletion. A toolbar provides contextual feedback on
selected items and offers controls to manage batch operations.

This update also adds a "None" mode to the index editor, enabling users
to disable indexing for a category. Additional refinements include improved
state handling for edit mode toggles, enhanced UI responsiveness, and
clarified editor interactions.
Use index_topic_id as an explicit discriminator for index mode:
NULL = none, -1 = direct (visual editor), positive = topic.

Adds mode helper methods (mode_none?, mode_direct?, mode_topic?)
to the Index model, updates all consumers, and replaces the
unique index with a partial unique index excluding -1.
- Add @Tracked doc_index_topic_id to Category via modifyClass
- Use get() from @ember/object to read doc_index_topic_id
  in getters, ensuring Glimmer tracks EmberObject changes
- Defer direct mode sentinel (-1) to validator on save
- Dirty form on topic selection via form.set
- Add system tests for topic chooser selection and save
Refactor how `doc_index_topic_id` and `doc_index_sections` are managed in the `doc-category-index-editor` and related components. Replace direct modifications of category properties with `args.form` updates to improve data flow consistency. Remove `@tracked` overrides and redundant transient state handling.

This change reduces complexity by standardizing property interactions and ensures proper synchronization of form data, addressing potential inconsistencies in save payloads and mode transitions.
Add a confirmation dialog when switching to "None" mode in the
`doc-category-index-tab` if category index data or topics exist.
The dialog ensures users confirm the action to prevent accidental
data loss. Introduce a private `#applyNoneMode` method to avoid
duplicated cleanup logic and improve code clarity.
Introduce a confirmation dialog in `doc-category-index-tab` when switching
from topic mode to direct mode. This ensures users acknowledge that the
topic-based index will be disconnected, and editor changes won't sync back
to the index topic.

Update button styles in `doc-category-index-editor` for consistency and
add localized warning messages to improve clarity for users transitioning
between modes. Refine mode transitions to avoid accidental data loss and
enhance user experience.
- Add drag handle in batch selection bar for bulk reordering
- Items-only selection: drop between links in any section
- Sections-only selection: drop between sections
- Mixed selection: drag handle hidden
- Show drop indicator inside empty sections for item drags
- Disable batch edit when only one section with <= 1 link
- Reset batch mode on form discard
- Match apply button size to save button (btn-small)
- Only process drops when a visual indicator was shown
- Enlarge last item drop target area for easier dragging
- Align batch checkboxes and drag handles with section/item icons
- Validate pending edits, empty sections, empty titles, empty URLs
- Block save with FormKit error feedback via registerValidator
- Block apply with validation check before API call
- Send link type and topic_id to backend for proper validation
- Auto-detect when title matches topic title and store as nil
- Display "auto" badge on topic links using automatic titles
- Show inline validation errors (danger border + message) on confirm
- Use topic title as input placeholder for topic links
Update localized text and related system tests to rename "Visual editor"
to "Editor" in sidebar index mode options. This simplifies the terminology,
providing a clearer and more concise label for users while maintaining
functionality. Adjust system test selectors to reflect the updated wording.
When switching to disabled mode, #applyNoneMode() sets doc_index_sections
to "[]" on the form. But the editor's willDestroy() then overwrites it
with the full serialized sections, causing the save to send stale data
and revert back to editor mode.
- Wrap IndexSaver#save_sections! in a transaction so a
  mid-save failure rolls back instead of leaving partial data
- Reject direct PUT updates to topic-mode indexes instead of
  silently converting them to direct mode (returns 403)
- Validate links on focus-out before auto-committing edits
- Clamp editingCount decrements to prevent negative drift
  from racing cancel/focus-out callbacks
- Raise Discourse::InvalidParameters when sections or links
  per section exceed limits instead of silently truncating
- Remove unnecessary first_post load from
  serialize_index_structure (sidebar data comes from DB)
- Use Number() coercion in willDestroy to handle FormKit
  storing doc_index_topic_id as a string
- Inline _fetchCategoryTopics into its only call site
- Add comprehensive IndexSaver spec covering limits,
  filtering, topic_id extraction, and auto-title detection
- Add controller spec for limit validation and category
  save with doc_index_sections param
- Fix timer leak in apply() by storing and cancelling
  discourseLater in willDestroy
- Fix editingCount getting stuck when sections are removed
  while links are being edited (add willDestroy to child
  components)
- Add isDestroying guard to next() callback in onCardFocusOut
- Clean up _autoExpandTimer on IndexEditorSection destroy
- Handle empty sections after build_sections filtering in
  IndexSaver (treat as blank input)
- Add JSON parse error handling in category callback
- Add hard limit (5000) to topics endpoint and surface
  truncation warning in the UI
- Remove unused :type param from strong params
- Extract duplicated searchFilters getter to parent component
- Add aria-hidden to collapsed section body
- Add focus-visible styles to interactive elements (drag
  handles, section title label, topic href, batch drag handle)
- Rename drag state CSS classes to BEM is- prefix convention
- Add visibility: hidden to collapsed sections for a11y
- Add explanatory comment to double-class specificity hack
- Clean up --selected selectors to use & nesting
- Annotate pill border-radius magic number
- Fix IndexSaver filtering tests to expect index destruction
  when all sections are filtered out (not just empty sections)
- Add test for destroying existing index when all sections
  filter out
- Add total_count assertion to topics endpoint test
- Add test for invalid JSON in doc_index_sections returning 400
- Guard non-array JSON in IndexSaver#save_sections!
- Extract duplicate cleanup logic to private destroy_index!
- Clear stale direct-mode sidebar sections when switching
  to topic mode in CategoryIndexManager
- Fix batch reorder drops on selected items causing
  misinsertion (guard with early return)
- Set doc_index_topic_id to -1 explicitly on
  switchToDirectMode
- Add isDestroying guards after await in all async methods
  (apply, _doIndexAllTopics, addMissingTopicsToSection,
  loadIndexTopic)
- Clear existing _autoExpandTimer before creating new one
  on repeated sectionDragEnter
- Replace clearTimeout with cancel() from @ember/runloop
  for discourseLater timers
- CSS: merge danger-hover button selectors, merge
  --selected rules, fix cursor on section-title-label,
  add flex-shrink to batch-drag-handle, add
  prefers-reduced-motion media query
- Add tests for non-array JSON input and direct-to-topic
  mode section cleanup
megothss added 30 commits April 7, 2026 23:38
Give legacy category settings (enable_simplified_category_creation=false)
the same mode selection (none, topic, editor) and visual editor that the
new simplified flow already has.

- Extract DocIndexModeSelector: shared dropdown component used by both
  the new-flow tab and legacy settings
- Extract DocIndexModeState: shared state manager encapsulating mode
  tracking, switching with confirmation dialogs, topic loading, and
  topic validation
- Refactor DocCategoryIndexEditor to use ConditionalInElement for
  toolbar and a new @footerElement arg for footer teleporting
- Create DocIndexEditorModal: DModal wrapper with toolbar/footer
  targets and close guard for validation errors
- Rework DocCategorySettings: mode selector, topic chooser, and
  editor modal trigger replacing the old topic-only UI
- Add z-index overrides for DMenu dropdowns inside the modal
- Add system specs and page object methods for legacy flow
The first section's header is suppressed in the sidebar, so an empty
title is valid. This change:

- Skips empty-title validation for the first section (client + server)
- Shows a "Not collapsible (no title)" placeholder in the editor
- Prevents the first section from auto-entering edit mode or being
  deleted on cancel when its title is intentionally empty
- Disables the Apply button when validation errors exist
- Adds JS unit tests for the validation function and system tests
  for the editor behavior
…rm fields

Renames the transient form field from `_docIndexSections` to
`_docIndexEditorState` to clearly distinguish it from
`doc_index_sections` (the backend payload). Adds a JSDoc to
`_saveToTransientData()` explaining the purpose of each field and
why both must be committed after Apply.
Renames the auto-title badge from "auto" to "auto-title" to clearly
distinguish it from "auto-indexed". Unifies both item badges under a
single `__item-badge` CSS class with consistent pill styling.
- Align batch edit and wrench buttons to the right of the toolbar
- Add select all, invert, clear, and close buttons to the batch bar
  with tooltips on all buttons
- Select all/invert are context-dependent: operate on sections if any
  are selected, otherwise on items
- Style delete and clear-index buttons with danger-red color and
  danger background on hover (matching post menu pattern)
- Show confirmation dialog when exiting batch mode with active selection
- Remove 8 abandoned locale keys
The modal page object's `add_section` was missing the
fallback for the first section not auto-entering title
edit mode. Also update the "flash error" test to check
for a disabled Apply button instead, matching the current
behavior and the non-legacy spec.
Topics that change archetype (e.g., banner <-> regular) or
visibility (hidden <-> visible) were not automatically
added/removed from the auto-index sidebar.

- Listen to :topic_status_updated for visibility changes
- Add after_save callback to detect archetype changes
  (make_banner!, remove_banner!, PM conversions)
- Fix AddTopic#topic_qualifies to check archetype,
  consistent with Sync's qualifying query
- Fix has_auto_index_for_category? to actually filter
  by category_id (parameter was accepted but unused)
Add tests for topic lifecycle events (created, recovered,
trashed, destroyed), category filtering, and subcategory
support.

Fix has_auto_index_for_category? to check
auto_index_include_subcategories when matching via parent
category, avoiding no-op job enqueues for subcategory
topics when the flag is disabled.
The auto_index_include_subcategories setting had no UI.
Add a dropdown on the auto-index badge that reveals an
"Include subcategories" checkbox. The badge label updates
to reflect the current state.

Also trigger a resync when the setting changes, with a
confirmation dialog warning the user.
Add a "Resync topics on next save" button to the auto-index
badge dropdown. The button toggles a pending state reflected
in the badge label, and can be cancelled before saving.

The resync button is hidden when the subcategory setting has
been changed (since that already forces a resync). Toggling
subcategories back to the original value skips the
confirmation dialog.
Add IndexSaver specs for sync_auto_index_if_needed! with
force param. Add controller specs for force_sync param and
auto_index_include_subcategories triggering resync.

Add system specs for the badge dropdown: toggling include
subcategories, resync pending state, and subcategory topics
being included after apply. Add page object helpers for
badge dropdown interactions.
Convert IndexSaver, CategoryIndexManager, and IndexStructureRefresher
from plain Ruby classes to Discourse's Service::Base pattern with the
standard contract/model/policy/step/transaction DSL.

This brings consistency with the auto-indexer services that already
use Service::Base, simplifies the controller (result matchers replace
manual orchestration), and standardizes error handling across all
service entry points.

Key changes:
- IndexSaver: params contract, only_if for force_direct/sync,
  transaction-wrapped save, automatic auto-index sync
- CategoryIndexManager: only_if branching for remove/direct/topic modes
- IndexStructureRefresher: linear pipeline with policy guards
- IndexesController#update: uses service runner with result matchers
- All callers updated: category callbacks, initializers, job, rake task
Use #private fields where possible, extract shared
isAboveElement utility, remove unnecessary optional
chaining, simplify sidebar classNames getter, and
reorder class members per ESLint sort-class-members rule.
Add `doc_categories_index_editor` site setting as an experimental
upcoming change. When disabled, the "Editor" mode option is hidden
from the mode selector and backend endpoints reject new direct-mode
index creation. Categories already using the editor remain fully
functional so admins can manage or switch away from them.
The sync service previously loaded ALL qualifying topic IDs into memory
via an unbounded pluck, then used in-memory set operations for diffing.
Since a section holds at most 200 links, this was wasteful for large
categories.

Replace with two DB-level queries:
- compute_topics_to_add: uses a NOT IN subquery to find qualifying
  topics not already linked, capped at MAX_LINKS_PER_SECTION
- find_stale_links: uses a LEFT JOIN against topics to detect
  auto-indexed links whose topics no longer qualify (deleted,
  invisible, moved, etc.)
Limit the number of topics loaded by the `topics` action from 5000 to 50
to improve resource utilization and reduce memory overhead. This change
impacts categories with large volumes of topics, making the action faster
and less memory-intensive.

Assumption: The reduced limit aligns with expected use cases and does not
disrupt workflows heavily relying on large topic loads.
Replace per-topic find_by with a single WHERE IN query in
add_missing_topics, and skip cache clear + MessageBus publish
when no changes were made. Also remove stray `For` token.
Move update_subcategory_setting inside the transaction block so
it rolls back with save_sections on failure. Add safe navigation
for first_post in both report files to handle hard-deleted posts.
Reuse context[:sections_data] in determine_sync_needed instead
of re-parsing params. Add missing invalid_sections locale key.
Add :topic to the sidebar_links preload so topics are eager-loaded
for serialization. Remove unused Topic.clear_doc_categories_cache
class method. Rename validate_topic to ensure_valid_topic in
IndexStructureRefresher to reflect its destructive cleanup behavior.
Add tabindex="0" to drag handle spans for keyboard accessibility.
Delete unused doc-category-settings-form.gjs component. Remove
non-functional @computed decorator from sidebar panel. Deduplicate
validation error messages with Set. Merge duplicate CSS rule for
index tab header. Rename mismatched-category locale key to use
underscores.
New links and sections auto-entering edit mode now correctly
increment editingCount via onEditStateChange, preventing batch
mode toggle during editing. Add missing topic_id, topicTitle,
and autoTitle properties to links created by addMissingTopics
and indexAllTopics. Commit the subcategory setting form field
after successful apply to clear the dirty state.
Walk up the full ancestor chain in has_auto_index_for_category?
instead of checking only the immediate parent, supporting sites
with max_category_nesting > 2. Move auto-index remove before
the early return in handle_category_change so stale links are
cleaned up when a topic moves to a category where it is the
index topic.
Nullify direct-mode sentinel values (-1) in migration rollback
so the full unique index can be created. Simplify
matching_category_ids to avoid duplicate root category ID.
Use delete_all instead of destroy_all for stale link removal
since SidebarLink has no destroy callbacks.
…tion

Add controller auth tests for #update endpoint, job spec for
DocCategoriesAutoIndex, sidebar_structure unit tests, PM edge
case in refresher, auto-index category move tests, and JS
validation tests for empty sections, links, and deduplication.
Replace the imperative `editingCount` counter + `onEditStateChange`
callback with editing flags stored on section/link data objects and
a derived getter. Child constructors were updating the parent's
`editingCount` during render after `canToggleBatchMode` had already
read it, causing an Ember autotracking assertion failure.

Also adds a missing blank line before the `transaction` block in
`IndexSaver` to fix the Rubocop lint.
… in specs

When saving a category in topic mode, the frontend sends a null value
for doc_index_sections which arrives as an empty string. The
doc_index_sections callback then called IndexSaver, whose
not_topic_managed policy failed because the index had just been
created in topic mode by the preceding doc_index_topic_id callback.
This raised Discourse::InvalidAccess (403), rolling back the entire
transaction including the index creation.

Also adds the missing doc_categories_index_editor site setting to
auto_index_spec and legacy_spec, which were broken since the editor
mode was gated behind this setting.
Change the `impact` setting from "staff" to "admins" and introduce a new
`disallow_enabled_for_groups` option in `doc_categories_homepage` to ensure
fine-grain control over features. This aligns configuration with intended
permissions and improves clarity for administrators managing doc categories.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants