chore: sync GovOPlaN module split state
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
webui/node_modules/
|
||||||
|
webui/dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.component-test-build/
|
||||||
|
.module-test-build/
|
||||||
|
.policy-test-build/
|
||||||
|
.template-preview-test-build/
|
||||||
|
.import-test-build/
|
||||||
|
webui/.component-test-build/
|
||||||
|
webui/.module-test-build/
|
||||||
|
webui/.policy-test-build/
|
||||||
|
webui/.template-preview-test-build/
|
||||||
|
webui/.import-test-build/
|
||||||
@@ -42,3 +42,9 @@ Frontend package:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`.
|
Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`.
|
||||||
|
|
||||||
|
## Concept documents
|
||||||
|
|
||||||
|
- `docs/DOCUMENTATION_LAYER_CONCEPT.md` defines the configured, available, and evidence documentation model.
|
||||||
|
- `docs/INSTANCE_AWARE_DOCUMENTATION.md` defines the runtime context and condition model.
|
||||||
|
- `docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md` defines the workflow, structure/reference, and design-pattern documentation experience.
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# Docs Backlog
|
|
||||||
|
|
||||||
## P1 Immediate
|
|
||||||
|
|
||||||
- [ ] Define the `docs` backend manifest, permissions, module metadata, and route contribution.
|
|
||||||
- [ ] Define documentation source DTOs for manifests, routes, capabilities, policies, configuration packages, and wiki-backed sources.
|
|
||||||
- [ ] Implement configured, available, and evidence classification for documentation entries.
|
|
||||||
- [ ] Add API routes for current-context documentation lookup and source inspection.
|
|
||||||
- [ ] Create focused tests for role-aware and configuration-aware documentation filtering.
|
|
||||||
|
|
||||||
## P2 Module Integration
|
|
||||||
|
|
||||||
- [ ] Define module manifest fields for documentation contributions.
|
|
||||||
- [ ] Define capability metadata fields that the docs module can render without importing provider internals.
|
|
||||||
- [ ] Integrate release catalog and configuration package descriptions as documentation sources.
|
|
||||||
- [ ] Link repository docs and synced wiki pages as durable evidence sources.
|
|
||||||
|
|
||||||
## P2 WebUI
|
|
||||||
|
|
||||||
- [ ] Build the documentation browser route contribution and navigation metadata.
|
|
||||||
- [ ] Build configured-system, available-feature, and evidence views.
|
|
||||||
- [ ] Build route-aware help entry points from the shared WebUI shell.
|
|
||||||
|
|
||||||
## P3 Governance
|
|
||||||
|
|
||||||
- [ ] Document ownership rules for runtime docs, repository docs, Gitea wiki pages, and active Gitea issues.
|
|
||||||
- [ ] Document privacy and permission boundaries for role-aware documentation.
|
|
||||||
293
docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md
Normal file
293
docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Documentation Experience Concept
|
||||||
|
|
||||||
|
GovOPlaN Docs should support three ways of understanding the configured
|
||||||
|
platform:
|
||||||
|
|
||||||
|
- outcome-oriented workflow guidance
|
||||||
|
- structure-oriented reference documentation
|
||||||
|
- shared design-pattern explanations
|
||||||
|
|
||||||
|
These are different reading modes over the same configured-system knowledge.
|
||||||
|
They should link to each other, respect user and admin perspectives, and keep
|
||||||
|
the current installation as the primary source of truth.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
The docs experience should answer these questions first:
|
||||||
|
|
||||||
|
- What can I do in this system?
|
||||||
|
- Which steps do I take to finish this task?
|
||||||
|
- What does this screen or field mean?
|
||||||
|
- Why does the interface behave this way?
|
||||||
|
- For admins: which API, permission, policy, or configuration source backs this
|
||||||
|
screen?
|
||||||
|
|
||||||
|
It should not start from a full product manual. The current tenant, installed
|
||||||
|
modules, enabled routes, permissions, and configuration decide what is shown as
|
||||||
|
the default path.
|
||||||
|
|
||||||
|
Tracking issue: `add-ideas/govoplan-docs#15`.
|
||||||
|
|
||||||
|
## Editorial Pillars
|
||||||
|
|
||||||
|
### Workflow Guidance
|
||||||
|
|
||||||
|
Workflow docs are written from the reader's intent:
|
||||||
|
|
||||||
|
- "I want to create an API key."
|
||||||
|
- "I want to add a user to a group."
|
||||||
|
- "I want to configure a reusable mail sender."
|
||||||
|
- "I want to understand why an option is disabled."
|
||||||
|
|
||||||
|
Each workflow topic should have:
|
||||||
|
|
||||||
|
- outcome
|
||||||
|
- audience, usually user or admin
|
||||||
|
- prerequisites and blockers
|
||||||
|
- steps using visible interface labels
|
||||||
|
- expected result
|
||||||
|
- verification or review step when relevant
|
||||||
|
- related structure topics
|
||||||
|
- related design patterns
|
||||||
|
|
||||||
|
User workflow topics avoid API paths, internal module ids, raw permission names,
|
||||||
|
and server configuration unless the user must act on them. They should explain
|
||||||
|
who can help when a feature is not available.
|
||||||
|
|
||||||
|
Admin workflow topics can include technical context: API endpoints, required
|
||||||
|
permissions, policy sources, configuration keys, migration or audit effects, and
|
||||||
|
operator escalation paths.
|
||||||
|
|
||||||
|
### Structure Reference
|
||||||
|
|
||||||
|
Structure docs describe what is visible on a screen and how it maps to platform
|
||||||
|
contracts.
|
||||||
|
|
||||||
|
For users, structure reference is a field glossary:
|
||||||
|
|
||||||
|
- screen section
|
||||||
|
- visible label
|
||||||
|
- short plain-language meaning
|
||||||
|
- allowed values or validation
|
||||||
|
- where the value appears later
|
||||||
|
- common mistakes or limits
|
||||||
|
|
||||||
|
For admins, structure reference adds technical columns:
|
||||||
|
|
||||||
|
- route and module
|
||||||
|
- API method and path
|
||||||
|
- request or response field
|
||||||
|
- configuration key or policy field
|
||||||
|
- permission or scope
|
||||||
|
- provenance or inheritance rule
|
||||||
|
- audit/event implication
|
||||||
|
- safety or approval requirement
|
||||||
|
|
||||||
|
Structure docs should be linked from workflow steps and from inline field help.
|
||||||
|
They should also link back to the workflows where the field is commonly used.
|
||||||
|
|
||||||
|
### Design Patterns
|
||||||
|
|
||||||
|
Pattern docs explain repeated platform behavior once, then link to examples.
|
||||||
|
They are not implementation tutorials; they explain what a reader should expect
|
||||||
|
when they see a familiar affordance.
|
||||||
|
|
||||||
|
Initial pattern topics should include:
|
||||||
|
|
||||||
|
- field help marker next to labels
|
||||||
|
- disabled action and blocker explanation
|
||||||
|
- advanced options panel
|
||||||
|
- guided wizard and review step
|
||||||
|
- policy provenance and effective value rows
|
||||||
|
- status badges and problem lists
|
||||||
|
- in-app help menu and route-specific help context
|
||||||
|
- evidence, available, and configured documentation layers
|
||||||
|
|
||||||
|
The field help marker is the immediate example. In the app, `FieldLabel` can
|
||||||
|
render `InlineHelp` beside form labels, and `helpForFieldLabel` currently maps
|
||||||
|
known labels to short help text. The docs layer should treat this as a pattern:
|
||||||
|
brief local explanation beside the field, with a link to the fuller structure or
|
||||||
|
workflow topic when more detail is needed.
|
||||||
|
|
||||||
|
## Audience Model
|
||||||
|
|
||||||
|
The docs source model may be shared, but presentation must be audience-specific.
|
||||||
|
|
||||||
|
### User View
|
||||||
|
|
||||||
|
User docs should be calm, task-focused, and plain-language:
|
||||||
|
|
||||||
|
- start from "I want to..."
|
||||||
|
- use current interface labels
|
||||||
|
- explain limits and unavailable options without exposing internals
|
||||||
|
- show who can change configuration or permissions
|
||||||
|
- link to short field explanations and general patterns
|
||||||
|
|
||||||
|
User docs should not require knowledge of APIs, scopes, module ids, deployment
|
||||||
|
configuration, or policy source chains.
|
||||||
|
|
||||||
|
### Admin View
|
||||||
|
|
||||||
|
Admin docs should still be workflow-centered, but they may be technical:
|
||||||
|
|
||||||
|
- start from tenant, module, access, policy, and operator tasks
|
||||||
|
- expose API endpoints, DTO fields, permissions, module ids, capabilities, and
|
||||||
|
configuration keys
|
||||||
|
- show route and field mappings
|
||||||
|
- explain provenance, inheritance, audit, and approval behavior
|
||||||
|
- include evidence for installed-but-disabled and optional capabilities
|
||||||
|
|
||||||
|
Admin docs are the right place to map an interface field to the API field and
|
||||||
|
explain operational consequences.
|
||||||
|
|
||||||
|
## Presentation Shape
|
||||||
|
|
||||||
|
The docs module should remain part of the platform because it needs the current
|
||||||
|
configuration, registry, permissions, modules, routes, and policy state. The
|
||||||
|
presentation should still feel like a documentation interface, not like another
|
||||||
|
business module.
|
||||||
|
|
||||||
|
The recommended shape is:
|
||||||
|
|
||||||
|
- keep the `docs` backend module and configured-system API
|
||||||
|
- expose a full-page Help Center route served by the same server
|
||||||
|
- use lighter documentation chrome instead of normal module-workspace density
|
||||||
|
- keep route-aware entry points from the app shell, help menu, field help, and
|
||||||
|
admin screens
|
||||||
|
- allow direct links such as `/docs?type=user&topic=...` or
|
||||||
|
`/docs?type=admin&route=...`
|
||||||
|
- label the content as documentation for "this system" rather than as a generic
|
||||||
|
marketing or product manual
|
||||||
|
|
||||||
|
This preserves third-person reference behavior while keeping the runtime
|
||||||
|
benefits of a docs module. The server can still filter and explain content based
|
||||||
|
on the actual installation.
|
||||||
|
|
||||||
|
## Navigation Model
|
||||||
|
|
||||||
|
The Help Center should have primary modes:
|
||||||
|
|
||||||
|
- Workflows
|
||||||
|
- Reference
|
||||||
|
- Patterns
|
||||||
|
- This system
|
||||||
|
|
||||||
|
`This system` is the configured/available/evidence view that already exists in
|
||||||
|
the first docs module slice. It should remain useful for admins and diagnostics,
|
||||||
|
but normal users should mostly enter through Workflows or contextual help.
|
||||||
|
|
||||||
|
Each topic should support stable anchors and related-topic links:
|
||||||
|
|
||||||
|
- workflow topics link to fields and patterns
|
||||||
|
- field/reference topics link to workflows and API details when the reader is an
|
||||||
|
admin
|
||||||
|
- pattern topics link to examples and the components that implement them
|
||||||
|
- unavailable topics link to the blocker and the actor who can resolve it
|
||||||
|
|
||||||
|
## Source Contract Direction
|
||||||
|
|
||||||
|
The current `DocumentationTopic` contract can carry the first version through
|
||||||
|
`metadata`, `documentation_types`, `conditions`, `links`, `related_modules`, and
|
||||||
|
`unlocks`. As the docs experience matures, typed source DTOs should be added for
|
||||||
|
clarity.
|
||||||
|
|
||||||
|
Recommended topic kinds:
|
||||||
|
|
||||||
|
- `workflow`
|
||||||
|
- `reference`
|
||||||
|
- `pattern`
|
||||||
|
- `system`
|
||||||
|
|
||||||
|
Recommended workflow metadata:
|
||||||
|
|
||||||
|
- `outcome`
|
||||||
|
- `steps`
|
||||||
|
- `prerequisites`
|
||||||
|
- `result`
|
||||||
|
- `verification`
|
||||||
|
- `related_field_ids`
|
||||||
|
- `related_pattern_ids`
|
||||||
|
|
||||||
|
Recommended reference metadata:
|
||||||
|
|
||||||
|
- `route`
|
||||||
|
- `screen`
|
||||||
|
- `section`
|
||||||
|
- `fields`
|
||||||
|
- `api`
|
||||||
|
- `permissions`
|
||||||
|
- `configuration_keys`
|
||||||
|
- `policy_sources`
|
||||||
|
|
||||||
|
Recommended field metadata:
|
||||||
|
|
||||||
|
- `field_id`
|
||||||
|
- `label`
|
||||||
|
- `user_description`
|
||||||
|
- `admin_description`
|
||||||
|
- `api_field`
|
||||||
|
- `api_path`
|
||||||
|
- `configuration_key`
|
||||||
|
- `permission_scope`
|
||||||
|
- `validation`
|
||||||
|
- `provenance`
|
||||||
|
- `audit_event`
|
||||||
|
|
||||||
|
Recommended pattern metadata:
|
||||||
|
|
||||||
|
- `pattern_id`
|
||||||
|
- `purpose`
|
||||||
|
- `when_used`
|
||||||
|
- `user_explanation`
|
||||||
|
- `admin_explanation`
|
||||||
|
- `component_refs`
|
||||||
|
- `related_topics`
|
||||||
|
|
||||||
|
Feature modules should contribute this data through manifests, runtime providers,
|
||||||
|
route metadata, generated docs, or typed DTOs. The docs module renders and
|
||||||
|
links the material; it should not import feature-module internals.
|
||||||
|
|
||||||
|
## Authoring Rules
|
||||||
|
|
||||||
|
Workflow topics:
|
||||||
|
|
||||||
|
- use visible UI labels
|
||||||
|
- keep each step actionable
|
||||||
|
- mention blockers where the user would encounter them
|
||||||
|
- link to field/reference topics instead of repeating field tables
|
||||||
|
|
||||||
|
Reference topics:
|
||||||
|
|
||||||
|
- keep user columns short
|
||||||
|
- put API, permission, and configuration mapping in the admin view
|
||||||
|
- identify generated or runtime-derived values
|
||||||
|
- avoid exposing secrets or raw policy payloads
|
||||||
|
|
||||||
|
Pattern topics:
|
||||||
|
|
||||||
|
- explain the pattern once
|
||||||
|
- link to current examples
|
||||||
|
- distinguish user meaning from admin/operator meaning
|
||||||
|
- stay stable even when individual screens change
|
||||||
|
|
||||||
|
## Implementation Path
|
||||||
|
|
||||||
|
1. Keep the existing context API and add topic kind metadata.
|
||||||
|
2. Add docs-topic grouping for workflows, reference, patterns, and system
|
||||||
|
diagnostics.
|
||||||
|
3. Add backend DTOs for field/reference metadata after the metadata shape is
|
||||||
|
proven with one or two modules.
|
||||||
|
4. Rework the WebUI into a Help Center route with audience mode, search,
|
||||||
|
contextual anchors, and the four navigation modes.
|
||||||
|
5. Connect in-app help entry points to docs topics: help menu, route context,
|
||||||
|
inline field help, disabled-action blockers, and admin reference tables.
|
||||||
|
6. Seed access-module content first because users, groups, roles, sessions, and
|
||||||
|
API keys have clear user/admin splits and concrete API mappings.
|
||||||
|
7. Add pattern topics for existing core UI behavior, starting with field help,
|
||||||
|
blockers, advanced panels, wizards, and policy provenance.
|
||||||
|
8. Add tests for classification, audience filtering, topic linking, and route or
|
||||||
|
field anchor generation.
|
||||||
|
|
||||||
|
The first shippable milestone should be small: one workflow, one structure
|
||||||
|
reference, and one design-pattern topic for access administration, all filtered
|
||||||
|
by the current docs context and visible in the Help Center.
|
||||||
@@ -6,6 +6,11 @@ GovOPlaN Docs provides a documentation layer that starts with the configured sys
|
|||||||
|
|
||||||
Only after that should the documentation provide evidence about what else might be possible: installed-but-disabled features, optional modules, extension points, product capabilities, or examples from other configurations.
|
Only after that should the documentation provide evidence about what else might be possible: installed-but-disabled features, optional modules, extension points, product capabilities, or examples from other configurations.
|
||||||
|
|
||||||
|
The editorial experience is defined in
|
||||||
|
`docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md`: workflow guidance, structure
|
||||||
|
reference, and shared design-pattern explanations are separate reading modes
|
||||||
|
over this configured-system model.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
Traditional product documentation describes the full system. That creates avoidable complexity in configurable public-sector platforms because most users only need a slice of the system:
|
Traditional product documentation describes the full system. That creates avoidable complexity in configurable public-sector platforms because most users only need a slice of the system:
|
||||||
@@ -71,6 +76,7 @@ Evidence should be discoverable, but it should not be the primary reading path f
|
|||||||
The docs module should consume documentation sources through explicit contracts:
|
The docs module should consume documentation sources through explicit contracts:
|
||||||
|
|
||||||
- module manifest documentation metadata
|
- module manifest documentation metadata
|
||||||
|
- module manifest runtime documentation providers
|
||||||
- route metadata and page-level help references
|
- route metadata and page-level help references
|
||||||
- capability metadata
|
- capability metadata
|
||||||
- configuration-package descriptions
|
- configuration-package descriptions
|
||||||
@@ -80,6 +86,113 @@ The docs module should consume documentation sources through explicit contracts:
|
|||||||
|
|
||||||
Feature modules should contribute documentation metadata without the docs module importing their internals.
|
Feature modules should contribute documentation metadata without the docs module importing their internals.
|
||||||
|
|
||||||
|
### Module-Owned Documentation
|
||||||
|
|
||||||
|
Documentation is distributed over the modules that own the behavior. A module can
|
||||||
|
register durable topics directly in its `ModuleManifest.documentation` tuple.
|
||||||
|
Use this for stable explanations such as the module purpose, common workflows,
|
||||||
|
policy hierarchy, route meaning, and links to public docs or repository docs.
|
||||||
|
|
||||||
|
When the text depends on active configuration, a module registers a provider in
|
||||||
|
`ModuleManifest.documentation_providers`. The provider receives a
|
||||||
|
`DocumentationContext` with the active registry, principal, settings, and a
|
||||||
|
database session when available. The module can then describe the effective
|
||||||
|
state without the docs module importing feature internals.
|
||||||
|
|
||||||
|
For example, `govoplan-mail` contributes static documentation for reusable mail
|
||||||
|
profiles and a runtime tenant policy topic. If the effective tenant mail policy
|
||||||
|
limits sending to approved profile ids and disables user, group, and
|
||||||
|
campaign-local profiles, the runtime docs state that users can choose approved
|
||||||
|
profiles but cannot bring arbitrary SMTP or IMAP servers at lower scopes. If the
|
||||||
|
policy allows lower scopes, the same topic explains which scopes may define
|
||||||
|
profiles and whether credential inheritance is locked.
|
||||||
|
|
||||||
|
Runtime providers must avoid leaking secrets. They should summarize posture,
|
||||||
|
counts, source provenance, and enabling conditions rather than exposing
|
||||||
|
credentials, hostnames, profile ids, or raw policy payloads unless the route is
|
||||||
|
explicitly intended for that level of detail.
|
||||||
|
|
||||||
|
### Conditions And Unlocks
|
||||||
|
|
||||||
|
Documentation topics can declare conditions:
|
||||||
|
|
||||||
|
- required modules
|
||||||
|
- one-of module alternatives
|
||||||
|
- modules that must be absent
|
||||||
|
- required capabilities
|
||||||
|
- required scopes or one-of scope alternatives
|
||||||
|
- configuration keys that influence the topic
|
||||||
|
|
||||||
|
The docs API classifies satisfied topics into their requested layer. Unsatisfied
|
||||||
|
topics remain visible as available or evidence documentation when safe, with a
|
||||||
|
reason such as a missing module, capability, or scope.
|
||||||
|
|
||||||
|
Topics can also declare related modules and unlock notes. This lets a module
|
||||||
|
state that additional behavior becomes available when another module is
|
||||||
|
installed. Runtime docs should link to the configured local behavior first and
|
||||||
|
then to public module documentation for broader examples.
|
||||||
|
|
||||||
|
### Admin And User Documentation
|
||||||
|
|
||||||
|
GovOPlaN has two documentation presentations over the same source model.
|
||||||
|
|
||||||
|
Admin documentation is allowed to be technical. It can expose installed module
|
||||||
|
ids, route contributions, API paths, permissions, capability names, server
|
||||||
|
configuration keys, policy source chains, migration notes, and operator
|
||||||
|
evidence. This is the right place to explain exactly why a setting is available,
|
||||||
|
which API applies it, and which configuration key or policy source controls it.
|
||||||
|
|
||||||
|
User documentation should be written for people without technical background. It
|
||||||
|
should explain what the user can do, what they cannot do, and who to ask when a
|
||||||
|
needed option is missing. It should avoid route names, API calls, raw policy
|
||||||
|
payloads, internal module ids, and server configuration unless that information
|
||||||
|
is genuinely meaningful to the user.
|
||||||
|
|
||||||
|
Each `DocumentationTopic` declares `documentation_types`, currently `admin`,
|
||||||
|
`user`, or both. Runtime providers receive the requested type in
|
||||||
|
`DocumentationContext.documentation_type`, so they can generate different text
|
||||||
|
from the same effective configuration. For example, mail admin docs can mention
|
||||||
|
mail profile policy sources and API endpoints, while user docs can simply say
|
||||||
|
whether the user may choose an approved mail profile or add a local mail server.
|
||||||
|
|
||||||
|
The first presentation rule is:
|
||||||
|
|
||||||
|
- admin docs show technical context and evidence tables
|
||||||
|
- user docs show plain-language topics, examples, limits, and escalation paths
|
||||||
|
- both presentations are filtered by installed modules, active configuration,
|
||||||
|
permissions, and safe disclosure rules
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
Localization should be prepared before substantial documentation text is added.
|
||||||
|
Every durable topic should have a stable `i18n_key`. The English source text can
|
||||||
|
remain in the manifest or provider, but topics may include locale-specific
|
||||||
|
`translations` for `title`, `summary`, and `body`.
|
||||||
|
|
||||||
|
The docs API accepts a requested locale and falls back in this order:
|
||||||
|
|
||||||
|
- exact locale, such as `de-AT`
|
||||||
|
- base language, such as `de`
|
||||||
|
- English translation if present
|
||||||
|
- source text
|
||||||
|
|
||||||
|
This keeps the first implementation simple while avoiding a later rewrite of
|
||||||
|
every module documentation contribution. A later dedicated translation catalog
|
||||||
|
can still use the same `i18n_key` values.
|
||||||
|
|
||||||
|
### Public Documentation For Missing Modules
|
||||||
|
|
||||||
|
The configured-system docs should not explain every uninstalled module in full.
|
||||||
|
General documentation for the product and modules not installed in the current
|
||||||
|
system belongs on `https://govplan.add-ideas.de/`, served by `govoplan-web`.
|
||||||
|
|
||||||
|
Runtime documentation should link to the public page when:
|
||||||
|
|
||||||
|
- an optional module is not installed
|
||||||
|
- a feature is unlocked by adding another module
|
||||||
|
- an example uses modules that are not active locally
|
||||||
|
- a user needs conceptual background beyond the configured system
|
||||||
|
|
||||||
## Rendering Model
|
## Rendering Model
|
||||||
|
|
||||||
The runtime docs UI should be context-aware:
|
The runtime docs UI should be context-aware:
|
||||||
@@ -111,6 +224,7 @@ The first implementation should define:
|
|||||||
- a configured/available/evidence classifier
|
- a configured/available/evidence classifier
|
||||||
- a minimal API for current-context documentation
|
- a minimal API for current-context documentation
|
||||||
- a WebUI route contribution for the documentation browser
|
- a WebUI route contribution for the documentation browser
|
||||||
|
- a provider contract for configuration-derived runtime topics
|
||||||
|
|
||||||
The first UI can be simple:
|
The first UI can be simple:
|
||||||
|
|
||||||
@@ -119,5 +233,6 @@ The first UI can be simple:
|
|||||||
- active policies
|
- active policies
|
||||||
- available-but-disabled evidence
|
- available-but-disabled evidence
|
||||||
- source links to wiki or repository docs
|
- source links to wiki or repository docs
|
||||||
|
- module documentation topics with links and unlock notes
|
||||||
|
|
||||||
Once the source model is stable, feature modules can add richer documentation contributions through their manifests and capabilities.
|
Once the source model is stable, feature modules can add richer documentation contributions through their manifests and capabilities.
|
||||||
|
|||||||
70
docs/INSTANCE_AWARE_DOCUMENTATION.md
Normal file
70
docs/INSTANCE_AWARE_DOCUMENTATION.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Instance-Aware Documentation
|
||||||
|
|
||||||
|
The docs module renders documentation for the current GovOPlaN instance instead
|
||||||
|
of showing a static product manual.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
The documentation context is built from:
|
||||||
|
|
||||||
|
- enabled module manifests
|
||||||
|
- frontend routes and navigation metadata
|
||||||
|
- module permissions and the current principal's granted scopes
|
||||||
|
- documentation topics contributed by manifests
|
||||||
|
- runtime documentation providers contributed by modules
|
||||||
|
- optional dependencies and declared capabilities
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
| Layer | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `always` | Generic platform concepts that are useful in every deployment. |
|
||||||
|
| `configured` | Documentation for installed modules/features visible in this instance. |
|
||||||
|
| `available` | Installed features hidden by missing permissions or unmet conditions. |
|
||||||
|
| `evidence` | Hints for unavailable optional modules, missing capabilities, or external evidence sources. |
|
||||||
|
|
||||||
|
Normal user documentation should emphasize `always` and `configured` content.
|
||||||
|
Admin documentation may show all layers plus route, permission, module, and
|
||||||
|
capability diagnostics.
|
||||||
|
|
||||||
|
## Conditions
|
||||||
|
|
||||||
|
Documentation topics can declare:
|
||||||
|
|
||||||
|
- required modules
|
||||||
|
- any-of module sets
|
||||||
|
- modules that must be absent
|
||||||
|
- required capabilities
|
||||||
|
- required scopes
|
||||||
|
- any-of scope sets
|
||||||
|
- related configuration keys
|
||||||
|
|
||||||
|
The API returns both a human-readable `reason` and structured `blockers`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reason": "missing modules: mail; missing scopes: mail:profile:read",
|
||||||
|
"blockers": {
|
||||||
|
"modules": ["mail"],
|
||||||
|
"capabilities": [],
|
||||||
|
"scopes": ["mail:profile:read"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The WebUI must display the reason and, when present, the blocker lists. This
|
||||||
|
prevents dead-end instructions: users see whether a feature is unavailable
|
||||||
|
because a module is missing, a capability is not registered, or their current
|
||||||
|
permissions do not expose it.
|
||||||
|
|
||||||
|
## Module Guidance
|
||||||
|
|
||||||
|
Module docs should not hard-code assumptions about sibling modules. A module
|
||||||
|
should contribute conditional topics such as:
|
||||||
|
|
||||||
|
- "Send selected file by mail" requiring `files` and `mail`
|
||||||
|
- "Campaign attachments from storage" requiring `campaigns` and `files`
|
||||||
|
- "Mailbox diagnostics" requiring `mail.devMailbox`
|
||||||
|
|
||||||
|
The docs module remains the renderer. Feature modules own their subject matter
|
||||||
|
and describe unlocks through manifest metadata.
|
||||||
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/docs-webui",
|
||||||
|
"version": "0.1.6",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "webui/src/index.ts",
|
||||||
|
"module": "webui/src/index.ts",
|
||||||
|
"types": "webui/src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./webui/src/index.ts",
|
||||||
|
"import": "./webui/src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"webui/src",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.6",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
24
pyproject.toml
Normal file
24
pyproject.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "govoplan-docs"
|
||||||
|
version = "0.1.6"
|
||||||
|
description = "GovOPlaN documentation module for configured-system, available, and evidence documentation."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
authors = [{ name = "GovOPlaN" }]
|
||||||
|
dependencies = [
|
||||||
|
"govoplan-core>=0.1.6",
|
||||||
|
"govoplan-access>=0.1.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
govoplan_docs = ["py.typed"]
|
||||||
|
|
||||||
|
[project.entry-points."govoplan.modules"]
|
||||||
|
docs = "govoplan_docs.backend.manifest:get_manifest"
|
||||||
5
src/govoplan_docs/__init__.py
Normal file
5
src/govoplan_docs/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""GovOPlaN configured-system documentation module."""
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
|
__version__ = "0.1.6"
|
||||||
1
src/govoplan_docs/backend/__init__.py
Normal file
1
src/govoplan_docs/backend/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Backend integration for the GovOPlaN docs module."""
|
||||||
1
src/govoplan_docs/backend/api/__init__.py
Normal file
1
src/govoplan_docs/backend/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Docs API package."""
|
||||||
1
src/govoplan_docs/backend/api/v1/__init__.py
Normal file
1
src/govoplan_docs/backend/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Docs API v1 package."""
|
||||||
556
src/govoplan_docs/backend/api/v1/routes.py
Normal file
556
src/govoplan_docs/backend/api/v1/routes.py
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||||
|
|
||||||
|
from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope, require_any_scope
|
||||||
|
from govoplan_core.core.modules import (
|
||||||
|
DocumentationCondition,
|
||||||
|
DocumentationContext,
|
||||||
|
DocumentationLink,
|
||||||
|
DocumentationTopic,
|
||||||
|
DocumentationType,
|
||||||
|
FrontendRoute,
|
||||||
|
ModuleManifest,
|
||||||
|
NavItem,
|
||||||
|
PermissionDefinition,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
|
from govoplan_core.db.session import get_database
|
||||||
|
|
||||||
|
from govoplan_docs.backend.manifest import DOCS_READ_SCOPES
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/docs", tags=["docs"])
|
||||||
|
|
||||||
|
TOPIC_KINDS = ("workflow", "reference", "pattern", "system")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/context")
|
||||||
|
def docs_context(
|
||||||
|
request: Request,
|
||||||
|
documentation_type: DocumentationType = Query(default="admin", alias="type", pattern="^(admin|user)$"),
|
||||||
|
locale: str | None = Query(default=None, min_length=2, max_length=20),
|
||||||
|
principal: ApiPrincipal = Depends(require_any_scope(*DOCS_READ_SCOPES)),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
registry = _registry(request)
|
||||||
|
resolved_locale = _preferred_locale(request, locale)
|
||||||
|
modules = [_module_payload(manifest) for manifest in registry.manifests()]
|
||||||
|
permissions = [_permission_payload(permission, principal) for permission in registry.permissions()]
|
||||||
|
route_items = _route_items(registry.manifests(), principal)
|
||||||
|
visible_routes = [item for item in route_items if item["visible"]]
|
||||||
|
available_routes = [item for item in route_items if not item["visible"]]
|
||||||
|
optional_modules = _optional_module_evidence(registry.manifests())
|
||||||
|
documentation_layers = _documentation_layers(request, registry, principal, documentation_type=documentation_type, locale=resolved_locale)
|
||||||
|
documentation_count = sum(len(layer) for layer in documentation_layers.values())
|
||||||
|
topic_groups = _documentation_topic_groups(documentation_layers)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"actor": {
|
||||||
|
"tenant_id": principal.tenant_id,
|
||||||
|
"user_id": principal.user.id,
|
||||||
|
"scope_count": len(principal.scopes),
|
||||||
|
"documentation_type": documentation_type,
|
||||||
|
"locale": resolved_locale,
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"module_count": len(modules),
|
||||||
|
"visible_route_count": len(visible_routes),
|
||||||
|
"available_route_count": len(available_routes),
|
||||||
|
"permission_count": len(permissions),
|
||||||
|
"granted_permission_count": sum(1 for item in permissions if item["granted"]),
|
||||||
|
"optional_module_count": len(optional_modules),
|
||||||
|
"documentation_topic_count": documentation_count,
|
||||||
|
"configured_documentation_topic_count": len(documentation_layers["configured"]),
|
||||||
|
"workflow_topic_count": len(topic_groups["workflow"]),
|
||||||
|
"reference_topic_count": len(topic_groups["reference"]),
|
||||||
|
"pattern_topic_count": len(topic_groups["pattern"]),
|
||||||
|
"system_topic_count": len(topic_groups["system"]),
|
||||||
|
},
|
||||||
|
"topic_groups": topic_groups,
|
||||||
|
"layers": {
|
||||||
|
"always": {
|
||||||
|
"documentation": documentation_layers["always"],
|
||||||
|
},
|
||||||
|
"configured": {
|
||||||
|
"modules": modules,
|
||||||
|
"routes": visible_routes,
|
||||||
|
"permissions": permissions,
|
||||||
|
"documentation": documentation_layers["configured"],
|
||||||
|
},
|
||||||
|
"available": {
|
||||||
|
"routes": available_routes,
|
||||||
|
"permissions": [item for item in permissions if not item["granted"]],
|
||||||
|
"documentation": documentation_layers["available"],
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"optional_modules": optional_modules,
|
||||||
|
"sources": _evidence_sources(),
|
||||||
|
"documentation": documentation_layers["evidence"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _registry(request: Request) -> PlatformRegistry:
|
||||||
|
registry = getattr(request.app.state, "govoplan_registry", None)
|
||||||
|
if not isinstance(registry, PlatformRegistry):
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="GovOPlaN module registry is not configured")
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def _module_payload(manifest: ModuleManifest) -> dict[str, Any]:
|
||||||
|
frontend = manifest.frontend
|
||||||
|
migration = manifest.migration_spec
|
||||||
|
return {
|
||||||
|
"id": manifest.id,
|
||||||
|
"name": manifest.name,
|
||||||
|
"version": manifest.version,
|
||||||
|
"dependencies": list(manifest.dependencies),
|
||||||
|
"optional_dependencies": list(manifest.optional_dependencies),
|
||||||
|
"permission_count": len(manifest.permissions),
|
||||||
|
"role_template_count": len(manifest.role_templates),
|
||||||
|
"nav_count": len(manifest.nav_items) + (len(frontend.nav_items) if frontend else 0),
|
||||||
|
"route_count": (1 if manifest.route_factory else 0) + (len(frontend.routes) if frontend else 0),
|
||||||
|
"frontend_package": frontend.package_name if frontend else None,
|
||||||
|
"backend_route_contributed": manifest.route_factory is not None,
|
||||||
|
"migration_module_id": migration.module_id if migration else None,
|
||||||
|
"capabilities": sorted(manifest.capability_factories),
|
||||||
|
"documentation_count": len(manifest.documentation),
|
||||||
|
"documentation_provider_count": len(manifest.documentation_providers),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _permission_payload(permission: PermissionDefinition, principal: ApiPrincipal) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"scope": permission.scope,
|
||||||
|
"label": permission.label,
|
||||||
|
"description": permission.description,
|
||||||
|
"category": permission.category,
|
||||||
|
"level": permission.level,
|
||||||
|
"module_id": permission.module_id,
|
||||||
|
"resource": permission.resource,
|
||||||
|
"action": permission.action,
|
||||||
|
"deprecated": permission.deprecated,
|
||||||
|
"granted": has_scope(principal, permission.scope),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _route_items(manifests: tuple[ModuleManifest, ...], principal: ApiPrincipal) -> list[dict[str, Any]]:
|
||||||
|
items: list[dict[str, Any]] = []
|
||||||
|
for manifest in manifests:
|
||||||
|
seen: set[tuple[str, str]] = set()
|
||||||
|
for item in manifest.nav_items:
|
||||||
|
key = ("nav", item.path)
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
items.append(_nav_item_payload(manifest.id, item, principal, source="nav"))
|
||||||
|
if manifest.frontend is None:
|
||||||
|
continue
|
||||||
|
nav_by_path = {item.path: item for item in manifest.frontend.nav_items}
|
||||||
|
for item in manifest.frontend.nav_items:
|
||||||
|
key = ("frontend_nav", item.path)
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
items.append(_nav_item_payload(manifest.id, item, principal, source="frontend_nav"))
|
||||||
|
for route in manifest.frontend.routes:
|
||||||
|
key = ("frontend_route", route.path)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
items.append(_frontend_route_payload(manifest.id, route, nav_by_path.get(route.path), principal))
|
||||||
|
return sorted(items, key=lambda item: (int(item["order"]), str(item["path"]), str(item["source"])))
|
||||||
|
|
||||||
|
|
||||||
|
def _nav_item_payload(module_id: str, item: NavItem, principal: ApiPrincipal, *, source: str) -> dict[str, Any]:
|
||||||
|
visible, reason = _visibility(item.required_all, item.required_any, principal)
|
||||||
|
return {
|
||||||
|
"module_id": module_id,
|
||||||
|
"path": item.path,
|
||||||
|
"label": item.label,
|
||||||
|
"icon": item.icon,
|
||||||
|
"section": item.section,
|
||||||
|
"source": source,
|
||||||
|
"component": None,
|
||||||
|
"required_all": list(item.required_all),
|
||||||
|
"required_any": list(item.required_any),
|
||||||
|
"order": item.order,
|
||||||
|
"visible": visible,
|
||||||
|
"reason": reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _frontend_route_payload(module_id: str, route: FrontendRoute, nav_item: NavItem | None, principal: ApiPrincipal) -> dict[str, Any]:
|
||||||
|
visible, reason = _visibility(route.required_all, route.required_any, principal)
|
||||||
|
return {
|
||||||
|
"module_id": module_id,
|
||||||
|
"path": route.path,
|
||||||
|
"label": nav_item.label if nav_item else route.component,
|
||||||
|
"icon": nav_item.icon if nav_item else None,
|
||||||
|
"section": nav_item.section if nav_item else None,
|
||||||
|
"source": "frontend_route",
|
||||||
|
"component": route.component,
|
||||||
|
"required_all": list(route.required_all),
|
||||||
|
"required_any": list(route.required_any),
|
||||||
|
"order": route.order,
|
||||||
|
"visible": visible,
|
||||||
|
"reason": reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _visibility(required_all: tuple[str, ...], required_any: tuple[str, ...], principal: ApiPrincipal) -> tuple[bool, str]:
|
||||||
|
missing_all = [scope for scope in required_all if not has_scope(principal, scope)]
|
||||||
|
any_satisfied = not required_any or any(has_scope(principal, scope) for scope in required_any)
|
||||||
|
if not missing_all and any_satisfied:
|
||||||
|
return True, "visible"
|
||||||
|
reasons: list[str] = []
|
||||||
|
if missing_all:
|
||||||
|
reasons.append("missing " + ", ".join(missing_all))
|
||||||
|
if not any_satisfied:
|
||||||
|
reasons.append("requires one of " + ", ".join(required_any))
|
||||||
|
return False, "; ".join(reasons)
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_module_evidence(manifests: tuple[ModuleManifest, ...]) -> list[dict[str, str]]:
|
||||||
|
installed = {manifest.id for manifest in manifests}
|
||||||
|
evidence: list[dict[str, str]] = []
|
||||||
|
for manifest in manifests:
|
||||||
|
for module_id in manifest.optional_dependencies:
|
||||||
|
evidence.append({
|
||||||
|
"module_id": module_id,
|
||||||
|
"source_module_id": manifest.id,
|
||||||
|
"status": "installed" if module_id in installed else "not_installed",
|
||||||
|
"reason": f"{manifest.name} declares optional integration with {module_id}.",
|
||||||
|
})
|
||||||
|
return sorted(evidence, key=lambda item: (item["status"], item["module_id"], item["source_module_id"]))
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_layers(
|
||||||
|
request: Request,
|
||||||
|
registry: PlatformRegistry,
|
||||||
|
principal: ApiPrincipal,
|
||||||
|
*,
|
||||||
|
documentation_type: DocumentationType,
|
||||||
|
locale: str,
|
||||||
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
settings = _settings(request)
|
||||||
|
try:
|
||||||
|
with get_database().SessionLocal() as session:
|
||||||
|
return _classify_documentation(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
|
||||||
|
except RuntimeError:
|
||||||
|
return _classify_documentation(registry, principal, settings=settings, session=None, documentation_type=documentation_type, locale=locale)
|
||||||
|
|
||||||
|
|
||||||
|
def _settings(request: Request) -> object | None:
|
||||||
|
direct_settings = getattr(request.app.state, "govoplan_settings", None)
|
||||||
|
if direct_settings is not None:
|
||||||
|
return direct_settings
|
||||||
|
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
|
||||||
|
return getattr(lifecycle, "settings", None)
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_documentation(
|
||||||
|
registry: PlatformRegistry,
|
||||||
|
principal: ApiPrincipal,
|
||||||
|
*,
|
||||||
|
settings: object | None,
|
||||||
|
session: object | None,
|
||||||
|
documentation_type: DocumentationType,
|
||||||
|
locale: str,
|
||||||
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
layers: dict[str, list[dict[str, Any]]] = {"always": [], "configured": [], "available": [], "evidence": []}
|
||||||
|
installed = {manifest.id for manifest in registry.manifests()}
|
||||||
|
topics = _collect_documentation_topics(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
|
||||||
|
for source_module_id, topic in sorted(topics, key=lambda item: (item[1].order, item[0], item[1].id)):
|
||||||
|
if not _topic_matches_documentation_type(topic, documentation_type):
|
||||||
|
continue
|
||||||
|
active, reason, blockers = _documentation_visibility(topic, installed, registry, principal)
|
||||||
|
target_layer = _documentation_target_layer(topic, active, blockers)
|
||||||
|
if target_layer not in layers:
|
||||||
|
target_layer = "evidence"
|
||||||
|
layers[target_layer].append(_documentation_topic_payload(
|
||||||
|
source_module_id,
|
||||||
|
topic,
|
||||||
|
active=active,
|
||||||
|
reason=reason,
|
||||||
|
target_layer=target_layer,
|
||||||
|
blockers=blockers,
|
||||||
|
locale=locale,
|
||||||
|
))
|
||||||
|
return layers
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_documentation_topics(
|
||||||
|
registry: PlatformRegistry,
|
||||||
|
principal: ApiPrincipal,
|
||||||
|
*,
|
||||||
|
settings: object | None,
|
||||||
|
session: object | None,
|
||||||
|
documentation_type: DocumentationType,
|
||||||
|
locale: str,
|
||||||
|
) -> list[tuple[str, DocumentationTopic]]:
|
||||||
|
topics: list[tuple[str, DocumentationTopic]] = []
|
||||||
|
for manifest in registry.manifests():
|
||||||
|
for topic in manifest.documentation:
|
||||||
|
topics.append((manifest.id, topic))
|
||||||
|
if not manifest.documentation_providers:
|
||||||
|
continue
|
||||||
|
context = DocumentationContext(
|
||||||
|
registry=registry,
|
||||||
|
principal=principal,
|
||||||
|
settings=settings,
|
||||||
|
session=session,
|
||||||
|
documentation_type=documentation_type,
|
||||||
|
locale=locale,
|
||||||
|
data={"source_module_id": manifest.id},
|
||||||
|
)
|
||||||
|
for provider in manifest.documentation_providers:
|
||||||
|
try:
|
||||||
|
provided_topics = tuple(provider(context))
|
||||||
|
except Exception as exc:
|
||||||
|
provided_topics = (
|
||||||
|
DocumentationTopic(
|
||||||
|
id=f"{manifest.id}.runtime-documentation-unavailable",
|
||||||
|
title=f"{manifest.name} runtime documentation unavailable",
|
||||||
|
summary="The module registered runtime documentation, but it could not be evaluated for this request.",
|
||||||
|
body="The static module documentation remains available. Check the module API and logs if this persists.",
|
||||||
|
layer="evidence",
|
||||||
|
documentation_types=(documentation_type,),
|
||||||
|
source_module_id=manifest.id,
|
||||||
|
metadata={"error_type": type(exc).__name__},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for topic in provided_topics:
|
||||||
|
topics.append((manifest.id, topic))
|
||||||
|
return topics
|
||||||
|
|
||||||
|
|
||||||
|
def _topic_matches_documentation_type(topic: DocumentationTopic, documentation_type: DocumentationType) -> bool:
|
||||||
|
return documentation_type in (topic.documentation_types or ("admin",))
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_visibility(
|
||||||
|
topic: DocumentationTopic,
|
||||||
|
installed: set[str],
|
||||||
|
registry: PlatformRegistry,
|
||||||
|
principal: ApiPrincipal,
|
||||||
|
) -> tuple[bool, str, dict[str, list[str]]]:
|
||||||
|
if not topic.conditions:
|
||||||
|
return True, "documented", {"modules": [], "capabilities": [], "scopes": []}
|
||||||
|
|
||||||
|
reasons: list[str] = []
|
||||||
|
blockers = {"modules": [], "capabilities": [], "scopes": []}
|
||||||
|
for condition in topic.conditions:
|
||||||
|
active, reason, condition_blockers = _condition_visibility(condition, installed, registry, principal)
|
||||||
|
if active:
|
||||||
|
return True, reason, {"modules": [], "capabilities": [], "scopes": []}
|
||||||
|
reasons.append(reason)
|
||||||
|
for key, values in condition_blockers.items():
|
||||||
|
blockers[key].extend(value for value in values if value not in blockers[key])
|
||||||
|
return False, "; ".join(reason for reason in reasons if reason) or "documentation conditions are not satisfied", blockers
|
||||||
|
|
||||||
|
|
||||||
|
def _condition_visibility(
|
||||||
|
condition: DocumentationCondition,
|
||||||
|
installed: set[str],
|
||||||
|
registry: PlatformRegistry,
|
||||||
|
principal: ApiPrincipal,
|
||||||
|
) -> tuple[bool, str, dict[str, list[str]]]:
|
||||||
|
blockers = {"modules": [], "capabilities": [], "scopes": []}
|
||||||
|
missing_required_modules = [module_id for module_id in condition.required_modules if module_id not in installed]
|
||||||
|
if missing_required_modules:
|
||||||
|
blockers["modules"].extend(missing_required_modules)
|
||||||
|
if condition.any_modules and not any(module_id in installed for module_id in condition.any_modules):
|
||||||
|
blockers["modules"].extend(module_id for module_id in condition.any_modules if module_id not in blockers["modules"])
|
||||||
|
conflicting_modules = [module_id for module_id in condition.missing_modules if module_id in installed]
|
||||||
|
if conflicting_modules:
|
||||||
|
blockers["modules"].extend(conflicting_modules)
|
||||||
|
missing_capabilities = [name for name in condition.required_capabilities if not registry.has_capability(name)]
|
||||||
|
if missing_capabilities:
|
||||||
|
blockers["capabilities"].extend(missing_capabilities)
|
||||||
|
missing_scopes = [scope for scope in condition.required_scopes if not has_scope(principal, scope)]
|
||||||
|
missing_any_scopes = list(condition.any_scopes) if condition.any_scopes and not any(has_scope(principal, scope) for scope in condition.any_scopes) else []
|
||||||
|
if missing_scopes:
|
||||||
|
blockers["scopes"].extend(missing_scopes)
|
||||||
|
if missing_any_scopes:
|
||||||
|
blockers["scopes"].extend(scope for scope in missing_any_scopes if scope not in blockers["scopes"])
|
||||||
|
|
||||||
|
if not blockers["modules"] and not blockers["capabilities"] and not blockers["scopes"]:
|
||||||
|
return True, "conditions satisfied", blockers
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
if missing_required_modules:
|
||||||
|
parts.append("missing modules: " + ", ".join(missing_required_modules))
|
||||||
|
if condition.any_modules and not any(module_id in installed for module_id in condition.any_modules):
|
||||||
|
parts.append("requires one installed module from: " + ", ".join(condition.any_modules))
|
||||||
|
if conflicting_modules:
|
||||||
|
parts.append("not active when installed: " + ", ".join(conflicting_modules))
|
||||||
|
if missing_capabilities:
|
||||||
|
parts.append("missing capabilities: " + ", ".join(missing_capabilities))
|
||||||
|
if missing_scopes:
|
||||||
|
parts.append("missing scopes: " + ", ".join(missing_scopes))
|
||||||
|
if missing_any_scopes:
|
||||||
|
parts.append("requires one scope from: " + ", ".join(missing_any_scopes))
|
||||||
|
return False, "; ".join(parts), blockers
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_target_layer(topic: DocumentationTopic, active: bool, blockers: dict[str, list[str]]) -> str:
|
||||||
|
if topic.layer == "always":
|
||||||
|
return "always"
|
||||||
|
if active:
|
||||||
|
return topic.layer
|
||||||
|
if blockers["modules"] or blockers["capabilities"]:
|
||||||
|
return "evidence"
|
||||||
|
return "available"
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_topic_payload(
|
||||||
|
source_module_id: str,
|
||||||
|
topic: DocumentationTopic,
|
||||||
|
*,
|
||||||
|
active: bool,
|
||||||
|
reason: str,
|
||||||
|
target_layer: str,
|
||||||
|
blockers: dict[str, list[str]],
|
||||||
|
locale: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
module_id = topic.source_module_id or source_module_id
|
||||||
|
translation_locale, translation = _translation_for_locale(topic, locale)
|
||||||
|
kind = _documentation_topic_kind(topic)
|
||||||
|
return {
|
||||||
|
"id": topic.id,
|
||||||
|
"source_module_id": module_id,
|
||||||
|
"kind": kind,
|
||||||
|
"anchor_id": _documentation_topic_anchor(module_id, topic.id),
|
||||||
|
"title": str(translation.get("title") or topic.title),
|
||||||
|
"summary": str(translation.get("summary") or topic.summary),
|
||||||
|
"body": str(translation.get("body") or topic.body),
|
||||||
|
"layer": topic.layer,
|
||||||
|
"target_layer": target_layer,
|
||||||
|
"documentation_types": list(topic.documentation_types),
|
||||||
|
"active": active,
|
||||||
|
"reason": reason,
|
||||||
|
"blockers": {
|
||||||
|
"modules": sorted(dict.fromkeys(blockers.get("modules", ()))),
|
||||||
|
"capabilities": sorted(dict.fromkeys(blockers.get("capabilities", ()))),
|
||||||
|
"scopes": sorted(dict.fromkeys(blockers.get("scopes", ()))),
|
||||||
|
},
|
||||||
|
"audience": list(topic.audience),
|
||||||
|
"order": topic.order,
|
||||||
|
"i18n_key": topic.i18n_key or topic.id,
|
||||||
|
"locale": locale,
|
||||||
|
"translation_locale": translation_locale,
|
||||||
|
"conditions": [_documentation_condition_payload(condition) for condition in topic.conditions],
|
||||||
|
"links": [_documentation_link_payload(link) for link in topic.links],
|
||||||
|
"related_modules": list(topic.related_modules),
|
||||||
|
"unlocks": list(topic.unlocks),
|
||||||
|
"configuration_keys": sorted({*topic.configuration_keys, *(key for condition in topic.conditions for key in condition.configuration_keys)}),
|
||||||
|
"metadata": dict(topic.metadata),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_topic_kind(topic: DocumentationTopic) -> str:
|
||||||
|
raw_kind = topic.metadata.get("kind")
|
||||||
|
if isinstance(raw_kind, str):
|
||||||
|
normalized = raw_kind.strip().lower().replace("_", "-")
|
||||||
|
if normalized in TOPIC_KINDS:
|
||||||
|
return normalized
|
||||||
|
return "system"
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_topic_anchor(source_module_id: str, topic_id: str) -> str:
|
||||||
|
raw = f"docs-topic-{source_module_id}-{topic_id}"
|
||||||
|
cleaned = "".join(char if char.isalnum() or char in {"_", "-"} else "-" for char in raw)
|
||||||
|
while "--" in cleaned:
|
||||||
|
cleaned = cleaned.replace("--", "-")
|
||||||
|
return cleaned.strip("-").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_topic_groups(documentation_layers: dict[str, list[dict[str, Any]]]) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
groups: dict[str, list[dict[str, Any]]] = {kind: [] for kind in TOPIC_KINDS}
|
||||||
|
for topic in _all_documentation_topics(documentation_layers):
|
||||||
|
kind = str(topic.get("kind") or "system")
|
||||||
|
groups.setdefault(kind, []).append(topic)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def _all_documentation_topics(documentation_layers: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]:
|
||||||
|
topics: list[dict[str, Any]] = []
|
||||||
|
for layer in ("always", "configured", "available", "evidence"):
|
||||||
|
topics.extend(documentation_layers.get(layer, ()))
|
||||||
|
return topics
|
||||||
|
|
||||||
|
|
||||||
|
def _translation_for_locale(topic: DocumentationTopic, locale: str) -> tuple[str, Mapping[str, str]]:
|
||||||
|
translations = topic.translations or {}
|
||||||
|
for candidate in _locale_candidates(locale):
|
||||||
|
translation = translations.get(candidate)
|
||||||
|
if translation:
|
||||||
|
return candidate, translation
|
||||||
|
return "source", {}
|
||||||
|
|
||||||
|
|
||||||
|
def _locale_candidates(locale: str) -> tuple[str, ...]:
|
||||||
|
normalized = _normalize_locale(locale)
|
||||||
|
base = normalized.split("-", 1)[0]
|
||||||
|
candidates = [normalized]
|
||||||
|
if base and base != normalized:
|
||||||
|
candidates.append(base)
|
||||||
|
if "en" not in candidates:
|
||||||
|
candidates.append("en")
|
||||||
|
return tuple(candidates)
|
||||||
|
|
||||||
|
|
||||||
|
def _preferred_locale(request: Request, explicit_locale: str | None) -> str:
|
||||||
|
if explicit_locale:
|
||||||
|
return _normalize_locale(explicit_locale)
|
||||||
|
accept_language = request.headers.get("accept-language", "")
|
||||||
|
first = accept_language.split(",", 1)[0].split(";", 1)[0].strip()
|
||||||
|
return _normalize_locale(first or "en")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_locale(value: str) -> str:
|
||||||
|
clean = "".join(char for char in value.strip().replace("_", "-") if char.isalnum() or char == "-")
|
||||||
|
if not clean:
|
||||||
|
return "en"
|
||||||
|
parts = [part for part in clean.split("-") if part]
|
||||||
|
if not parts:
|
||||||
|
return "en"
|
||||||
|
head = parts[0].lower()
|
||||||
|
tail = [part.upper() if len(part) == 2 else part for part in parts[1:]]
|
||||||
|
return "-".join([head, *tail])[:20]
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_condition_payload(condition: DocumentationCondition) -> dict[str, list[str]]:
|
||||||
|
return {
|
||||||
|
"required_modules": list(condition.required_modules),
|
||||||
|
"any_modules": list(condition.any_modules),
|
||||||
|
"missing_modules": list(condition.missing_modules),
|
||||||
|
"required_capabilities": list(condition.required_capabilities),
|
||||||
|
"required_scopes": list(condition.required_scopes),
|
||||||
|
"any_scopes": list(condition.any_scopes),
|
||||||
|
"configuration_keys": list(condition.configuration_keys),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_link_payload(link: DocumentationLink) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"label": link.label,
|
||||||
|
"href": link.href,
|
||||||
|
"kind": link.kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_sources() -> list[dict[str, str]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"label": "Documentation layer concept",
|
||||||
|
"source": "govoplan-docs/docs/DOCUMENTATION_LAYER_CONCEPT.md",
|
||||||
|
"layer": "evidence",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Master roadmap",
|
||||||
|
"source": "govoplan-core/docs/GOVOPLAN_MASTER_ROADMAP.md",
|
||||||
|
"layer": "evidence",
|
||||||
|
},
|
||||||
|
]
|
||||||
146
src/govoplan_docs/backend/manifest.py
Normal file
146
src/govoplan_docs/backend/manifest.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from govoplan_core.core.modules import (
|
||||||
|
DocumentationLink,
|
||||||
|
DocumentationTopic,
|
||||||
|
FrontendModule,
|
||||||
|
FrontendRoute,
|
||||||
|
ModuleContext,
|
||||||
|
ModuleManifest,
|
||||||
|
NavItem,
|
||||||
|
PermissionDefinition,
|
||||||
|
RoleTemplate,
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCS_READ_SCOPE = "docs:documentation:read"
|
||||||
|
DOCS_READ_SCOPES = (DOCS_READ_SCOPE, "system:settings:read", "admin:settings:read")
|
||||||
|
|
||||||
|
|
||||||
|
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||||
|
module_id, resource, action = scope.split(":", 2)
|
||||||
|
return PermissionDefinition(
|
||||||
|
scope=scope,
|
||||||
|
label=label,
|
||||||
|
description=description,
|
||||||
|
category="Documentation",
|
||||||
|
level="tenant",
|
||||||
|
module_id=module_id,
|
||||||
|
resource=resource,
|
||||||
|
action=action,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _route_factory(context: ModuleContext):
|
||||||
|
del context
|
||||||
|
from govoplan_docs.backend.api.v1.routes import router
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
manifest = ModuleManifest(
|
||||||
|
id="docs",
|
||||||
|
name="Docs",
|
||||||
|
version="0.1.6",
|
||||||
|
dependencies=("access",),
|
||||||
|
optional_dependencies=("policy", "audit", "ops", "workflow", "search"),
|
||||||
|
permissions=(
|
||||||
|
_permission(
|
||||||
|
DOCS_READ_SCOPE,
|
||||||
|
"View configured documentation",
|
||||||
|
"Read documentation generated from installed modules, visible routes, permissions, and evidence sources.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
role_templates=(
|
||||||
|
RoleTemplate(
|
||||||
|
slug="docs_reader",
|
||||||
|
name="Documentation reader",
|
||||||
|
description="Read the configured-system documentation browser.",
|
||||||
|
permissions=(DOCS_READ_SCOPE,),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
route_factory=_route_factory,
|
||||||
|
nav_items=(NavItem(path="/docs", label="Docs", icon="reports", required_any=DOCS_READ_SCOPES, order=880),),
|
||||||
|
frontend=FrontendModule(
|
||||||
|
module_id="docs",
|
||||||
|
package_name="@govoplan/docs-webui",
|
||||||
|
routes=(FrontendRoute(path="/docs", component="DocsPage", required_any=DOCS_READ_SCOPES, order=880),),
|
||||||
|
nav_items=(NavItem(path="/docs", label="Docs", icon="reports", required_any=DOCS_READ_SCOPES, order=880),),
|
||||||
|
),
|
||||||
|
documentation=(
|
||||||
|
DocumentationTopic(
|
||||||
|
id="docs.configured-system-documentation",
|
||||||
|
title="Configured system documentation",
|
||||||
|
summary="This documentation page starts with installed modules, active configuration, visible routes, and permissions for the current actor.",
|
||||||
|
body="Module manifests can contribute durable documentation topics. Modules can also register runtime documentation providers when content depends on tenant policy, installed integrations, or operational settings.",
|
||||||
|
layer="always",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("tenant_admin", "operator", "module_admin"),
|
||||||
|
order=10,
|
||||||
|
i18n_key="docs.topic.configured_system_documentation",
|
||||||
|
translations={
|
||||||
|
"de": {
|
||||||
|
"title": "Dokumentation dieses Systems",
|
||||||
|
"summary": "Diese Dokumentation beginnt mit den installierten Modulen, der aktiven Konfiguration und den Funktionen, die fuer diese Rolle sichtbar sind.",
|
||||||
|
"body": "Module koennen feste Dokumentationsabschnitte beitragen. Wenn Inhalte von Tenant-Regeln, installierten Integrationen oder Betriebsoptionen abhaengen, kann ein Modul laufzeitbasierte Dokumentation registrieren.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="Public GovOPlaN module documentation",
|
||||||
|
href="https://govplan.add-ideas.de/",
|
||||||
|
kind="public",
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Documentation layer concept",
|
||||||
|
href="govoplan-docs/docs/DOCUMENTATION_LAYER_CONCEPT.md",
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Instance-aware documentation contract",
|
||||||
|
href="govoplan-docs/docs/INSTANCE_AWARE_DOCUMENTATION.md",
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metadata={"kind": "system"},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="docs.pattern.field-help",
|
||||||
|
title="Field help marker",
|
||||||
|
summary="A small help marker next to a label gives local context without turning dense forms into manuals.",
|
||||||
|
body="Use the marker for short explanations of a field, option, or compact term. Link to a workflow or reference topic when the reader needs steps, API mapping, policy provenance, or operational detail.",
|
||||||
|
layer="always",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("user", "tenant_admin", "operator", "module_admin"),
|
||||||
|
order=20,
|
||||||
|
i18n_key="docs.topic.pattern.field_help",
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="Documentation experience concept",
|
||||||
|
href="govoplan-docs/docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md",
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metadata={
|
||||||
|
"kind": "pattern",
|
||||||
|
"pattern_id": "field-help-marker",
|
||||||
|
"purpose": "Keep labels scannable while making short explanations available on demand.",
|
||||||
|
"when_used": "Form labels, toggle labels, effective-value rows, and compact admin terms.",
|
||||||
|
"user_explanation": "Open the marker when a label is unclear. It should explain the local choice in one or two sentences.",
|
||||||
|
"admin_explanation": "Field help stays local. Longer procedural, API, or policy explanations belong in linked workflow or reference topics.",
|
||||||
|
"component_refs": [
|
||||||
|
"govoplan-core/webui/src/components/help/FieldLabel.tsx",
|
||||||
|
"govoplan-core/webui/src/components/help/InlineHelp.tsx",
|
||||||
|
"govoplan-core/webui/src/utils/fieldHelp.ts",
|
||||||
|
],
|
||||||
|
"related_topic_ids": [
|
||||||
|
"access.reference.admin-access-fields",
|
||||||
|
"access.workflow.grant-user-access",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_manifest() -> ModuleManifest:
|
||||||
|
return manifest
|
||||||
1
src/govoplan_docs/py.typed
Normal file
1
src/govoplan_docs/py.typed
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
91
tests/test_docs_context.py
Normal file
91
tests/test_docs_context.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from govoplan_access.backend.manifest import get_manifest as get_access_manifest
|
||||||
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
|
from govoplan_tenancy.backend.manifest import get_manifest as get_tenancy_manifest
|
||||||
|
from govoplan_docs.backend.api.v1.routes import (
|
||||||
|
_classify_documentation,
|
||||||
|
_documentation_topic_anchor,
|
||||||
|
_documentation_topic_groups,
|
||||||
|
)
|
||||||
|
from govoplan_docs.backend.manifest import get_manifest as get_docs_manifest
|
||||||
|
|
||||||
|
|
||||||
|
class FakePrincipal:
|
||||||
|
tenant_id = "tenant-1"
|
||||||
|
user = SimpleNamespace(id="user-1")
|
||||||
|
|
||||||
|
def __init__(self, scopes: set[str]) -> None:
|
||||||
|
self.scopes = frozenset(scopes)
|
||||||
|
|
||||||
|
def has(self, required_scope: str) -> bool:
|
||||||
|
return required_scope in self.scopes
|
||||||
|
|
||||||
|
|
||||||
|
class DocsContextTests(unittest.TestCase):
|
||||||
|
def test_topic_groups_preserve_layer_classification(self) -> None:
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(get_tenancy_manifest())
|
||||||
|
registry.register(get_access_manifest())
|
||||||
|
registry.register(get_docs_manifest())
|
||||||
|
principal = FakePrincipal({"admin:users:read", "admin:groups:read", "docs:documentation:read"})
|
||||||
|
|
||||||
|
layers = _classify_documentation(
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
settings=None,
|
||||||
|
session=None,
|
||||||
|
documentation_type="admin",
|
||||||
|
locale="en",
|
||||||
|
)
|
||||||
|
groups = _documentation_topic_groups(layers)
|
||||||
|
|
||||||
|
workflow_ids = {topic["id"] for topic in groups["workflow"]}
|
||||||
|
reference_ids = {topic["id"] for topic in groups["reference"]}
|
||||||
|
pattern_ids = {topic["id"] for topic in groups["pattern"]}
|
||||||
|
system_ids = {topic["id"] for topic in groups["system"]}
|
||||||
|
|
||||||
|
self.assertIn("access.workflow.grant-user-access", workflow_ids)
|
||||||
|
self.assertIn("access.reference.admin-access-fields", reference_ids)
|
||||||
|
self.assertIn("docs.pattern.field-help", pattern_ids)
|
||||||
|
self.assertIn("docs.configured-system-documentation", system_ids)
|
||||||
|
|
||||||
|
configured_ids = {topic["id"] for topic in layers["configured"]}
|
||||||
|
always_ids = {topic["id"] for topic in layers["always"]}
|
||||||
|
self.assertIn("access.workflow.grant-user-access", configured_ids)
|
||||||
|
self.assertIn("docs.pattern.field-help", always_ids)
|
||||||
|
|
||||||
|
def test_unavailable_topic_still_appears_in_kind_group(self) -> None:
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(get_tenancy_manifest())
|
||||||
|
registry.register(get_access_manifest())
|
||||||
|
registry.register(get_docs_manifest())
|
||||||
|
principal = FakePrincipal({"docs:documentation:read"})
|
||||||
|
|
||||||
|
layers = _classify_documentation(
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
settings=None,
|
||||||
|
session=None,
|
||||||
|
documentation_type="user",
|
||||||
|
locale="en",
|
||||||
|
)
|
||||||
|
groups = _documentation_topic_groups(layers)
|
||||||
|
workflows = {topic["id"]: topic for topic in groups["workflow"]}
|
||||||
|
|
||||||
|
self.assertIn("access.workflow.grant-user-access", workflows)
|
||||||
|
self.assertFalse(workflows["access.workflow.grant-user-access"]["active"])
|
||||||
|
self.assertIn("access.workflow.grant-user-access", {topic["id"] for topic in layers["available"]})
|
||||||
|
|
||||||
|
def test_topic_anchor_is_stable(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
_documentation_topic_anchor("access", "access.workflow.grant-user-access"),
|
||||||
|
"docs-topic-access-access-workflow-grant-user-access",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
30
webui/package.json
Normal file
30
webui/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/docs-webui",
|
||||||
|
"version": "0.1.6",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.6",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
webui/src/api/docs.ts
Normal file
169
webui/src/api/docs.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export type DocsModule = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
dependencies: string[];
|
||||||
|
optional_dependencies: string[];
|
||||||
|
permission_count: number;
|
||||||
|
role_template_count: number;
|
||||||
|
nav_count: number;
|
||||||
|
route_count: number;
|
||||||
|
frontend_package?: string | null;
|
||||||
|
backend_route_contributed: boolean;
|
||||||
|
migration_module_id?: string | null;
|
||||||
|
capabilities: string[];
|
||||||
|
documentation_count: number;
|
||||||
|
documentation_provider_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsRoute = {
|
||||||
|
module_id: string;
|
||||||
|
path: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string | null;
|
||||||
|
section?: string | null;
|
||||||
|
source: string;
|
||||||
|
component?: string | null;
|
||||||
|
required_all: string[];
|
||||||
|
required_any: string[];
|
||||||
|
order: number;
|
||||||
|
visible: boolean;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsPermission = {
|
||||||
|
scope: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
level: "system" | "tenant";
|
||||||
|
module_id: string;
|
||||||
|
resource: string;
|
||||||
|
action: string;
|
||||||
|
deprecated: boolean;
|
||||||
|
granted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsOptionalModuleEvidence = {
|
||||||
|
module_id: string;
|
||||||
|
source_module_id: string;
|
||||||
|
status: "installed" | "not_installed" | string;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsSource = {
|
||||||
|
label: string;
|
||||||
|
source: string;
|
||||||
|
layer: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsDocumentationCondition = {
|
||||||
|
required_modules: string[];
|
||||||
|
any_modules: string[];
|
||||||
|
missing_modules: string[];
|
||||||
|
required_capabilities: string[];
|
||||||
|
required_scopes: string[];
|
||||||
|
any_scopes: string[];
|
||||||
|
configuration_keys: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsDocumentationLink = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
kind: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsTopicKind = "workflow" | "reference" | "pattern" | "system" | string;
|
||||||
|
|
||||||
|
export type DocsDocumentationTopic = {
|
||||||
|
id: string;
|
||||||
|
source_module_id: string;
|
||||||
|
kind: DocsTopicKind;
|
||||||
|
anchor_id: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
body: string;
|
||||||
|
layer: "always" | "configured" | "available" | "evidence" | string;
|
||||||
|
target_layer: string;
|
||||||
|
documentation_types: Array<"admin" | "user" | string>;
|
||||||
|
active: boolean;
|
||||||
|
reason: string;
|
||||||
|
blockers: {
|
||||||
|
modules: string[];
|
||||||
|
capabilities: string[];
|
||||||
|
scopes: string[];
|
||||||
|
};
|
||||||
|
audience: string[];
|
||||||
|
order: number;
|
||||||
|
i18n_key: string;
|
||||||
|
locale: string;
|
||||||
|
translation_locale: string;
|
||||||
|
conditions: DocsDocumentationCondition[];
|
||||||
|
links: DocsDocumentationLink[];
|
||||||
|
related_modules: string[];
|
||||||
|
unlocks: string[];
|
||||||
|
configuration_keys: string[];
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsContext = {
|
||||||
|
actor: {
|
||||||
|
tenant_id: string;
|
||||||
|
user_id: string;
|
||||||
|
scope_count: number;
|
||||||
|
documentation_type: "admin" | "user";
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
summary: {
|
||||||
|
module_count: number;
|
||||||
|
visible_route_count: number;
|
||||||
|
available_route_count: number;
|
||||||
|
permission_count: number;
|
||||||
|
granted_permission_count: number;
|
||||||
|
optional_module_count: number;
|
||||||
|
documentation_topic_count: number;
|
||||||
|
configured_documentation_topic_count: number;
|
||||||
|
workflow_topic_count: number;
|
||||||
|
reference_topic_count: number;
|
||||||
|
pattern_topic_count: number;
|
||||||
|
system_topic_count: number;
|
||||||
|
};
|
||||||
|
topic_groups: {
|
||||||
|
workflow: DocsDocumentationTopic[];
|
||||||
|
reference: DocsDocumentationTopic[];
|
||||||
|
pattern: DocsDocumentationTopic[];
|
||||||
|
system: DocsDocumentationTopic[];
|
||||||
|
[kind: string]: DocsDocumentationTopic[];
|
||||||
|
};
|
||||||
|
layers: {
|
||||||
|
always: {
|
||||||
|
documentation: DocsDocumentationTopic[];
|
||||||
|
};
|
||||||
|
configured: {
|
||||||
|
modules: DocsModule[];
|
||||||
|
routes: DocsRoute[];
|
||||||
|
permissions: DocsPermission[];
|
||||||
|
documentation: DocsDocumentationTopic[];
|
||||||
|
};
|
||||||
|
available: {
|
||||||
|
routes: DocsRoute[];
|
||||||
|
permissions: DocsPermission[];
|
||||||
|
documentation: DocsDocumentationTopic[];
|
||||||
|
};
|
||||||
|
evidence: {
|
||||||
|
optional_modules: DocsOptionalModuleEvidence[];
|
||||||
|
sources: DocsSource[];
|
||||||
|
documentation: DocsDocumentationTopic[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchDocsContext(settings: ApiSettings, options: { documentationType?: "admin" | "user"; locale?: string } = {}): Promise<DocsContext> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.documentationType) params.set("type", options.documentationType);
|
||||||
|
if (options.locale) params.set("locale", options.locale);
|
||||||
|
const query = params.toString();
|
||||||
|
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`);
|
||||||
|
}
|
||||||
950
webui/src/features/docs/DocsPage.tsx
Normal file
950
webui/src/features/docs/DocsPage.tsx
Normal file
@@ -0,0 +1,950 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DismissibleAlert,
|
||||||
|
ExplorerTree,
|
||||||
|
LoadingFrame,
|
||||||
|
PageTitle,
|
||||||
|
SegmentedControl,
|
||||||
|
StatusBadge,
|
||||||
|
adminErrorMessage,
|
||||||
|
useGuardedNavigate,
|
||||||
|
usePlatformLanguage,
|
||||||
|
type ApiSettings
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
|
import {
|
||||||
|
fetchDocsContext,
|
||||||
|
type DocsContext,
|
||||||
|
type DocsDocumentationTopic,
|
||||||
|
type DocsModule,
|
||||||
|
type DocsOptionalModuleEvidence,
|
||||||
|
type DocsRoute,
|
||||||
|
type DocsSource
|
||||||
|
} from "../../api/docs";
|
||||||
|
|
||||||
|
type DocumentationType = "admin" | "user";
|
||||||
|
|
||||||
|
type DocsPageNode =
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
kind: "topic";
|
||||||
|
topic: DocsDocumentationTopic;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
kind: "topic-list";
|
||||||
|
topics: DocsDocumentationTopic[];
|
||||||
|
emptyText: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
kind: "admin-reference";
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocsTreeNode = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
page: DocsPageNode;
|
||||||
|
children: DocsTreeNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type OutlineItem = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useGuardedNavigate();
|
||||||
|
const { language } = usePlatformLanguage();
|
||||||
|
const [context, setContext] = useState<DocsContext | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
|
||||||
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
|
||||||
|
const locale = localeFromSearch(location.search) ?? language;
|
||||||
|
const adminDocs = documentationType === "admin";
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
setContext(await fetchDocsContext(settings, { documentationType, locale }));
|
||||||
|
} catch (err) {
|
||||||
|
setError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextType = documentationTypeFromSearch(location.search);
|
||||||
|
setDocumentationType((current) => current === nextType ? current : nextType);
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale]);
|
||||||
|
|
||||||
|
const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]);
|
||||||
|
const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
|
||||||
|
const selectedPage = selectedPageFromSearch(location.search, pages);
|
||||||
|
const topicById = useMemo(() => topicIndex(context), [context]);
|
||||||
|
const visibleRoutes = context?.layers.configured.routes ?? [];
|
||||||
|
const availableRoutes = context?.layers.available.routes ?? [];
|
||||||
|
const grantedPermissions = context?.layers.configured.permissions.filter((item) => item.granted) ?? [];
|
||||||
|
const moduleRows = useMemo(() => context?.layers.configured.modules ?? [], [context]);
|
||||||
|
const outlineItems = outlineForPage(selectedPage, adminDocs);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpandedNodes(new Set(defaultExpandedNodeIds(treeNodes)));
|
||||||
|
}, [treeNodes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPage) return;
|
||||||
|
const ancestorIds = ancestorNodeIdsForPage(treeNodes, selectedPage.id);
|
||||||
|
if (!ancestorIds.length) return;
|
||||||
|
setExpandedNodes((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
for (const nodeId of ancestorIds) {
|
||||||
|
next.add(nodeId);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [treeNodes, selectedPage?.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="workspace module-workspace docs-workspace">
|
||||||
|
<aside className="section-sidebar docs-outline" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
|
||||||
|
<div className="docs-sidebar-header">
|
||||||
|
<div className="section-title">i18n:govoplan-docs.help_center.f3f3a34b</div>
|
||||||
|
<AudienceToggle selected={documentationType} onSelect={selectDocumentationType} />
|
||||||
|
</div>
|
||||||
|
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
|
||||||
|
<ExplorerTree
|
||||||
|
nodes={treeNodes}
|
||||||
|
getNodeId={(node) => node.id}
|
||||||
|
getNodeLabel={(node) => node.title}
|
||||||
|
getNodeChildren={(node) => node.children}
|
||||||
|
activeId={activeNodeIdForPage(treeNodes, selectedPage?.id ?? "")}
|
||||||
|
expandedIds={expandedNodes}
|
||||||
|
depth={0}
|
||||||
|
childrenBaseClassName="docs-tree-children"
|
||||||
|
nodeContainerClassName="docs-tree-node"
|
||||||
|
nodeWrapBaseClassName="docs-tree-row"
|
||||||
|
toggleBaseClassName="docs-tree-toggle"
|
||||||
|
nodeButtonBaseClassName="docs-tree-page"
|
||||||
|
getNodeWrapStyle={(_node, context) => ({ paddingLeft: `${context.depth * 14}px` })}
|
||||||
|
getNodeButtonClassName={(node) => selectedPage?.id === node.page.id ? "is-active" : ""}
|
||||||
|
renderToggleIcon={(_node, context) => context.hasChildren ? context.expanded ? <ChevronDown size={15} /> : <ChevronRight size={15} /> : <span className="docs-tree-toggle-placeholder" />}
|
||||||
|
renderNodeContent={(node) => node.title}
|
||||||
|
onToggle={(node) => toggleNode(node.id)}
|
||||||
|
onOpen={(node) => selectPage(node.page)}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<section className="workspace-content docs-workspace-content">
|
||||||
|
<div className="content-pad workspace-data-page docs-page">
|
||||||
|
<div className="page-heading split workspace-heading">
|
||||||
|
<div>
|
||||||
|
<PageTitle loading={loading}>i18n:govoplan-docs.help_center.f3f3a34b</PageTitle>
|
||||||
|
<p>{adminDocs ? "i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e" : "i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad"}</p>
|
||||||
|
</div>
|
||||||
|
<div className="button-row compact-actions">
|
||||||
|
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-docs.reload.cce71553</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||||
|
|
||||||
|
<LoadingFrame loading={loading} label="i18n:govoplan-docs.loading_documentation_context.1c091645">
|
||||||
|
<div className="docs-content">
|
||||||
|
<main className="docs-page-main">
|
||||||
|
<SelectedPageContent
|
||||||
|
page={selectedPage}
|
||||||
|
adminDocs={adminDocs}
|
||||||
|
documentationType={documentationType}
|
||||||
|
topicById={topicById}
|
||||||
|
moduleRows={moduleRows}
|
||||||
|
visibleRoutes={visibleRoutes}
|
||||||
|
availableRoutes={availableRoutes}
|
||||||
|
grantedPermissions={grantedPermissions}
|
||||||
|
evidenceModules={context?.layers.evidence.optional_modules ?? []}
|
||||||
|
evidenceSources={context?.layers.evidence.sources ?? []}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
<PageOutline items={outlineItems} />
|
||||||
|
</div>
|
||||||
|
</LoadingFrame>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function selectDocumentationType(type: DocumentationType) {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
params.set("type", type);
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPage(page: DocsPageNode) {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
params.set("topic", page.id);
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNode(nodeId: string) {
|
||||||
|
setExpandedNodes((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
if (next.has(nodeId)) {
|
||||||
|
next.delete(nodeId);
|
||||||
|
} else {
|
||||||
|
next.add(nodeId);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AudienceToggle({ selected, onSelect }: { selected: DocumentationType; onSelect: (type: DocumentationType) => void }) {
|
||||||
|
return (
|
||||||
|
<SegmentedControl
|
||||||
|
className="docs-audience-toggle"
|
||||||
|
role="group"
|
||||||
|
size="equal"
|
||||||
|
width="fill"
|
||||||
|
ariaLabel="i18n:govoplan-docs.documentation_type.5a66690c"
|
||||||
|
value={selected}
|
||||||
|
onChange={onSelect}
|
||||||
|
options={[
|
||||||
|
{ id: "admin", label: "i18n:govoplan-docs.admin_docs.bf504a56" },
|
||||||
|
{ id: "user", label: "i18n:govoplan-docs.user_docs.1e38e8d3" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageOutline({ items }: { items: OutlineItem[] }) {
|
||||||
|
return (
|
||||||
|
<aside className="docs-page-outline-box" aria-label="i18n:govoplan-docs.page_outline.e5940949">
|
||||||
|
<h2>i18n:govoplan-docs.page_outline.e5940949</h2>
|
||||||
|
{items.length ?
|
||||||
|
<nav>
|
||||||
|
{items.map((item) => <a key={item.id} href={`#${item.id}`}>{item.label}</a>)}
|
||||||
|
</nav> :
|
||||||
|
<p className="muted">i18n:govoplan-docs.no_outline_items.3ea2842e</p>
|
||||||
|
}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectedPageContent({
|
||||||
|
page,
|
||||||
|
adminDocs,
|
||||||
|
documentationType,
|
||||||
|
topicById,
|
||||||
|
moduleRows,
|
||||||
|
visibleRoutes,
|
||||||
|
availableRoutes,
|
||||||
|
grantedPermissions,
|
||||||
|
evidenceModules,
|
||||||
|
evidenceSources
|
||||||
|
}: {
|
||||||
|
page: DocsPageNode | null;
|
||||||
|
adminDocs: boolean;
|
||||||
|
documentationType: DocumentationType;
|
||||||
|
topicById: Map<string, DocsDocumentationTopic>;
|
||||||
|
moduleRows: DocsModule[];
|
||||||
|
visibleRoutes: DocsRoute[];
|
||||||
|
availableRoutes: DocsRoute[];
|
||||||
|
grantedPermissions: Array<{ scope: string; label: string; category: string }>;
|
||||||
|
evidenceModules: DocsOptionalModuleEvidence[];
|
||||||
|
evidenceSources: DocsSource[];
|
||||||
|
}) {
|
||||||
|
if (!page) {
|
||||||
|
return (
|
||||||
|
<section className="docs-section">
|
||||||
|
<h2>i18n:govoplan-docs.help_center.f3f3a34b</h2>
|
||||||
|
<p className="muted">i18n:govoplan-docs.no_system_topics_found.d01a8068</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (page.kind === "topic") {
|
||||||
|
return <TopicPage topic={page.topic} showTechnical={adminDocs} documentationType={documentationType} topicById={topicById} />;
|
||||||
|
}
|
||||||
|
if (page.kind === "admin-reference") {
|
||||||
|
return (
|
||||||
|
<section className="docs-section" id={page.id}>
|
||||||
|
<h2>{page.title}</h2>
|
||||||
|
<section id="docs-admin-modules" className="docs-reference-block">
|
||||||
|
<h3>i18n:govoplan-docs.modules.04e9462c</h3>
|
||||||
|
<ModuleTable modules={moduleRows} />
|
||||||
|
</section>
|
||||||
|
<section id="docs-admin-routes" className="docs-reference-block">
|
||||||
|
<h3>i18n:govoplan-docs.visible_routes.6c9b1605</h3>
|
||||||
|
<RouteTable routes={visibleRoutes} emptyText="i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b" />
|
||||||
|
<h3>i18n:govoplan-docs.available_routes.c2635868</h3>
|
||||||
|
<RouteTable routes={availableRoutes} emptyText="i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845" />
|
||||||
|
</section>
|
||||||
|
<section id="docs-admin-permissions" className="docs-reference-block">
|
||||||
|
<h3>i18n:govoplan-docs.granted_permissions.0a232e78</h3>
|
||||||
|
<PermissionList permissions={grantedPermissions} />
|
||||||
|
<EvidenceList modules={evidenceModules} sources={evidenceSources} />
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<section className="docs-section" id={page.id}>
|
||||||
|
<h2>{page.title}</h2>
|
||||||
|
<DocumentationTopicList topics={page.topics} emptyText={page.emptyText} showTechnical={adminDocs} documentationType={documentationType} topicById={topicById} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopicPage({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
return (
|
||||||
|
<article className={`docs-topic docs-topic-page docs-topic-${topic.kind}`} id={topicAnchorId(topic)}>
|
||||||
|
<div className="docs-topic-meta">
|
||||||
|
<StatusBadge status={topic.active ? "success" : "warning"} label={topic.active ? topic.kind : topic.target_layer} />
|
||||||
|
<span className="muted">{topic.source_module_id}</span>
|
||||||
|
{showTechnical && <span className="muted">{topic.i18n_key} · {topic.translation_locale}</span>}
|
||||||
|
</div>
|
||||||
|
<h2>{topic.title}</h2>
|
||||||
|
{topic.summary && <p id={`${topicAnchorId(topic)}-summary`} className="docs-topic-summary">{topic.summary}</p>}
|
||||||
|
<div id={`${topicAnchorId(topic)}-details`}>
|
||||||
|
<BodyText value={topic.body} />
|
||||||
|
<TopicMetadata topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />
|
||||||
|
{!topic.active && <UnavailableDocumentationReason topic={topic} />}
|
||||||
|
</div>
|
||||||
|
{!!topic.unlocks.length && <p className="muted">i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}</p>}
|
||||||
|
{!!topic.links.length && <TopicLinks topic={topic} />}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DocumentationTopicList({ topics, emptyText, showTechnical = false, documentationType, topicById }: { topics: DocsDocumentationTopic[]; emptyText: string; showTechnical?: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
if (!topics.length) return <p className="muted">{emptyText}</p>;
|
||||||
|
return (
|
||||||
|
<div className="docs-topic-list">
|
||||||
|
{topics.map((topic) =>
|
||||||
|
<article className={`docs-topic docs-topic-${topic.kind}`} id={topicAnchorId(topic)} key={`${topic.source_module_id}-${topic.id}`}>
|
||||||
|
<div className="docs-topic-meta">
|
||||||
|
<StatusBadge status={topic.active ? "success" : "warning"} label={topic.active ? topic.kind : topic.target_layer} />
|
||||||
|
<span className="muted">{topic.source_module_id}</span>
|
||||||
|
{showTechnical && <span className="muted">{topic.i18n_key} · {topic.translation_locale}</span>}
|
||||||
|
</div>
|
||||||
|
<h3>{topic.title}</h3>
|
||||||
|
{topic.summary && <p className="docs-topic-summary">{topic.summary}</p>}
|
||||||
|
<BodyText value={topic.body} />
|
||||||
|
<TopicMetadata topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />
|
||||||
|
{!topic.active && <UnavailableDocumentationReason topic={topic} />}
|
||||||
|
{!!topic.unlocks.length && <p className="muted">i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}</p>}
|
||||||
|
{!!topic.links.length && <TopicLinks topic={topic} />}
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopicMetadata({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
if (topic.kind === "workflow") return <WorkflowDetails topic={topic} documentationType={documentationType} topicById={topicById} />;
|
||||||
|
if (topic.kind === "reference") return <ReferenceDetails topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />;
|
||||||
|
if (topic.kind === "pattern") return <PatternDetails topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />;
|
||||||
|
return <RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkflowDetails({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
const prerequisites = metadataList(topic.metadata, "prerequisites");
|
||||||
|
const steps = metadataList(topic.metadata, "steps");
|
||||||
|
const outcome = metadataString(topic.metadata, "outcome");
|
||||||
|
const result = metadataString(topic.metadata, "result");
|
||||||
|
const verification = metadataString(topic.metadata, "verification");
|
||||||
|
const prefix = topicAnchorId(topic);
|
||||||
|
return (
|
||||||
|
<div className="docs-topic-details">
|
||||||
|
{outcome && <DetailBlock id={`${prefix}-outcome`} title="i18n:govoplan-docs.outcome.10172bd3" value={outcome} />}
|
||||||
|
{!!prerequisites.length && <DetailList id={`${prefix}-prerequisites`} title="i18n:govoplan-docs.prerequisites.fdf2407f" items={prerequisites} />}
|
||||||
|
{!!steps.length &&
|
||||||
|
<div className="docs-detail-block" id={`${prefix}-steps`}>
|
||||||
|
<h4>i18n:govoplan-docs.steps.6041435e</h4>
|
||||||
|
<ol>
|
||||||
|
{steps.map((step) => <li key={step}>{step}</li>)}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{result && <DetailBlock id={`${prefix}-result`} title="i18n:govoplan-docs.result.1f4fbf43" value={result} />}
|
||||||
|
{verification && <DetailBlock id={`${prefix}-verification`} title="i18n:govoplan-docs.verification.4b5f10dd" value={verification} />}
|
||||||
|
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReferenceDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
const route = metadataString(topic.metadata, "route");
|
||||||
|
const screen = metadataString(topic.metadata, "screen");
|
||||||
|
const section = metadataString(topic.metadata, "section");
|
||||||
|
const fields = metadataRecords(topic.metadata, "fields");
|
||||||
|
return (
|
||||||
|
<div className="docs-topic-details">
|
||||||
|
{(route || screen || section) &&
|
||||||
|
<dl className="detail-list compact">
|
||||||
|
{screen && <div><dt>i18n:govoplan-docs.screen.c4878ec4</dt><dd>{screen}</dd></div>}
|
||||||
|
{section && <div><dt>i18n:govoplan-docs.section.5e498158</dt><dd>{section}</dd></div>}
|
||||||
|
{showTechnical && route && <div><dt>i18n:govoplan-docs.route.4999528e</dt><dd>{route}</dd></div>}
|
||||||
|
</dl>
|
||||||
|
}
|
||||||
|
{!!fields.length && <ReferenceFieldTable id={`${topicAnchorId(topic)}-fields`} fields={fields} showTechnical={showTechnical} />}
|
||||||
|
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReferenceFieldTable({ id, fields, showTechnical }: { id: string; fields: Record<string, unknown>[]; showTechnical: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className="admin-table-wrap" id={id}>
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>i18n:govoplan-docs.field.7558c082</th>
|
||||||
|
<th>i18n:govoplan-docs.meaning.584d8aa0</th>
|
||||||
|
{showTechnical && <th>i18n:govoplan-docs.api_mapping.e969f6f7</th>}
|
||||||
|
{showTechnical && <th>i18n:govoplan-docs.permission.d71c9448</th>}
|
||||||
|
<th>i18n:govoplan-docs.validation.ef7f6d9c</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{fields.map((field) =>
|
||||||
|
<tr key={metadataString(field, "field_id") || metadataString(field, "label")}>
|
||||||
|
<td><strong>{metadataString(field, "label")}</strong></td>
|
||||||
|
<td>
|
||||||
|
{metadataString(field, "user_description")}
|
||||||
|
{showTechnical && metadataString(field, "admin_description") && <span className="muted block">{metadataString(field, "admin_description")}</span>}
|
||||||
|
{showTechnical && metadataString(field, "provenance") && <span className="muted block">{metadataString(field, "provenance")}</span>}
|
||||||
|
</td>
|
||||||
|
{showTechnical && <td>{metadataString(field, "api_path")}<span className="muted block">{metadataString(field, "api_field")}</span></td>}
|
||||||
|
{showTechnical && <td>{metadataString(field, "permission_scope") || "-"}</td>}
|
||||||
|
<td>{metadataString(field, "validation") || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PatternDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
const purpose = metadataString(topic.metadata, "purpose");
|
||||||
|
const whenUsed = metadataString(topic.metadata, "when_used");
|
||||||
|
const explanation = showTechnical ? metadataString(topic.metadata, "admin_explanation") : metadataString(topic.metadata, "user_explanation");
|
||||||
|
const componentRefs = metadataList(topic.metadata, "component_refs");
|
||||||
|
const prefix = topicAnchorId(topic);
|
||||||
|
return (
|
||||||
|
<div className="docs-topic-details">
|
||||||
|
{purpose && <DetailBlock id={`${prefix}-purpose`} title="i18n:govoplan-docs.purpose.4af40581" value={purpose} />}
|
||||||
|
{whenUsed && <DetailBlock id={`${prefix}-when-used`} title="i18n:govoplan-docs.when_used.b870b361" value={whenUsed} />}
|
||||||
|
{explanation && <DetailBlock id={`${prefix}-details`} title="i18n:govoplan-docs.details.a6b3c45f" value={explanation} />}
|
||||||
|
{showTechnical && !!componentRefs.length && <DetailList id={`${prefix}-components`} title="i18n:govoplan-docs.components.e81e57d8" items={componentRefs} />}
|
||||||
|
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailBlock({ id, title, value }: { id: string; title: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="docs-detail-block" id={id}>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<p>{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailList({ id, title, items }: { id: string; title: string; items: string[] }) {
|
||||||
|
return (
|
||||||
|
<div className="docs-detail-block" id={id}>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<ul>
|
||||||
|
{items.map((item) => <li key={item}>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RelatedTopics({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
||||||
|
const related = metadataList(topic.metadata, "related_topic_ids")
|
||||||
|
.map((id) => topicById.get(id))
|
||||||
|
.filter((item): item is DocsDocumentationTopic => Boolean(item));
|
||||||
|
if (!related.length) return null;
|
||||||
|
return (
|
||||||
|
<p className="docs-topic-links">
|
||||||
|
<span>i18n:govoplan-docs.related_topics.d5eb8b74 </span>
|
||||||
|
{related.map((item, index) =>
|
||||||
|
<span key={item.id}>
|
||||||
|
{index > 0 && " | "}
|
||||||
|
<Link to={topicPageHref(item, documentationType)}>{item.title}</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopicLinks({ topic }: { topic: DocsDocumentationTopic }) {
|
||||||
|
return (
|
||||||
|
<p className="docs-topic-links">
|
||||||
|
{topic.links.map((link, index) =>
|
||||||
|
<span key={link.href}>
|
||||||
|
{index > 0 && " | "}
|
||||||
|
<a href={link.href} target={link.kind === "public" ? "_blank" : undefined} rel={link.kind === "public" ? "noreferrer" : undefined}>{link.label}</a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BodyText({ value }: { value: string }) {
|
||||||
|
if (!value) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{value.split("\n").filter(Boolean).map((line) => <p className="docs-topic-body" key={line}>{line}</p>)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnavailableDocumentationReason({ topic }: { topic: DocsDocumentationTopic }) {
|
||||||
|
const rows = [
|
||||||
|
["i18n:govoplan-docs.modules.04e9462c", topic.blockers.modules],
|
||||||
|
["i18n:govoplan-docs.capabilities.ca09c54b", topic.blockers.capabilities],
|
||||||
|
["i18n:govoplan-docs.permissions.d06d5557", topic.blockers.scopes]
|
||||||
|
].filter(([, values]) => Array.isArray(values) && values.length);
|
||||||
|
return (
|
||||||
|
<div className="docs-unavailable-reason">
|
||||||
|
<p className="muted">{topic.reason}</p>
|
||||||
|
{!!rows.length &&
|
||||||
|
<dl className="detail-list compact">
|
||||||
|
{rows.map(([label, values]) =>
|
||||||
|
<div key={String(label)}>
|
||||||
|
<dt>{label}</dt>
|
||||||
|
<dd>{(values as string[]).join(", ")}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function docsTreeNodes(context: DocsContext | null, adminDocs: boolean): DocsTreeNode[] {
|
||||||
|
const allTopics = allDocumentationTopics(context);
|
||||||
|
const systemTopics = context?.topic_groups.system ?? [];
|
||||||
|
const workflowTopics = context?.topic_groups.workflow ?? [];
|
||||||
|
const referenceTopics = context?.topic_groups.reference ?? [];
|
||||||
|
const patternTopics = context?.topic_groups.pattern ?? [];
|
||||||
|
const systemTopicIds = new Set(systemTopics.map((topic) => topic.id));
|
||||||
|
const moduleTopics = allTopics.filter((topic) => !systemTopicIds.has(topic.id));
|
||||||
|
const workingTopics = uniqueTopics([...patternTopics, ...workflowTopics]);
|
||||||
|
const troubleshootingTopics = allTopics.filter((topic) => !topic.active || topic.layer === "evidence");
|
||||||
|
const basicsNode = categoryTreeNode(
|
||||||
|
"basics",
|
||||||
|
"i18n:govoplan-docs.basics.5fcebeef",
|
||||||
|
patternTopics,
|
||||||
|
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6"
|
||||||
|
);
|
||||||
|
const commonTasksNode = categoryTreeNode(
|
||||||
|
"common-tasks",
|
||||||
|
"i18n:govoplan-docs.common_tasks.9f825c48",
|
||||||
|
workflowTopics,
|
||||||
|
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf"
|
||||||
|
);
|
||||||
|
const nodes: DocsTreeNode[] = [
|
||||||
|
categoryTreeNode(
|
||||||
|
"about-govoplan",
|
||||||
|
"i18n:govoplan-docs.about_govoplan.6b2d7127",
|
||||||
|
systemTopics,
|
||||||
|
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: "tree:working-with-govoplan",
|
||||||
|
title: "i18n:govoplan-docs.working_with_govoplan.81908ef4",
|
||||||
|
page: {
|
||||||
|
id: "working-with-govoplan:overview",
|
||||||
|
title: "i18n:govoplan-docs.working_with_govoplan.81908ef4",
|
||||||
|
kind: "topic-list",
|
||||||
|
topics: workingTopics,
|
||||||
|
emptyText: "i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384"
|
||||||
|
},
|
||||||
|
children: [basicsNode, commonTasksNode]
|
||||||
|
},
|
||||||
|
categoryTreeNode(
|
||||||
|
"advanced",
|
||||||
|
"i18n:govoplan-docs.advanced.203a8d6b",
|
||||||
|
referenceTopics,
|
||||||
|
"i18n:govoplan-docs.no_advanced_topics_found.271166c9"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: "tree:modules",
|
||||||
|
title: "i18n:govoplan-docs.modules.04e9462c",
|
||||||
|
page: {
|
||||||
|
id: "modules:overview",
|
||||||
|
title: "i18n:govoplan-docs.modules.04e9462c",
|
||||||
|
kind: "topic-list",
|
||||||
|
topics: moduleTopics,
|
||||||
|
emptyText: "i18n:govoplan-docs.no_module_topics_found.0cbbdc9b"
|
||||||
|
},
|
||||||
|
children: moduleTreeNodes(moduleTopics, context)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (adminDocs) {
|
||||||
|
nodes.push({
|
||||||
|
id: "tree:administration",
|
||||||
|
title: "i18n:govoplan-docs.administration.101aedaf",
|
||||||
|
page: {
|
||||||
|
id: "administration:overview",
|
||||||
|
title: "i18n:govoplan-docs.administration.101aedaf",
|
||||||
|
kind: "topic-list",
|
||||||
|
topics: referenceTopics,
|
||||||
|
emptyText: "i18n:govoplan-docs.no_administration_topics_found.6213dff2"
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
...referenceTopics.map(topicTreeNode),
|
||||||
|
{
|
||||||
|
id: "tree:administration:technical-reference",
|
||||||
|
title: "i18n:govoplan-docs.technical_reference.f271430d",
|
||||||
|
page: {
|
||||||
|
id: "system:technical-reference",
|
||||||
|
title: "i18n:govoplan-docs.technical_reference.f271430d",
|
||||||
|
kind: "admin-reference"
|
||||||
|
},
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (troubleshootingTopics.length) {
|
||||||
|
nodes.push(categoryTreeNode(
|
||||||
|
"troubleshooting",
|
||||||
|
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434",
|
||||||
|
troubleshootingTopics,
|
||||||
|
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function categoryTreeNode(id: string, title: string, topics: DocsDocumentationTopic[], emptyText: string): DocsTreeNode {
|
||||||
|
const sortedTopics = [...topics].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
|
||||||
|
return {
|
||||||
|
id: `tree:${id}`,
|
||||||
|
title,
|
||||||
|
page: {
|
||||||
|
id: `${id}:overview`,
|
||||||
|
title,
|
||||||
|
kind: "topic-list",
|
||||||
|
topics: sortedTopics,
|
||||||
|
emptyText
|
||||||
|
},
|
||||||
|
children: nestedTopicNodes(sortedTopics)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function topicPage(topic: DocsDocumentationTopic): DocsPageNode {
|
||||||
|
return {
|
||||||
|
id: topic.id,
|
||||||
|
title: topic.title,
|
||||||
|
kind: "topic",
|
||||||
|
topic
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function topicTreeNode(topic: DocsDocumentationTopic): DocsTreeNode {
|
||||||
|
return {
|
||||||
|
id: `tree:topic:${topic.id}`,
|
||||||
|
title: topic.title,
|
||||||
|
page: topicPage(topic),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleTreeNodes(topics: DocsDocumentationTopic[], context: DocsContext | null): DocsTreeNode[] {
|
||||||
|
const topicsByModule = new Map<string, DocsDocumentationTopic[]>();
|
||||||
|
for (const topic of topics) {
|
||||||
|
const moduleTopics = topicsByModule.get(topic.source_module_id) ?? [];
|
||||||
|
moduleTopics.push(topic);
|
||||||
|
topicsByModule.set(topic.source_module_id, moduleTopics);
|
||||||
|
}
|
||||||
|
const configuredModules = context?.layers.configured.modules ?? [];
|
||||||
|
const moduleOrder = new Map(configuredModules.map((module, index) => [module.id, index]));
|
||||||
|
return Array.from(topicsByModule.entries())
|
||||||
|
.sort(([left], [right]) => (moduleOrder.get(left) ?? 999) - (moduleOrder.get(right) ?? 999) || moduleTitle(left, context).localeCompare(moduleTitle(right, context)))
|
||||||
|
.map(([moduleId, moduleTopics]) => {
|
||||||
|
const sortedTopics = [...moduleTopics].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
|
||||||
|
const title = moduleTitle(moduleId, context);
|
||||||
|
return {
|
||||||
|
id: `tree:module:${moduleId}`,
|
||||||
|
title,
|
||||||
|
page: {
|
||||||
|
id: `module:${moduleId}`,
|
||||||
|
title,
|
||||||
|
kind: "topic-list",
|
||||||
|
topics: sortedTopics,
|
||||||
|
emptyText: "i18n:govoplan-docs.no_system_topics_found.d01a8068"
|
||||||
|
},
|
||||||
|
children: nestedTopicNodes(sortedTopics)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nestedTopicNodes(topics: DocsDocumentationTopic[]): DocsTreeNode[] {
|
||||||
|
const nodes = new Map(topics.map((topic) => [topic.id, topicTreeNode(topic)]));
|
||||||
|
const roots: DocsTreeNode[] = [];
|
||||||
|
for (const topic of topics) {
|
||||||
|
const node = nodes.get(topic.id);
|
||||||
|
if (!node) continue;
|
||||||
|
const parentId = topicParentId(topic);
|
||||||
|
const parent = parentId ? nodes.get(parentId) : undefined;
|
||||||
|
if (parent && parentId !== topic.id) {
|
||||||
|
parent.children.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
function topicParentId(topic: DocsDocumentationTopic): string {
|
||||||
|
return metadataString(topic.metadata, "tree_parent_id") || metadataString(topic.metadata, "parent_topic_id") || metadataString(topic.metadata, "parent_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleTitle(moduleId: string, context: DocsContext | null): string {
|
||||||
|
const module = context?.layers.configured.modules.find((item) => item.id === moduleId);
|
||||||
|
if (module) return module.name;
|
||||||
|
return moduleId
|
||||||
|
.replace(/^govoplan[-_]/, "")
|
||||||
|
.split(/[-_]+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(" ") || moduleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenTreePages(nodes: DocsTreeNode[]): DocsPageNode[] {
|
||||||
|
return nodes.flatMap((node) => [node.page, ...flattenTreePages(node.children)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultExpandedNodeIds(nodes: DocsTreeNode[]): string[] {
|
||||||
|
return nodes.filter((node) => node.children.length).map((node) => node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ancestorNodeIdsForPage(nodes: DocsTreeNode[], pageId: string, ancestors: string[] = []): string[] {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.page.id === pageId) return ancestors;
|
||||||
|
const match = ancestorNodeIdsForPage(node.children, pageId, [...ancestors, node.id]);
|
||||||
|
if (match.length) return match;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeNodeIdForPage(nodes: DocsTreeNode[], pageId: string): string {
|
||||||
|
if (!pageId) return "";
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.page.id === pageId) return node.id;
|
||||||
|
const childId = activeNodeIdForPage(node.children, pageId);
|
||||||
|
if (childId) return childId;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function allDocumentationTopics(context: DocsContext | null): DocsDocumentationTopic[] {
|
||||||
|
const topics = Object.values(context?.topic_groups ?? {}).flat();
|
||||||
|
return uniqueTopics(topics).sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueTopics(topics: DocsDocumentationTopic[]): DocsDocumentationTopic[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return topics.filter((topic) => {
|
||||||
|
if (seen.has(topic.id)) return false;
|
||||||
|
seen.add(topic.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedPageFromSearch(search: string, pages: DocsPageNode[]): DocsPageNode | null {
|
||||||
|
if (!pages.length) return null;
|
||||||
|
const requested = new URLSearchParams(search).get("topic") || "";
|
||||||
|
return pages.find((page) => page.id === requested) ?? pages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function outlineForPage(page: DocsPageNode | null, showTechnical: boolean): OutlineItem[] {
|
||||||
|
if (!page) return [];
|
||||||
|
if (page.kind === "admin-reference") {
|
||||||
|
return [
|
||||||
|
{ id: page.id, label: page.title },
|
||||||
|
{ id: "docs-admin-modules", label: "i18n:govoplan-docs.modules.04e9462c" },
|
||||||
|
{ id: "docs-admin-routes", label: "i18n:govoplan-docs.routes.03730e58" },
|
||||||
|
{ id: "docs-admin-permissions", label: "i18n:govoplan-docs.permissions.d06d5557" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (page.kind === "topic-list") {
|
||||||
|
return [
|
||||||
|
{ id: page.id, label: page.title },
|
||||||
|
...page.topics.map((topic) => ({ id: topicAnchorId(topic), label: topic.title }))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const topic = page.topic;
|
||||||
|
const prefix = topicAnchorId(topic);
|
||||||
|
const items: OutlineItem[] = [{ id: prefix, label: topic.title }];
|
||||||
|
if (topic.summary) items.push({ id: `${prefix}-summary`, label: "i18n:govoplan-docs.summary.d6b9936d" });
|
||||||
|
if (topic.kind === "workflow") {
|
||||||
|
if (metadataString(topic.metadata, "outcome")) items.push({ id: `${prefix}-outcome`, label: "i18n:govoplan-docs.outcome.10172bd3" });
|
||||||
|
if (metadataList(topic.metadata, "prerequisites").length) items.push({ id: `${prefix}-prerequisites`, label: "i18n:govoplan-docs.prerequisites.fdf2407f" });
|
||||||
|
if (metadataList(topic.metadata, "steps").length) items.push({ id: `${prefix}-steps`, label: "i18n:govoplan-docs.steps.6041435e" });
|
||||||
|
if (metadataString(topic.metadata, "result")) items.push({ id: `${prefix}-result`, label: "i18n:govoplan-docs.result.1f4fbf43" });
|
||||||
|
if (metadataString(topic.metadata, "verification")) items.push({ id: `${prefix}-verification`, label: "i18n:govoplan-docs.verification.4b5f10dd" });
|
||||||
|
}
|
||||||
|
if (topic.kind === "reference" && metadataRecords(topic.metadata, "fields").length) {
|
||||||
|
items.push({ id: `${prefix}-fields`, label: "i18n:govoplan-docs.field.7558c082" });
|
||||||
|
}
|
||||||
|
if (topic.kind === "pattern") {
|
||||||
|
if (metadataString(topic.metadata, "purpose")) items.push({ id: `${prefix}-purpose`, label: "i18n:govoplan-docs.purpose.4af40581" });
|
||||||
|
if (metadataString(topic.metadata, "when_used")) items.push({ id: `${prefix}-when-used`, label: "i18n:govoplan-docs.when_used.b870b361" });
|
||||||
|
if (showTechnical && metadataList(topic.metadata, "component_refs").length) items.push({ id: `${prefix}-components`, label: "i18n:govoplan-docs.components.e81e57d8" });
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentationTypeFromSearch(search: string): DocumentationType {
|
||||||
|
return new URLSearchParams(search).get("type") === "user" ? "user" : "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
function localeFromSearch(search: string): string | null {
|
||||||
|
const value = new URLSearchParams(search).get("locale");
|
||||||
|
if (!value) return null;
|
||||||
|
return value.toLowerCase().startsWith("de") ? "de" : "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
function topicAnchorId(topic: DocsDocumentationTopic): string {
|
||||||
|
return topic.anchor_id || `docs-topic-${topic.source_module_id}-${topic.id}`.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function topicPageHref(topic: DocsDocumentationTopic, documentationType: DocumentationType): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("type", documentationType);
|
||||||
|
params.set("topic", topic.id);
|
||||||
|
return `?${params.toString()}#${topicAnchorId(topic)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function topicIndex(context: DocsContext | null): Map<string, DocsDocumentationTopic> {
|
||||||
|
const index = new Map<string, DocsDocumentationTopic>();
|
||||||
|
for (const topic of allDocumentationTopics(context)) {
|
||||||
|
index.set(topic.id, topic);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataString(metadata: Record<string, unknown>, key: string): string {
|
||||||
|
const value = metadata[key];
|
||||||
|
return typeof value === "string" ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataList(metadata: Record<string, unknown>, key: string): string[] {
|
||||||
|
const value = metadata[key];
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.filter((item): item is string => typeof item === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataRecords(metadata: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
||||||
|
const value = metadata[key];
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModuleTable({ modules }: { modules: DocsModule[] }) {
|
||||||
|
if (!modules.length) return <p className="muted">i18n:govoplan-docs.no_configured_modules_found.f6f9ce24</p>;
|
||||||
|
return (
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>i18n:govoplan-docs.module.b8ff0289</th><th>i18n:govoplan-docs.routes.03730e58</th><th>i18n:govoplan-docs.permissions.d06d5557</th><th>i18n:govoplan-docs.frontend.152d1cf2</th><th>i18n:govoplan-docs.capabilities.ca09c54b</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{modules.map((module) =>
|
||||||
|
<tr key={module.id}>
|
||||||
|
<td><strong>{module.name}</strong><span className="muted block">{module.id} {module.version}</span></td>
|
||||||
|
<td>{module.route_count} i18n:govoplan-docs.route.200e2a66 {module.nav_count} nav</td>
|
||||||
|
<td>{module.permission_count}</td>
|
||||||
|
<td>{module.frontend_package || "-"}</td>
|
||||||
|
<td>{module.capabilities.length ? module.capabilities.join(", ") : "-"}{module.documentation_count || module.documentation_provider_count ? <span className="muted block">{module.documentation_count} i18n:govoplan-docs.docs.5dc1e9b8 {module.documentation_provider_count} provider</span> : null}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteTable({ routes, emptyText }: { routes: DocsRoute[]; emptyText: string }) {
|
||||||
|
if (!routes.length) return <p className="muted">{emptyText}</p>;
|
||||||
|
return (
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>i18n:govoplan-docs.route.4999528e</th><th>i18n:govoplan-docs.module.b8ff0289</th><th>i18n:govoplan-docs.source.6da13add</th><th>i18n:govoplan-docs.requirements.09a428f9</th><th>i18n:govoplan-docs.status.bae7d5be</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{routes.map((route) =>
|
||||||
|
<tr key={`${route.source}-${route.module_id}-${route.path}`}>
|
||||||
|
<td><strong>{route.label}</strong><span className="muted block">{route.path}</span></td>
|
||||||
|
<td>{route.module_id}</td>
|
||||||
|
<td>{route.source}</td>
|
||||||
|
<td>{routeRequirements(route)}</td>
|
||||||
|
<td><StatusBadge status={route.visible ? "success" : "warning"} label={route.visible ? "visible" : route.reason} /></td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PermissionList({ permissions }: { permissions: Array<{ scope: string; label: string; category: string }> }) {
|
||||||
|
if (!permissions.length) return <p className="muted">i18n:govoplan-docs.no_granted_platform_permissions_found.36010898</p>;
|
||||||
|
return (
|
||||||
|
<dl className="detail-list">
|
||||||
|
{permissions.slice(0, 24).map((permission) =>
|
||||||
|
<div key={permission.scope}>
|
||||||
|
<dt>{permission.category}</dt>
|
||||||
|
<dd><strong>{permission.label}</strong><span className="muted"> · {permission.scope}</span></dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{permissions.length > 24 && <div><dt>i18n:govoplan-docs.more.4bab2d8f</dt><dd>{permissions.length - 24} i18n:govoplan-docs.additional_permissions.8042cb01</dd></div>}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidence[]; sources: DocsSource[] }) {
|
||||||
|
if (!modules.length && !sources.length) return <p className="muted">i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6</p>;
|
||||||
|
return (
|
||||||
|
<dl className="detail-list">
|
||||||
|
{modules.map((item) =>
|
||||||
|
<div key={`${item.source_module_id}-${item.module_id}`}>
|
||||||
|
<dt><StatusBadge status={item.status === "installed" ? "success" : "inactive"} label={item.status} /></dt>
|
||||||
|
<dd><strong>{item.module_id}</strong><span className="muted"> · {item.reason}</span></dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sources.map((item) =>
|
||||||
|
<div key={item.source}>
|
||||||
|
<dt>{item.layer}</dt>
|
||||||
|
<dd><strong>{item.label}</strong><span className="muted"> · {item.source}</span></dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeRequirements(route: DocsRoute): string {
|
||||||
|
const parts = [];
|
||||||
|
if (route.required_all.length) parts.push(`all: ${route.required_all.join(", ")}`);
|
||||||
|
if (route.required_any.length) parts.push(`any: ${route.required_any.join(", ")}`);
|
||||||
|
return parts.join(" | ") || "-";
|
||||||
|
}
|
||||||
186
webui/src/i18n/generatedTranslations.ts
Normal file
186
webui/src/i18n/generatedTranslations.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
|
"en": {
|
||||||
|
"i18n:govoplan-docs.additional_permissions.8042cb01": "additional permissions",
|
||||||
|
"i18n:govoplan-docs.about_govoplan.6b2d7127": "About GovOPlaN",
|
||||||
|
"i18n:govoplan-docs.admin_docs.bf504a56": "Admin docs",
|
||||||
|
"i18n:govoplan-docs.administration.101aedaf": "Administration",
|
||||||
|
"i18n:govoplan-docs.advanced.203a8d6b": "Advanced",
|
||||||
|
"i18n:govoplan-docs.available_documentation.db8bcc42": "Available documentation",
|
||||||
|
"i18n:govoplan-docs.available_routes.c2635868": "Available routes",
|
||||||
|
"i18n:govoplan-docs.basics.5fcebeef": "Basics",
|
||||||
|
"i18n:govoplan-docs.capabilities.ca09c54b": "Capabilities",
|
||||||
|
"i18n:govoplan-docs.common_tasks.9f825c48": "Common tasks",
|
||||||
|
"i18n:govoplan-docs.configured_documentation.fb6eaaad": "Configured documentation",
|
||||||
|
"i18n:govoplan-docs.docs.5dc1e9b8": "docs,",
|
||||||
|
"i18n:govoplan-docs.docs.68a41942": "Docs",
|
||||||
|
"i18n:govoplan-docs.docs.e6706405": "DOCS",
|
||||||
|
"i18n:govoplan-docs.api_mapping.e969f6f7": "API mapping",
|
||||||
|
"i18n:govoplan-docs.components.e81e57d8": "Components",
|
||||||
|
"i18n:govoplan-docs.details.a6b3c45f": "Details",
|
||||||
|
"i18n:govoplan-docs.documentation_mode.35f872f3": "Documentation mode",
|
||||||
|
"i18n:govoplan-docs.documentation_outline.6f836b99": "Documentation outline",
|
||||||
|
"i18n:govoplan-docs.documentation_type.5a66690c": "Documentation type",
|
||||||
|
"i18n:govoplan-docs.evidence.7ea014de": "Evidence",
|
||||||
|
"i18n:govoplan-docs.field.7558c082": "Field",
|
||||||
|
"i18n:govoplan-docs.frontend.152d1cf2": "Frontend",
|
||||||
|
"i18n:govoplan-docs.granted_permissions.0a232e78": "Granted permissions",
|
||||||
|
"i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Guidance for the functions available in this installation.",
|
||||||
|
"i18n:govoplan-docs.help_center.f3f3a34b": "Help Center",
|
||||||
|
"i18n:govoplan-docs.loading_documentation_context.1c091645": "Loading documentation context...",
|
||||||
|
"i18n:govoplan-docs.meaning.584d8aa0": "Meaning",
|
||||||
|
"i18n:govoplan-docs.module.b8ff0289": "Module",
|
||||||
|
"i18n:govoplan-docs.modules.04e9462c": "Modules",
|
||||||
|
"i18n:govoplan-docs.more_examples.383f0e0e": "More examples",
|
||||||
|
"i18n:govoplan-docs.more.4bab2d8f": "More",
|
||||||
|
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d": "No GovOPlaN overview topics found.",
|
||||||
|
"i18n:govoplan-docs.no_administration_topics_found.6213dff2": "No administration topics found.",
|
||||||
|
"i18n:govoplan-docs.no_advanced_topics_found.271166c9": "No advanced topics found.",
|
||||||
|
"i18n:govoplan-docs.no_always_present_documentation_sections_found.3b89b31e": "No always-present documentation sections found.",
|
||||||
|
"i18n:govoplan-docs.no_available_documentation_topics_found.38b84a6d": "No available documentation topics found.",
|
||||||
|
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6": "No basic topics found.",
|
||||||
|
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf": "No common task topics found.",
|
||||||
|
"i18n:govoplan-docs.no_configured_documentation_topics_found.04dcfb92": "No configured documentation topics found.",
|
||||||
|
"i18n:govoplan-docs.no_configured_modules_found.f6f9ce24": "No configured modules found.",
|
||||||
|
"i18n:govoplan-docs.no_evidence_documentation_topics_found.c59bd849": "No evidence documentation topics found.",
|
||||||
|
"i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6": "No evidence sources found.",
|
||||||
|
"i18n:govoplan-docs.no_granted_platform_permissions_found.36010898": "No granted platform permissions found.",
|
||||||
|
"i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845": "No hidden installed routes found.",
|
||||||
|
"i18n:govoplan-docs.no_module_topics_found.0cbbdc9b": "No module topics found.",
|
||||||
|
"i18n:govoplan-docs.no_outline_items.3ea2842e": "No outline items for this page.",
|
||||||
|
"i18n:govoplan-docs.no_pattern_topics_found.35b70eee": "No design-pattern topics found.",
|
||||||
|
"i18n:govoplan-docs.no_reference_topics_found.724d5a68": "No reference topics found.",
|
||||||
|
"i18n:govoplan-docs.no_system_topics_found.d01a8068": "No system documentation topics found.",
|
||||||
|
"i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b": "No visible routes found for this actor.",
|
||||||
|
"i18n:govoplan-docs.no_workflow_topics_found.a82b52da": "No workflow topics found.",
|
||||||
|
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468": "No troubleshooting topics found.",
|
||||||
|
"i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384": "No working-with-GovOPlaN topics found.",
|
||||||
|
"i18n:govoplan-docs.outcome.10172bd3": "Outcome",
|
||||||
|
"i18n:govoplan-docs.page_outline.e5940949": "On this page",
|
||||||
|
"i18n:govoplan-docs.patterns.a2bc4d8f": "Patterns",
|
||||||
|
"i18n:govoplan-docs.permission.d71c9448": "Permission",
|
||||||
|
"i18n:govoplan-docs.permissions.d06d5557": "Permissions",
|
||||||
|
"i18n:govoplan-docs.prerequisites.fdf2407f": "Prerequisites",
|
||||||
|
"i18n:govoplan-docs.purpose.4af40581": "Purpose",
|
||||||
|
"i18n:govoplan-docs.reference.1fd9d50a": "Reference",
|
||||||
|
"i18n:govoplan-docs.related_topics.d5eb8b74": "Related topics:",
|
||||||
|
"i18n:govoplan-docs.reload.cce71553": "Reload",
|
||||||
|
"i18n:govoplan-docs.requirements.09a428f9": "Requirements",
|
||||||
|
"i18n:govoplan-docs.result.1f4fbf43": "Result",
|
||||||
|
"i18n:govoplan-docs.route.200e2a66": "route,",
|
||||||
|
"i18n:govoplan-docs.route.4999528e": "Route",
|
||||||
|
"i18n:govoplan-docs.routes.03730e58": "Routes",
|
||||||
|
"i18n:govoplan-docs.screen.c4878ec4": "Screen",
|
||||||
|
"i18n:govoplan-docs.section.5e498158": "Section",
|
||||||
|
"i18n:govoplan-docs.source.6da13add": "Source",
|
||||||
|
"i18n:govoplan-docs.status.bae7d5be": "Status",
|
||||||
|
"i18n:govoplan-docs.steps.6041435e": "Steps",
|
||||||
|
"i18n:govoplan-docs.summary.d6b9936d": "Summary",
|
||||||
|
"i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technical documentation for the modules and configuration active in this installation.",
|
||||||
|
"i18n:govoplan-docs.technical_reference.f271430d": "Technical reference",
|
||||||
|
"i18n:govoplan-docs.this_system.b13a51ad": "This system",
|
||||||
|
"i18n:govoplan-docs.toggle_tree_node.4070b59f": "Expand or collapse",
|
||||||
|
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434": "Troubleshooting and support",
|
||||||
|
"i18n:govoplan-docs.unlocks.67d569db": "Unlocks:",
|
||||||
|
"i18n:govoplan-docs.user_docs.1e38e8d3": "User docs",
|
||||||
|
"i18n:govoplan-docs.validation.ef7f6d9c": "Validation",
|
||||||
|
"i18n:govoplan-docs.verification.4b5f10dd": "Verification",
|
||||||
|
"i18n:govoplan-docs.visible_routes.6c9b1605": "Visible routes",
|
||||||
|
"i18n:govoplan-docs.when_used.b870b361": "When used",
|
||||||
|
"i18n:govoplan-docs.working_with_govoplan.81908ef4": "Working with GovOPlaN",
|
||||||
|
"i18n:govoplan-docs.workflows.4f4a16d1": "Workflows",
|
||||||
|
"i18n:govoplan-docs.your_documentation.8a4cd9a3": "Your documentation"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"i18n:govoplan-docs.additional_permissions.8042cb01": "additional permissions",
|
||||||
|
"i18n:govoplan-docs.about_govoplan.6b2d7127": "Über GovOPlaN",
|
||||||
|
"i18n:govoplan-docs.admin_docs.bf504a56": "Administrationsdokumentation",
|
||||||
|
"i18n:govoplan-docs.administration.101aedaf": "Administration",
|
||||||
|
"i18n:govoplan-docs.advanced.203a8d6b": "Fortgeschritten",
|
||||||
|
"i18n:govoplan-docs.available_documentation.db8bcc42": "Verfügbare Dokumentation",
|
||||||
|
"i18n:govoplan-docs.available_routes.c2635868": "Verfügbare Routen",
|
||||||
|
"i18n:govoplan-docs.basics.5fcebeef": "Grundlagen",
|
||||||
|
"i18n:govoplan-docs.capabilities.ca09c54b": "Fähigkeiten",
|
||||||
|
"i18n:govoplan-docs.common_tasks.9f825c48": "Häufige Aufgaben",
|
||||||
|
"i18n:govoplan-docs.configured_documentation.fb6eaaad": "Configured documentation",
|
||||||
|
"i18n:govoplan-docs.docs.5dc1e9b8": "docs,",
|
||||||
|
"i18n:govoplan-docs.docs.68a41942": "Dokumentation",
|
||||||
|
"i18n:govoplan-docs.docs.e6706405": "DOCS",
|
||||||
|
"i18n:govoplan-docs.api_mapping.e969f6f7": "API-Zuordnung",
|
||||||
|
"i18n:govoplan-docs.components.e81e57d8": "Komponenten",
|
||||||
|
"i18n:govoplan-docs.details.a6b3c45f": "Details",
|
||||||
|
"i18n:govoplan-docs.documentation_mode.35f872f3": "Dokumentationsmodus",
|
||||||
|
"i18n:govoplan-docs.documentation_outline.6f836b99": "Dokumentationsübersicht",
|
||||||
|
"i18n:govoplan-docs.documentation_type.5a66690c": "Dokumentationstyp",
|
||||||
|
"i18n:govoplan-docs.evidence.7ea014de": "Evidence",
|
||||||
|
"i18n:govoplan-docs.field.7558c082": "Feld",
|
||||||
|
"i18n:govoplan-docs.frontend.152d1cf2": "Frontend",
|
||||||
|
"i18n:govoplan-docs.granted_permissions.0a232e78": "Gewährte Berechtigungen",
|
||||||
|
"i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Anleitung für die in dieser Installation verfügbaren Funktionen.",
|
||||||
|
"i18n:govoplan-docs.help_center.f3f3a34b": "Hilfezentrum",
|
||||||
|
"i18n:govoplan-docs.loading_documentation_context.1c091645": "Dokumentationskontext wird geladen...",
|
||||||
|
"i18n:govoplan-docs.meaning.584d8aa0": "Bedeutung",
|
||||||
|
"i18n:govoplan-docs.module.b8ff0289": "Modul",
|
||||||
|
"i18n:govoplan-docs.modules.04e9462c": "Module",
|
||||||
|
"i18n:govoplan-docs.more_examples.383f0e0e": "More examples",
|
||||||
|
"i18n:govoplan-docs.more.4bab2d8f": "Weitere",
|
||||||
|
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d": "Keine GovOPlaN-Übersichtsthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_administration_topics_found.6213dff2": "Keine Administrationsthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_advanced_topics_found.271166c9": "Keine fortgeschrittenen Themen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_always_present_documentation_sections_found.3b89b31e": "Keine immer vorhandenen Dokumentationsabschnitte gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_available_documentation_topics_found.38b84a6d": "Keine verfügbaren Dokumentationsthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6": "Keine Grundlagenthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf": "Keine Themen zu häufigen Aufgaben gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_configured_documentation_topics_found.04dcfb92": "Keine konfigurierten Dokumentationsthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_configured_modules_found.f6f9ce24": "Keine konfigurierten Module gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_evidence_documentation_topics_found.c59bd849": "Keine Nachweis-Dokumentationsthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6": "Keine Nachweisquellen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_granted_platform_permissions_found.36010898": "Keine gewährten Plattformberechtigungen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845": "Keine ausgeblendeten installierten Routen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_module_topics_found.0cbbdc9b": "Keine Modulthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_outline_items.3ea2842e": "Keine Gliederungspunkte für diese Seite.",
|
||||||
|
"i18n:govoplan-docs.no_pattern_topics_found.35b70eee": "Keine Themen zu Entwurfsmustern gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_reference_topics_found.724d5a68": "Keine Referenzthemen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_system_topics_found.d01a8068": "Keine Systemdokumentation gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b": "Keine sichtbaren Routen für diesen Akteur gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_workflow_topics_found.a82b52da": "Keine Ablaufanleitungen gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468": "Keine Themen zu Fehlerbehebung gefunden.",
|
||||||
|
"i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384": "Keine Themen zum Arbeiten mit GovOPlaN gefunden.",
|
||||||
|
"i18n:govoplan-docs.outcome.10172bd3": "Ziel",
|
||||||
|
"i18n:govoplan-docs.page_outline.e5940949": "Auf dieser Seite",
|
||||||
|
"i18n:govoplan-docs.patterns.a2bc4d8f": "Muster",
|
||||||
|
"i18n:govoplan-docs.permission.d71c9448": "Berechtigung",
|
||||||
|
"i18n:govoplan-docs.permissions.d06d5557": "Berechtigungen",
|
||||||
|
"i18n:govoplan-docs.prerequisites.fdf2407f": "Voraussetzungen",
|
||||||
|
"i18n:govoplan-docs.purpose.4af40581": "Zweck",
|
||||||
|
"i18n:govoplan-docs.reference.1fd9d50a": "Referenz",
|
||||||
|
"i18n:govoplan-docs.related_topics.d5eb8b74": "Verwandte Themen:",
|
||||||
|
"i18n:govoplan-docs.reload.cce71553": "Neu laden",
|
||||||
|
"i18n:govoplan-docs.requirements.09a428f9": "Anforderungen",
|
||||||
|
"i18n:govoplan-docs.result.1f4fbf43": "Ergebnis",
|
||||||
|
"i18n:govoplan-docs.route.200e2a66": "route,",
|
||||||
|
"i18n:govoplan-docs.route.4999528e": "Route",
|
||||||
|
"i18n:govoplan-docs.routes.03730e58": "Routen",
|
||||||
|
"i18n:govoplan-docs.screen.c4878ec4": "Ansicht",
|
||||||
|
"i18n:govoplan-docs.section.5e498158": "Bereich",
|
||||||
|
"i18n:govoplan-docs.source.6da13add": "Quelle",
|
||||||
|
"i18n:govoplan-docs.status.bae7d5be": "Status",
|
||||||
|
"i18n:govoplan-docs.steps.6041435e": "Schritte",
|
||||||
|
"i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung",
|
||||||
|
"i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technische Dokumentation für die in dieser Installation aktiven Module und Konfiguration.",
|
||||||
|
"i18n:govoplan-docs.technical_reference.f271430d": "Technische Referenz",
|
||||||
|
"i18n:govoplan-docs.this_system.b13a51ad": "Dieses System",
|
||||||
|
"i18n:govoplan-docs.toggle_tree_node.4070b59f": "Auf- oder zuklappen",
|
||||||
|
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434": "Fehlerbehebung und Support",
|
||||||
|
"i18n:govoplan-docs.unlocks.67d569db": "Schaltet frei:",
|
||||||
|
"i18n:govoplan-docs.user_docs.1e38e8d3": "Benutzerdokumentation",
|
||||||
|
"i18n:govoplan-docs.validation.ef7f6d9c": "Prüfung",
|
||||||
|
"i18n:govoplan-docs.verification.4b5f10dd": "Kontrolle",
|
||||||
|
"i18n:govoplan-docs.visible_routes.6c9b1605": "Sichtbare Routen",
|
||||||
|
"i18n:govoplan-docs.when_used.b870b361": "Wann verwendet",
|
||||||
|
"i18n:govoplan-docs.working_with_govoplan.81908ef4": "Arbeiten mit GovOPlaN",
|
||||||
|
"i18n:govoplan-docs.workflows.4f4a16d1": "Abläufe",
|
||||||
|
"i18n:govoplan-docs.your_documentation.8a4cd9a3": "Your documentation"
|
||||||
|
}
|
||||||
|
};
|
||||||
2
webui/src/index.ts
Normal file
2
webui/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { docsModule as default, docsModule } from "./module";
|
||||||
|
export { default as DocsPage } from "./features/docs/DocsPage";
|
||||||
27
webui/src/module.ts
Normal file
27
webui/src/module.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createElement, lazy } from "react";
|
||||||
|
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
|
|
||||||
|
const DocsPage = lazy(() => import("./features/docs/DocsPage"));
|
||||||
|
|
||||||
|
const docsReadScopes = ["docs:documentation:read", "system:settings:read", "admin:settings:read"];
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: generatedTranslations.en,
|
||||||
|
de: generatedTranslations.de
|
||||||
|
};
|
||||||
|
|
||||||
|
export const docsModule: PlatformWebModule = {
|
||||||
|
id: "docs",
|
||||||
|
label: "i18n:govoplan-docs.docs.68a41942",
|
||||||
|
version: "1.0.0",
|
||||||
|
dependencies: ["access"],
|
||||||
|
optionalDependencies: ["policy", "audit", "ops", "workflow", "search"],
|
||||||
|
translations,
|
||||||
|
navItems: [{ to: "/docs", label: "i18n:govoplan-docs.docs.68a41942", iconName: "reports", anyOf: docsReadScopes, order: 880 }],
|
||||||
|
routes: [
|
||||||
|
{ path: "/docs", anyOf: docsReadScopes, order: 880, render: ({ settings }) => createElement(DocsPage, { settings }) }]
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default docsModule;
|
||||||
Reference in New Issue
Block a user