fastapi-voyager 0.15.2__tar.gz → 0.15.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.github/workflows/publish.yml +9 -1
  2. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/PKG-INFO +1 -1
  3. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/docs/changelog.md +3 -1
  4. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/version.py +1 -1
  5. fastapi_voyager-0.15.3/src/fastapi_voyager/web/store.js +529 -0
  6. fastapi_voyager-0.15.3/src/fastapi_voyager/web/vue-main.js +331 -0
  7. fastapi_voyager-0.15.2/src/fastapi_voyager/web/store.js +0 -117
  8. fastapi_voyager-0.15.2/src/fastapi_voyager/web/vue-main.js +0 -625
  9. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.githooks/README.md +0 -0
  10. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.githooks/pre-commit +0 -0
  11. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.gitignore +0 -0
  14. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.prettierignore +0 -0
  15. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.prettierrc +0 -0
  16. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/.python-version +0 -0
  17. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/CONTRIBUTING.md +0 -0
  18. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/LICENSE +0 -0
  19. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/README.md +0 -0
  20. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/docs/claude/0_REFACTORING_RENDER_NOTES.md +0 -0
  21. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/docs/idea.md +0 -0
  22. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/package-lock.json +0 -0
  23. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/pyproject.toml +0 -0
  24. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/release.md +0 -0
  25. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/setup-hooks.sh +0 -0
  26. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/__init__.py +0 -0
  27. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/cli.py +0 -0
  28. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/er_diagram.py +0 -0
  29. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/filter.py +0 -0
  30. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/module.py +0 -0
  31. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/pydantic_resolve_util.py +0 -0
  32. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/render.py +0 -0
  33. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/render_style.py +0 -0
  34. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/server.py +0 -0
  35. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/cluster.j2 +0 -0
  36. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/cluster_container.j2 +0 -0
  37. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/digraph.j2 +0 -0
  38. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/er_diagram.j2 +0 -0
  39. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/link.j2 +0 -0
  40. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/route_node.j2 +0 -0
  41. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/schema_node.j2 +0 -0
  42. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/tag_node.j2 +0 -0
  43. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/colored_text.j2 +0 -0
  44. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/pydantic_meta.j2 +0 -0
  45. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/schema_field_row.j2 +0 -0
  46. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/schema_header.j2 +0 -0
  47. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/schema_table.j2 +0 -0
  48. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/type.py +0 -0
  49. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/type_helper.py +0 -0
  50. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/voyager.py +0 -0
  51. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/demo.js +0 -0
  52. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  53. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  54. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  55. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/graph-ui.js +0 -0
  56. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  57. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  58. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  59. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  60. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  61. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  62. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  63. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  64. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  65. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/index.html +0 -0
  66. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/quasar.min.css +0 -0
  67. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/quasar.min.js +0 -0
  68. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/__init__.py +0 -0
  69. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/demo.py +0 -0
  70. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/demo_anno.py +0 -0
  71. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/programatic.py +0 -0
  72. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/service/__init__.py +0 -0
  73. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/service/schema/__init__.py +0 -0
  74. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/service/schema/base_entity.py +0 -0
  75. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/service/schema/extra.py +0 -0
  76. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/service/schema/schema.py +0 -0
  77. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_analysis.py +0 -0
  78. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_filter.py +0 -0
  79. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_generic.py +0 -0
  80. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_import.py +0 -0
  81. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_module.py +0 -0
  82. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_resolve_util_impl.py +0 -0
  83. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/tests/test_type_helper.py +0 -0
  84. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/uv.lock +0 -0
  85. {fastapi_voyager-0.15.2 → fastapi_voyager-0.15.3}/voyager.jpg +0 -0
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
 
13
13
  permissions:
14
- contents: read
14
+ contents: write
15
15
 
16
16
  steps:
17
17
  - name: Checkout repository
@@ -30,3 +30,11 @@ jobs:
30
30
 
31
31
  - name: Publish to PyPI
32
32
  run: uv publish --token ${{ secrets.PYPI_PUBLISHER }}
33
+
34
+ - name: Create GitHub Release
35
+ uses: softprops/action-gh-release@v2
36
+ with:
37
+ generate_release_notes: true
38
+ files: |
39
+ dist/*.tar.gz
40
+ dist/*.whl
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.15.2
3
+ Version: 0.15.3
4
4
  Summary: Visualize FastAPI application's routing tree and dependencies
5
5
  Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
6
  Project-URL: Source, https://github.com/allmonday/fastapi-voyager
@@ -164,7 +164,9 @@
164
164
  - [x] fix resetSearch issue: fail to go back previous tag/router after reset.
165
165
  - [x] left panel can be toggled.
166
166
  - 0.15.3
167
- - [ ] refactor vue-main.js, move methods to store
167
+ - [x] refactor vue-main.js, move methods to store
168
+ - [x] optimize search flow
169
+ - 0.15.4
168
170
  - [ ] add tests
169
171
 
170
172
  ## 1.0, release
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.2"
2
+ __version__ = "0.15.3"
@@ -0,0 +1,529 @@
1
+ const { reactive } = window.Vue
2
+
3
+ const state = reactive({
4
+ version: "",
5
+ config: {
6
+ initial_page_policy: "first",
7
+ has_er_diagram: false,
8
+ enable_pydantic_resolve_meta: false,
9
+ },
10
+
11
+ mode: "voyager", // voyager / er-diagram
12
+
13
+ previousTagRoute: {
14
+ // Store the last non-search tag/route selection for restoration when clearing search
15
+ // Used by resetSearch to return to the state before entering search mode
16
+ hasValue: false,
17
+ tag: null,
18
+ routeId: null,
19
+ },
20
+
21
+ swagger: {
22
+ url: "",
23
+ },
24
+
25
+ rightDrawer: {
26
+ drawer: false,
27
+ width: 300,
28
+ },
29
+
30
+ fieldOptions: [
31
+ { label: "No field", value: "single" },
32
+ { label: "Object fields", value: "object" },
33
+ { label: "All fields", value: "all" },
34
+ ],
35
+
36
+ // tags and routes
37
+ leftPanel: {
38
+ width: 300,
39
+ previousWidth: 300,
40
+ tags: null,
41
+ fullTagsCache: null, // Cache for full tags (before search)
42
+ tag: null,
43
+ _tag: null,
44
+ routeId: null,
45
+ collapsed: false,
46
+ },
47
+
48
+ graph: {
49
+ schemaId: null,
50
+ schemaKeys: new Set(),
51
+ schemaMap: {},
52
+ routeItems: [],
53
+ },
54
+
55
+ // schema options, schema, fields
56
+ search: {
57
+ mode: false,
58
+ invisible: false,
59
+ schemaName: null,
60
+ fieldName: null,
61
+ schemaOptions: [],
62
+ fieldOptions: [],
63
+ },
64
+
65
+ // cache all schema options for filtering
66
+ allSchemaOptions: [],
67
+
68
+ // route information
69
+ routeDetail: {
70
+ show: false,
71
+ routeCodeId: "",
72
+ },
73
+
74
+ // schema information
75
+ schemaDetail: {
76
+ show: false,
77
+ schemaCodeName: "",
78
+ },
79
+
80
+ searchDialog: {
81
+ show: false,
82
+ schema: null,
83
+ },
84
+
85
+ // global status
86
+ status: {
87
+ generating: false,
88
+ loading: false,
89
+ initializing: true,
90
+ },
91
+
92
+ // brief, hide primitive ...
93
+ modeControl: {
94
+ focus: false, // control the schema param
95
+ briefModeEnabled: false, // show brief mode toggle
96
+ pydanticResolveMetaEnabled: false, // show pydantic resolve meta toggle
97
+ },
98
+
99
+ // api filters
100
+ filter: {
101
+ hidePrimitiveRoute: false,
102
+ showFields: "object",
103
+ brief: false,
104
+ showModule: false,
105
+ },
106
+ })
107
+
108
+ const getters = {
109
+ /**
110
+ * Find tag name by route ID
111
+ * Used to determine which tag a route belongs to
112
+ */
113
+ findTagByRoute(routeId) {
114
+ return (
115
+ state.leftPanel.tags.find((tag) => (tag.routes || []).some((route) => route.id === routeId))
116
+ ?.name || null
117
+ )
118
+ },
119
+ }
120
+
121
+ const actions = {
122
+ /**
123
+ * Read tag and route from URL query parameters
124
+ * @returns {{ tag: string|null, route: string|null }}
125
+ */
126
+ readQuerySelection() {
127
+ if (typeof window === "undefined") {
128
+ return { tag: null, route: null }
129
+ }
130
+ const params = new URLSearchParams(window.location.search)
131
+ return {
132
+ tag: params.get("tag") || null,
133
+ route: params.get("route") || null,
134
+ }
135
+ },
136
+
137
+ /**
138
+ * Sync current tag and route selection to URL
139
+ * Updates browser URL without reloading the page
140
+ */
141
+ syncSelectionToUrl() {
142
+ if (typeof window === "undefined") {
143
+ return
144
+ }
145
+ const params = new URLSearchParams(window.location.search)
146
+ if (state.leftPanel.tag) {
147
+ params.set("tag", state.leftPanel.tag)
148
+ } else {
149
+ params.delete("tag")
150
+ }
151
+ if (state.leftPanel.routeId) {
152
+ params.set("route", state.leftPanel.routeId)
153
+ } else {
154
+ params.delete("route")
155
+ }
156
+ const hash = window.location.hash || ""
157
+ const search = params.toString()
158
+ const base = window.location.pathname
159
+ const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
160
+ window.history.replaceState({}, "", newUrl)
161
+ },
162
+
163
+ /**
164
+ * Apply selection from URL query parameters to state
165
+ * @param {{ tag: string|null, route: string|null }} selection
166
+ * @returns {boolean} - true if any selection was applied
167
+ */
168
+ applySelectionFromQuery(selection) {
169
+ let applied = false
170
+ if (selection.tag && state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
171
+ state.leftPanel.tag = selection.tag
172
+ state.leftPanel._tag = selection.tag
173
+ applied = true
174
+ }
175
+ if (selection.route && state.graph.routeItems?.[selection.route]) {
176
+ state.leftPanel.routeId = selection.route
177
+ applied = true
178
+ const inferredTag = getters.findTagByRoute(selection.route)
179
+ if (inferredTag) {
180
+ state.leftPanel.tag = inferredTag
181
+ state.leftPanel._tag = inferredTag
182
+ }
183
+ }
184
+ return applied
185
+ },
186
+
187
+ /**
188
+ * Restore full tags from cache
189
+ * Used when resetting search mode
190
+ */
191
+ loadFullTags() {
192
+ state.leftPanel.tags = state.leftPanel.fullTagsCache
193
+ },
194
+
195
+ /**
196
+ * Populate field options based on selected schema
197
+ * @param {string} schemaId - Schema ID
198
+ */
199
+ populateFieldOptions(schemaId) {
200
+ if (!schemaId) {
201
+ state.search.fieldOptions = []
202
+ state.search.fieldName = null
203
+ return
204
+ }
205
+ const schema = state.graph.schemaMap?.[schemaId]
206
+ if (!schema) {
207
+ state.search.fieldOptions = []
208
+ state.search.fieldName = null
209
+ return
210
+ }
211
+ const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
212
+ state.search.fieldOptions = fields
213
+ if (!fields.includes(state.search.fieldName)) {
214
+ state.search.fieldName = null
215
+ }
216
+ },
217
+
218
+ /**
219
+ * Rebuild schema options from schema map
220
+ * Should be called when schema map changes
221
+ */
222
+ rebuildSchemaOptions() {
223
+ const dict = state.graph.schemaMap || {}
224
+ const opts = Object.values(dict).map((s) => ({
225
+ label: s.name,
226
+ desc: s.id,
227
+ value: s.id,
228
+ }))
229
+ state.allSchemaOptions = opts
230
+ state.search.schemaOptions = opts.slice()
231
+ this.populateFieldOptions(state.search.schemaName)
232
+ },
233
+
234
+ /**
235
+ * Load tags based on search criteria
236
+ * @returns {Promise<void>}
237
+ */
238
+ async loadSearchedTags() {
239
+ try {
240
+ const payload = {
241
+ schema_name: state.search.schemaName,
242
+ schema_field: state.search.fieldName || null,
243
+ show_fields: state.filter.showFields,
244
+ brief: state.filter.brief,
245
+ hide_primitive_route: state.filter.hidePrimitiveRoute,
246
+ show_module: state.filter.showModule,
247
+ }
248
+ const res = await fetch("dot-search", {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify(payload),
252
+ })
253
+ if (res.ok) {
254
+ const data = await res.json()
255
+ const tags = Array.isArray(data.tags) ? data.tags : []
256
+ state.leftPanel.tags = tags
257
+ }
258
+ } catch (err) {
259
+ console.error("dot-search failed", err)
260
+ }
261
+ },
262
+
263
+ /**
264
+ * Load initial data from API
265
+ * @param {Function} onGenerate - Callback to generate graph after load
266
+ * @param {Function} renderBasedOnInitialPolicy - Callback to render based on policy
267
+ * @returns {Promise<void>}
268
+ */
269
+ async loadInitial(onGenerate, renderBasedOnInitialPolicy) {
270
+ state.initializing = true
271
+ try {
272
+ const res = await fetch("dot")
273
+ const data = await res.json()
274
+ const tags = Array.isArray(data.tags) ? data.tags : []
275
+ state.leftPanel.tags = tags
276
+ // Cache the full tags for later use (e.g., resetSearch)
277
+ state.leftPanel.fullTagsCache = tags
278
+
279
+ const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
280
+ // Build dict keyed by id for faster lookups and simpler prop passing
281
+ const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
282
+ state.graph.schemaMap = schemaMap
283
+ state.graph.schemaKeys = new Set(Object.keys(schemaMap))
284
+ state.graph.routeItems = data.tags
285
+ .map((t) => t.routes)
286
+ .flat()
287
+ .reduce((acc, r) => {
288
+ acc[r.id] = r
289
+ return acc
290
+ }, {})
291
+ state.modeControl.briefModeEnabled = data.enable_brief_mode || false
292
+ state.version = data.version || ""
293
+ state.swagger.url = data.swagger_url || null
294
+ state.config.has_er_diagram = data.has_er_diagram || false
295
+ state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
296
+
297
+ this.rebuildSchemaOptions()
298
+
299
+ const querySelection = this.readQuerySelection()
300
+ const restoredFromQuery = this.applySelectionFromQuery(querySelection)
301
+ if (restoredFromQuery) {
302
+ this.syncSelectionToUrl()
303
+ onGenerate()
304
+ return
305
+ } else {
306
+ state.config.initial_page_policy = data.initial_page_policy
307
+ renderBasedOnInitialPolicy()
308
+ }
309
+
310
+ // default route options placeholder
311
+ } catch (e) {
312
+ console.error("Initial load failed", e)
313
+ } finally {
314
+ state.initializing = false
315
+ }
316
+ },
317
+
318
+ /**
319
+ * Filter schema options based on search text
320
+ * Used by Quasar select component's filter function
321
+ * @param {string} val - Search text
322
+ * @param {Function} update - Quasar update callback
323
+ */
324
+ filterSearchSchemas(val, update) {
325
+ const needle = (val || "").toLowerCase()
326
+ update(() => {
327
+ if (!needle) {
328
+ state.search.schemaOptions = state.allSchemaOptions.slice()
329
+ return
330
+ }
331
+ state.search.schemaOptions = state.allSchemaOptions.filter((option) =>
332
+ option.label.toLowerCase().includes(needle)
333
+ )
334
+ })
335
+ },
336
+
337
+ /**
338
+ * Handle schema selection change
339
+ * Updates state and triggers search if a schema is selected
340
+ * @param {string} val - Selected schema ID
341
+ * @param {Function} onSearch - Callback to trigger search
342
+ */
343
+ onSearchSchemaChange(val, onSearch) {
344
+ state.search.schemaName = val
345
+ state.search.mode = false
346
+ if (!val) {
347
+ // Clearing the select should only run resetSearch via @clear
348
+ return
349
+ }
350
+ onSearch()
351
+ },
352
+
353
+ /**
354
+ * Reset detail panels (right drawer and route detail)
355
+ */
356
+ resetDetailPanels() {
357
+ state.rightDrawer.drawer = false
358
+ state.routeDetail.show = false
359
+ state.schemaDetail.schemaCodeName = ""
360
+ },
361
+
362
+ /**
363
+ * Reset left panel selection and regenerate
364
+ * @param {Function} onGenerate - Callback to regenerate graph
365
+ */
366
+ onReset(onGenerate) {
367
+ state.leftPanel.tag = null
368
+ state.leftPanel._tag = null
369
+ state.leftPanel.routeId = null
370
+ this.syncSelectionToUrl()
371
+ onGenerate()
372
+ },
373
+
374
+ /**
375
+ * Toggle pydantic resolve meta visibility
376
+ * @param {boolean} val - New value
377
+ * @param {Function} onGenerate - Callback to regenerate graph
378
+ */
379
+ togglePydanticResolveMeta(val, onGenerate) {
380
+ state.modeControl.pydanticResolveMetaEnabled = val
381
+ try {
382
+ localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
383
+ } catch (e) {
384
+ console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
385
+ }
386
+ onGenerate()
387
+ },
388
+
389
+ /**
390
+ * Toggle show module clustering
391
+ * @param {boolean} val - New value
392
+ * @param {Function} onGenerate - Callback to regenerate graph
393
+ */
394
+ toggleShowModule(val, onGenerate) {
395
+ state.filter.showModule = val
396
+ try {
397
+ localStorage.setItem("show_module_cluster", JSON.stringify(val))
398
+ } catch (e) {
399
+ console.warn("Failed to save show_module_cluster to localStorage", e)
400
+ }
401
+ onGenerate()
402
+ },
403
+
404
+ /**
405
+ * Toggle show fields option
406
+ * @param {string} field - Field display option ("single", "object", "all")
407
+ * @param {Function} onGenerate - Callback to regenerate graph
408
+ */
409
+ toggleShowField(field, onGenerate) {
410
+ state.filter.showFields = field
411
+ onGenerate(false)
412
+ },
413
+
414
+ /**
415
+ * Toggle brief mode
416
+ * @param {boolean} val - New value
417
+ * @param {Function} onGenerate - Callback to regenerate graph
418
+ */
419
+ toggleBrief(val, onGenerate) {
420
+ state.filter.brief = val
421
+ try {
422
+ localStorage.setItem("brief_mode", JSON.stringify(val))
423
+ } catch (e) {
424
+ console.warn("Failed to save brief_mode to localStorage", e)
425
+ }
426
+ onGenerate()
427
+ },
428
+
429
+ /**
430
+ * Toggle hide primitive route
431
+ * @param {boolean} val - New value
432
+ * @param {Function} onGenerate - Callback to regenerate graph
433
+ */
434
+ toggleHidePrimitiveRoute(val, onGenerate) {
435
+ state.filter.hidePrimitiveRoute = val
436
+ try {
437
+ localStorage.setItem("hide_primitive", JSON.stringify(val))
438
+ } catch (e) {
439
+ console.warn("Failed to save hide_primitive to localStorage", e)
440
+ }
441
+ onGenerate(false)
442
+ },
443
+
444
+ /**
445
+ * Render based on initial page policy
446
+ * @param {Function} onGenerate - Callback to regenerate graph
447
+ */
448
+ renderBasedOnInitialPolicy(onGenerate) {
449
+ switch (state.config.initial_page_policy) {
450
+ case "full":
451
+ onGenerate()
452
+ return
453
+ case "empty":
454
+ return
455
+ case "first":
456
+ state.leftPanel.tag = state.leftPanel.tags.length > 0 ? state.leftPanel.tags[0].name : null
457
+ state.leftPanel._tag = state.leftPanel.tag
458
+ this.syncSelectionToUrl()
459
+ onGenerate()
460
+ return
461
+ }
462
+ },
463
+
464
+ /**
465
+ * Build payload for Voyager rendering
466
+ * @returns {Object} Payload for dot API
467
+ */
468
+ buildVoyagerPayload() {
469
+ const activeSchema = state.search.mode ? state.search.schemaName : null
470
+ const activeField = state.search.mode ? state.search.fieldName : null
471
+ return {
472
+ tags: state.leftPanel.tag ? [state.leftPanel.tag] : null,
473
+ schema_name: activeSchema || null,
474
+ schema_field: activeField || null,
475
+ route_name: state.leftPanel.routeId || null,
476
+ show_fields: state.filter.showFields,
477
+ brief: state.filter.brief,
478
+ hide_primitive_route: state.filter.hidePrimitiveRoute,
479
+ show_module: state.filter.showModule,
480
+ show_pydantic_resolve_meta: state.modeControl.pydanticResolveMetaEnabled,
481
+ }
482
+ },
483
+
484
+ /**
485
+ * Build payload for ER Diagram rendering
486
+ * @returns {Object} Payload for er-diagram API
487
+ */
488
+ buildErDiagramPayload() {
489
+ return {
490
+ show_fields: state.filter.showFields,
491
+ show_module: state.filter.showModule,
492
+ }
493
+ },
494
+
495
+ /**
496
+ * Restore search state and return whether to regenerate
497
+ * @returns {boolean} - true if should regenerate with previous selection
498
+ */
499
+ resetSearchState() {
500
+ state.search.mode = false
501
+ const hadPreviousValue = state.previousTagRoute.hasValue
502
+
503
+ if (hadPreviousValue) {
504
+ state.leftPanel.tag = state.previousTagRoute.tag
505
+ state.leftPanel._tag = state.previousTagRoute.tag
506
+ state.leftPanel.routeId = state.previousTagRoute.routeId
507
+ // Clear the saved state
508
+ state.previousTagRoute.hasValue = false
509
+ } else {
510
+ state.leftPanel.tag = null
511
+ state.leftPanel._tag = null
512
+ state.leftPanel.routeId = null
513
+ }
514
+
515
+ this.syncSelectionToUrl()
516
+ this.loadFullTags()
517
+
518
+ return hadPreviousValue
519
+ },
520
+ }
521
+
522
+ const mutations = {}
523
+
524
+ export const store = {
525
+ state,
526
+ getters,
527
+ actions,
528
+ mutations,
529
+ }