feat(portal): implement dynamic drawer/page navigation management
Replace hardcoded sidebar drawer configuration with admin-manageable dynamic system. Extend page_status.json with drawer definitions and page assignments, add drawer CRUD API endpoints, render portal sidebar via Jinja2 loops, and extend /admin/pages UI with drawer management. Fix multi-worker cache invalidation via mtime-based staleness detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
55
openspec/specs/drawer-management/spec.md
Normal file
55
openspec/specs/drawer-management/spec.md
Normal file
@@ -0,0 +1,55 @@
|
||||
## Purpose
|
||||
Define stable requirements for drawer-management.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
### Requirement: Admin SHALL be able to create drawers
|
||||
The system SHALL allow admin users to create new navigation drawers via API, specifying a name, an order value, and an optional `admin_only` flag.
|
||||
|
||||
#### Scenario: Create a new drawer
|
||||
- **WHEN** admin sends POST `/admin/api/drawers` with `{"name": "自訂分類", "order": 4}`
|
||||
- **THEN** the system SHALL create a new drawer with a generated kebab-case id and persist it to `page_status.json`
|
||||
|
||||
#### Scenario: Create drawer with duplicate name
|
||||
- **WHEN** admin sends POST `/admin/api/drawers` with a name that already exists
|
||||
- **THEN** the system SHALL return 409 Conflict with an error message
|
||||
|
||||
### Requirement: Admin SHALL be able to rename drawers
|
||||
The system SHALL allow admin users to update a drawer's name via API.
|
||||
|
||||
#### Scenario: Rename a drawer
|
||||
- **WHEN** admin sends PUT `/admin/api/drawers/<id>` with `{"name": "新名稱"}`
|
||||
- **THEN** the system SHALL update the drawer name and persist the change
|
||||
|
||||
### Requirement: Admin SHALL be able to reorder drawers
|
||||
The system SHALL allow admin users to change a drawer's sort order via API.
|
||||
|
||||
#### Scenario: Change drawer order
|
||||
- **WHEN** admin sends PUT `/admin/api/drawers/<id>` with `{"order": 2}`
|
||||
- **THEN** the system SHALL update the drawer order and the sidebar SHALL reflect the new order on next page load
|
||||
|
||||
### Requirement: Admin SHALL be able to delete empty drawers
|
||||
The system SHALL allow admin users to delete a drawer only when no pages are assigned to it.
|
||||
|
||||
#### Scenario: Delete an empty drawer
|
||||
- **WHEN** admin sends DELETE `/admin/api/drawers/<id>` and the drawer has no assigned pages
|
||||
- **THEN** the system SHALL remove the drawer from the configuration
|
||||
|
||||
#### Scenario: Attempt to delete a drawer with assigned pages
|
||||
- **WHEN** admin sends DELETE `/admin/api/drawers/<id>` and the drawer still has assigned pages
|
||||
- **THEN** the system SHALL return 409 Conflict with an error listing the assigned pages
|
||||
|
||||
### Requirement: Admin SHALL be able to list all drawers
|
||||
The system SHALL provide an API to retrieve all drawers with their metadata.
|
||||
|
||||
#### Scenario: List all drawers
|
||||
- **WHEN** admin sends GET `/admin/api/drawers`
|
||||
- **THEN** the system SHALL return all drawers sorted by their `order` field
|
||||
|
||||
### Requirement: Drawer management SHALL be accessible from the admin UI
|
||||
The existing `/admin/pages` page SHALL include a drawer management section where admin can create, rename, reorder, and delete drawers.
|
||||
|
||||
#### Scenario: Admin opens page management
|
||||
- **WHEN** admin navigates to the page management UI
|
||||
- **THEN** the UI SHALL display a drawer list with controls for add, rename, reorder, and delete
|
||||
42
openspec/specs/page-drawer-assignment/spec.md
Normal file
42
openspec/specs/page-drawer-assignment/spec.md
Normal file
@@ -0,0 +1,42 @@
|
||||
## Purpose
|
||||
Define stable requirements for page-drawer-assignment.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
### Requirement: Admin SHALL be able to assign a page to a drawer
|
||||
The system SHALL allow admin users to assign a page to a specific drawer by setting its `drawer_id` via the existing page update API.
|
||||
|
||||
#### Scenario: Assign page to a drawer
|
||||
- **WHEN** admin sends PUT `/admin/api/pages/<route>` with `{"drawer_id": "queries"}`
|
||||
- **THEN** the page SHALL be associated with the specified drawer
|
||||
|
||||
#### Scenario: Assign page to non-existent drawer
|
||||
- **WHEN** admin sends PUT `/admin/api/pages/<route>` with a `drawer_id` that does not exist
|
||||
- **THEN** the system SHALL return 400 Bad Request with an error message
|
||||
|
||||
### Requirement: Admin SHALL be able to set page order within a drawer
|
||||
The system SHALL allow admin users to control the display order of pages within a drawer.
|
||||
|
||||
#### Scenario: Set page order
|
||||
- **WHEN** admin sends PUT `/admin/api/pages/<route>` with `{"order": 3}`
|
||||
- **THEN** the page SHALL be displayed at position 3 within its drawer on next portal load
|
||||
|
||||
### Requirement: Pages without a drawer assignment SHALL NOT appear in the sidebar
|
||||
Pages that have no `drawer_id` (e.g., sub-pages like `/wip-detail`, `/hold-detail`) SHALL NOT be rendered in the portal sidebar, but SHALL remain accessible via their direct routes.
|
||||
|
||||
#### Scenario: Sub-page without drawer assignment
|
||||
- **WHEN** a page exists in `page_status.json` without a `drawer_id`
|
||||
- **THEN** the page SHALL NOT appear in any sidebar drawer
|
||||
- **THEN** the page SHALL still be accessible via its direct URL
|
||||
|
||||
### Requirement: Page drawer assignment SHALL be configurable from the admin UI
|
||||
The existing `/admin/pages` page table SHALL include a drawer assignment dropdown and order controls for each page.
|
||||
|
||||
#### Scenario: Admin changes page drawer via UI
|
||||
- **WHEN** admin selects a different drawer from the dropdown for a page
|
||||
- **THEN** the UI SHALL call the page update API with the new `drawer_id`
|
||||
|
||||
#### Scenario: Admin clears drawer assignment via UI
|
||||
- **WHEN** admin selects "未分類" (unassigned) from the dropdown
|
||||
- **THEN** the page's `drawer_id` SHALL be removed and the page SHALL no longer appear in the sidebar
|
||||
@@ -5,15 +5,41 @@ Define stable requirements for portal-drawer-navigation.
|
||||
|
||||
|
||||
### Requirement: Portal Navigation SHALL Group Entries by Functional Drawers
|
||||
The portal SHALL group navigation entries into functional drawers: reports, queries, and development tools.
|
||||
The portal SHALL group navigation entries into functional drawers as defined in the `drawers` configuration of `page_status.json`, rendered dynamically via Jinja2 loop instead of hardcoded HTML.
|
||||
|
||||
#### Scenario: Drawer grouping visibility
|
||||
- **WHEN** users open the portal
|
||||
- **THEN** report pages and query pages SHALL appear in separate drawer groups
|
||||
- **THEN** the sidebar SHALL display drawers in the order defined by each drawer's `order` field
|
||||
- **THEN** each drawer SHALL show only the pages assigned to it via `drawer_id`, sorted by each page's `order` field
|
||||
|
||||
#### Scenario: Admin-only drawer visibility
|
||||
- **WHEN** a drawer has `admin_only: true` and the current user is not admin
|
||||
- **THEN** the drawer and all its pages SHALL NOT be rendered in the sidebar
|
||||
|
||||
#### Scenario: Empty drawer visibility
|
||||
- **WHEN** a drawer has no visible pages (all filtered out by `can_view_page()`)
|
||||
- **THEN** the drawer group title SHALL NOT be rendered
|
||||
|
||||
### Requirement: Existing Page Behavior SHALL Remain Compatible
|
||||
The portal navigation refactor SHALL preserve existing target routes and lazy-load behavior for content frames.
|
||||
|
||||
#### Scenario: Route continuity
|
||||
- **WHEN** a user selects an existing page entry from the new drawer
|
||||
- **WHEN** a user selects an existing page entry from a dynamically rendered drawer
|
||||
- **THEN** the corresponding original route SHALL be loaded without changing page business logic behavior
|
||||
|
||||
#### Scenario: Iframe lazy-load continuity
|
||||
- **WHEN** a sidebar item is clicked for the first time
|
||||
- **THEN** the iframe SHALL lazy-load its content from the page's route, consistent with current behavior
|
||||
|
||||
### Requirement: First-run migration SHALL populate drawer configuration automatically
|
||||
When `page_status.json` does not contain a `drawers` field, the system SHALL automatically create the default drawer structure matching the current hardcoded layout and assign existing pages to their corresponding drawers.
|
||||
|
||||
#### Scenario: First startup after deployment
|
||||
- **WHEN** the application starts and `page_status.json` has no `drawers` field
|
||||
- **THEN** the system SHALL create three default drawers (報表類, 查詢類, 開發工具)
|
||||
- **THEN** the system SHALL assign each existing page to its historically correct drawer
|
||||
- **THEN** the system SHALL persist the updated configuration immediately
|
||||
|
||||
#### Scenario: Subsequent startup
|
||||
- **WHEN** the application starts and `page_status.json` already contains a `drawers` field
|
||||
- **THEN** the system SHALL NOT modify the existing drawer configuration
|
||||
|
||||
Reference in New Issue
Block a user