fastapi-voyager 0.15.1__py3-none-any.whl → 0.15.3__py3-none-any.whl

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.
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.1"
2
+ __version__ = "0.15.3"
@@ -101,6 +101,30 @@
101
101
  }
102
102
  }
103
103
 
104
+ /* Tag Navigator Collapse Button */
105
+ .tag-navigator-collapse-btn-right {
106
+ position: absolute;
107
+ bottom: 8px;
108
+ left: 8px;
109
+ width: 32px;
110
+ height: 32px;
111
+ border-radius: 50%;
112
+ background: #009485;
113
+ color: white;
114
+ border: 2px solid white;
115
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
116
+ cursor: pointer;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ z-index: 100;
121
+ transition: all 0.2s ease;
122
+ }
123
+ .tag-navigator-collapse-btn-right:hover {
124
+ background: #007a6d;
125
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
126
+ }
127
+
104
128
  /* App boot loading overlay & gating */
105
129
  #app-loading-overlay {
106
130
  position: fixed;
@@ -352,7 +376,7 @@
352
376
  <q-splitter
353
377
  v-model="store.state.leftPanel.width"
354
378
  unit="px"
355
- :limits="store.state.mode === 'voyager' ? [200, 800] : [0, 0]"
379
+ :limits="store.state.mode === 'voyager' ? [0, 800] : [0, 0]"
356
380
  class="adjust-fit"
357
381
  >
358
382
  <template #before>
@@ -509,6 +533,19 @@
509
533
  />
510
534
  </div>
511
535
  </div>
536
+
537
+ <!-- Collapse toggle button for tag navigator - at bottom-left of right panel -->
538
+ <div
539
+ v-show="store.state.mode === 'voyager'"
540
+ class="tag-navigator-collapse-btn-right"
541
+ @click="toggleTagNavigatorCollapse"
542
+ :title="store.state.leftPanel.collapsed ? 'Expand tag navigator' : 'Collapse tag navigator'"
543
+ >
544
+ <q-icon
545
+ :name="store.state.leftPanel.collapsed ? 'chevron_right' : 'chevron_left'"
546
+ size="18px"
547
+ />
548
+ </div>
512
549
  </div>
513
550
  </template>
514
551
  </q-splitter>
@@ -1,10 +1,6 @@
1
1
  const { reactive } = window.Vue
2
2
 
3
3
  const state = reactive({
4
- item: {
5
- count: 0,
6
- },
7
-
8
4
  version: "",
9
5
  config: {
10
6
  initial_page_policy: "first",
@@ -15,7 +11,8 @@ const state = reactive({
15
11
  mode: "voyager", // voyager / er-diagram
16
12
 
17
13
  previousTagRoute: {
18
- // for shift + click, store previous tag/route, and populate back when needed
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
19
16
  hasValue: false,
20
17
  tag: null,
21
18
  routeId: null,
@@ -41,9 +38,11 @@ const state = reactive({
41
38
  width: 300,
42
39
  previousWidth: 300,
43
40
  tags: null,
41
+ fullTagsCache: null, // Cache for full tags (before search)
44
42
  tag: null,
45
43
  _tag: null,
46
44
  routeId: null,
45
+ collapsed: false,
47
46
  },
48
47
 
49
48
  graph: {
@@ -63,6 +62,9 @@ const state = reactive({
63
62
  fieldOptions: [],
64
63
  },
65
64
 
65
+ // cache all schema options for filtering
66
+ allSchemaOptions: [],
67
+
66
68
  // route information
67
69
  routeDetail: {
68
70
  show: false,
@@ -103,13 +105,425 @@ const state = reactive({
103
105
  },
104
106
  })
105
107
 
106
- const mutations = {
107
- increment() {
108
- state.item.count += 1
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
109
519
  },
110
520
  }
111
521
 
522
+ const mutations = {}
523
+
112
524
  export const store = {
113
525
  state,
526
+ getters,
527
+ actions,
114
528
  mutations,
115
529
  }
@@ -22,7 +22,6 @@ function loadToggleState(key, defaultValue = false) {
22
22
  const app = createApp({
23
23
  setup() {
24
24
  let graphUI = null
25
- const allSchemaOptions = ref([])
26
25
  const erDiagramLoading = ref(false)
27
26
  const erDiagramCache = ref("")
28
27
 
@@ -42,16 +41,13 @@ const app = createApp({
42
41
  graphUI = new GraphUI("#graph", {
43
42
  onSchemaShiftClick: (id) => {
44
43
  if (store.state.graph.schemaKeys.has(id)) {
45
- store.state.previousTagRoute.tag = store.state.leftPanel.tag
46
- store.state.previousTagRoute.routeId = store.state.leftPanel.routeId
47
- store.state.previousTagRoute.hasValue = true
48
44
  store.state.search.mode = true
49
45
  store.state.search.schemaName = id
50
46
  onSearch()
51
47
  }
52
48
  },
53
49
  onSchemaClick: (id) => {
54
- resetDetailPanels()
50
+ store.actions.resetDetailPanels()
55
51
  if (store.state.graph.schemaKeys.has(id)) {
56
52
  store.state.schemaDetail.schemaCodeName = id
57
53
  store.state.rightDrawer.drawer = true
@@ -62,241 +58,44 @@ const app = createApp({
62
58
  }
63
59
  },
64
60
  resetCb: () => {
65
- resetDetailPanels()
61
+ store.actions.resetDetailPanels()
66
62
  },
67
63
  })
68
64
  }
69
65
 
70
- function rebuildSchemaOptions() {
71
- const dict = store.state.graph.schemaMap || {}
72
- const opts = Object.values(dict).map((s) => ({
73
- label: s.name,
74
- desc: s.id,
75
- value: s.id,
76
- }))
77
- allSchemaOptions.value = opts
78
- store.state.search.schemaOptions = opts.slice()
79
- populateFieldOptions(store.state.search.schemaName)
80
- }
81
-
82
- function populateFieldOptions(schemaId) {
83
- if (!schemaId) {
84
- store.state.search.fieldOptions = []
85
- store.state.search.fieldName = null
86
- return
87
- }
88
- const schema = store.state.graph.schemaMap?.[schemaId]
89
- if (!schema) {
90
- store.state.search.fieldOptions = []
91
- store.state.search.fieldName = null
92
- return
93
- }
94
- const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
95
- store.state.search.fieldOptions = fields
96
- if (!fields.includes(store.state.search.fieldName)) {
97
- store.state.search.fieldName = null
98
- }
99
- }
100
-
101
- function filterSearchSchemas(val, update) {
102
- const needle = (val || "").toLowerCase()
103
- update(() => {
104
- if (!needle) {
105
- store.state.search.schemaOptions = allSchemaOptions.value.slice()
106
- return
107
- }
108
- store.state.search.schemaOptions = allSchemaOptions.value.filter((option) =>
109
- option.label.toLowerCase().includes(needle)
110
- )
111
- })
112
- }
113
-
114
- function onSearchSchemaChange(val) {
115
- store.state.search.schemaName = val
116
- store.state.search.mode = false
117
- if (!val) {
118
- // Clearing the select should only run resetSearch via @clear
119
- return
120
- }
121
- onSearch()
122
- }
123
-
124
- function readQuerySelection() {
125
- if (typeof window === "undefined") {
126
- return { tag: null, route: null }
127
- }
128
- const params = new URLSearchParams(window.location.search)
129
- return {
130
- tag: params.get("tag") || null,
131
- route: params.get("route") || null,
132
- }
133
- }
134
-
135
- function findTagByRoute(routeId) {
136
- return (
137
- store.state.leftPanel.tags.find((tag) =>
138
- (tag.routes || []).some((route) => route.id === routeId)
139
- )?.name || null
140
- )
141
- }
66
+ async function resetSearch() {
67
+ const hadPreviousValue = store.actions.resetSearchState()
142
68
 
143
- function syncSelectionToUrl() {
144
- if (typeof window === "undefined") {
145
- return
146
- }
147
- const params = new URLSearchParams(window.location.search)
148
- if (store.state.leftPanel.tag) {
149
- params.set("tag", store.state.leftPanel.tag)
150
- } else {
151
- params.delete("tag")
152
- }
153
- if (store.state.leftPanel.routeId) {
154
- params.set("route", store.state.leftPanel.routeId)
69
+ // If we restored a previous tag/route, generate with it
70
+ // Otherwise, fall back to initial policy
71
+ if (hadPreviousValue) {
72
+ onGenerate()
155
73
  } else {
156
- params.delete("route")
157
- }
158
- const hash = window.location.hash || ""
159
- const search = params.toString()
160
- const base = window.location.pathname
161
- const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
162
- window.history.replaceState({}, "", newUrl)
163
- }
164
-
165
- function applySelectionFromQuery(selection) {
166
- let applied = false
167
- if (selection.tag && store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
168
- store.state.leftPanel.tag = selection.tag
169
- store.state.leftPanel._tag = selection.tag
170
- applied = true
74
+ store.actions.renderBasedOnInitialPolicy(onGenerate)
171
75
  }
172
- if (selection.route && store.state.graph.routeItems?.[selection.route]) {
173
- store.state.leftPanel.routeId = selection.route
174
- applied = true
175
- const inferredTag = findTagByRoute(selection.route)
176
- if (inferredTag) {
177
- store.state.leftPanel.tag = inferredTag
178
- store.state.leftPanel._tag = inferredTag
179
- }
180
- }
181
- return applied
182
76
  }
183
77
 
184
- async function resetSearch() {
185
- store.state.search.mode = false
186
- console.log(store.state.previousTagRoute.hasValue)
187
- console.log(store.state.previousTagRoute)
188
- if (store.state.previousTagRoute.hasValue) {
189
- store.state.leftPanel.tag = store.state.previousTagRoute.tag
190
- store.state.leftPanel._tag = store.state.previousTagRoute.tag
191
- store.state.leftPanel.routeId = store.state.previousTagRoute.routeId
192
- store.state.previousTagRoute.hasValue = false
193
- } else {
194
- store.state.leftPanel.tag = null
195
- store.state.leftPanel._tag = null
196
- store.state.leftPanel.routeId = null
78
+ async function onSearch() {
79
+ // Save current state before entering search mode (only if not already saved)
80
+ if (!store.state.previousTagRoute.hasValue) {
81
+ store.state.previousTagRoute.tag = store.state.leftPanel.tag
82
+ store.state.previousTagRoute.routeId = store.state.leftPanel.routeId
83
+ store.state.previousTagRoute.hasValue = true
197
84
  }
198
85
 
199
- syncSelectionToUrl()
200
- await loadSearchedTags()
201
- renderBasedOnInitialPolicy()
202
- }
203
-
204
- async function onSearch() {
205
- console.log("start search")
206
86
  store.state.search.mode = true
207
87
  store.state.leftPanel.tag = null
208
88
  store.state.leftPanel._tag = null
209
89
  store.state.leftPanel.routeId = null
210
- syncSelectionToUrl()
211
- await loadSearchedTags()
90
+ store.actions.syncSelectionToUrl()
91
+ await store.actions.loadSearchedTags()
212
92
  await onGenerate()
213
93
  }
214
- async function loadSearchedTags() {
215
- try {
216
- const payload = {
217
- schema_name: store.state.search.schemaName,
218
- schema_field: store.state.search.fieldName || null,
219
- show_fields: store.state.filter.showFields,
220
- brief: store.state.filter.brief,
221
- hide_primitive_route: store.state.filter.hidePrimitiveRoute,
222
- show_module: store.state.filter.showModule,
223
- }
224
- const res = await fetch("dot-search", {
225
- method: "POST",
226
- headers: { "Content-Type": "application/json" },
227
- body: JSON.stringify(payload),
228
- })
229
- if (res.ok) {
230
- const data = await res.json()
231
- const tags = Array.isArray(data.tags) ? data.tags : []
232
- store.state.leftPanel.tags = tags
233
- }
234
- } catch (err) {
235
- console.error("dot-search failed", err)
236
- }
237
- }
238
94
 
239
95
  async function loadInitial() {
240
- store.state.initializing = true
241
- try {
242
- const res = await fetch("dot")
243
- const data = await res.json()
244
- store.state.leftPanel.tags = Array.isArray(data.tags) ? data.tags : []
245
-
246
- const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
247
- // Build dict keyed by id for faster lookups and simpler prop passing
248
- const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
249
- store.state.graph.schemaMap = schemaMap
250
- store.state.graph.schemaKeys = new Set(Object.keys(schemaMap))
251
- store.state.graph.routeItems = data.tags
252
- .map((t) => t.routes)
253
- .flat()
254
- .reduce((acc, r) => {
255
- acc[r.id] = r
256
- return acc
257
- }, {})
258
- store.state.modeControl.briefModeEnabled = data.enable_brief_mode || false
259
- store.state.version = data.version || ""
260
- store.state.swagger.url = data.swagger_url || null
261
- store.state.config.has_er_diagram = data.has_er_diagram || false
262
- store.state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
263
-
264
- rebuildSchemaOptions()
265
-
266
- const querySelection = readQuerySelection()
267
- const restoredFromQuery = applySelectionFromQuery(querySelection)
268
- if (restoredFromQuery) {
269
- syncSelectionToUrl()
270
- onGenerate()
271
- return
272
- } else {
273
- store.state.config.initial_page_policy = data.initial_page_policy
274
- renderBasedOnInitialPolicy()
275
- }
276
-
277
- // default route options placeholder
278
- } catch (e) {
279
- console.error("Initial load failed", e)
280
- } finally {
281
- store.state.initializing = false
282
- }
283
- }
284
-
285
- async function renderBasedOnInitialPolicy() {
286
- switch (store.state.config.initial_page_policy) {
287
- case "full":
288
- onGenerate()
289
- return
290
- case "empty":
291
- return
292
- case "first":
293
- store.state.leftPanel.tag =
294
- store.state.leftPanel.tags.length > 0 ? store.state.leftPanel.tags[0].name : null
295
- store.state.leftPanel._tag = store.state.leftPanel.tag
296
- syncSelectionToUrl()
297
- onGenerate()
298
- return
299
- }
96
+ await store.actions.loadInitial(onGenerate, (cb) =>
97
+ store.actions.renderBasedOnInitialPolicy(cb)
98
+ )
300
99
  }
301
100
 
302
101
  async function onGenerate(resetZoom = true) {
@@ -311,21 +110,9 @@ const app = createApp({
311
110
  }
312
111
 
313
112
  async function renderVoyager(resetZoom = true) {
314
- const activeSchema = store.state.search.mode ? store.state.search.schemaName : null
315
- const activeField = store.state.search.mode ? store.state.search.fieldName : null
316
113
  store.state.generating = true
317
114
  try {
318
- const payload = {
319
- tags: store.state.leftPanel.tag ? [store.state.leftPanel.tag] : null,
320
- schema_name: activeSchema || null,
321
- schema_field: activeField || null,
322
- route_name: store.state.leftPanel.routeId || null,
323
- show_fields: store.state.filter.showFields,
324
- brief: store.state.filter.brief,
325
- hide_primitive_route: store.state.filter.hidePrimitiveRoute,
326
- show_module: store.state.filter.showModule,
327
- show_pydantic_resolve_meta: store.state.modeControl.pydanticResolveMetaEnabled,
328
- }
115
+ const payload = store.actions.buildVoyagerPayload()
329
116
  initGraphUI()
330
117
  const res = await fetch("dot", {
331
118
  method: "POST",
@@ -342,37 +129,10 @@ const app = createApp({
342
129
  }
343
130
  }
344
131
 
345
- function resetDetailPanels() {
346
- store.state.rightDrawer.drawer = false
347
- store.state.routeDetail.show = false
348
- store.state.schemaDetail.schemaCodeName = ""
349
- }
350
-
351
- async function onReset() {
352
- store.state.leftPanel.tag = null
353
- store.state.leftPanel._tag = null
354
- store.state.leftPanel.routeId = null
355
- syncSelectionToUrl()
356
- onGenerate()
357
- }
358
-
359
- async function togglePydanticResolveMeta(val) {
360
- store.state.modeControl.pydanticResolveMetaEnabled = val
361
- try {
362
- localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
363
- } catch (e) {
364
- console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
365
- }
366
- onGenerate()
367
- }
368
-
369
132
  async function renderErDiagram(resetZoom = true) {
370
133
  initGraphUI()
371
134
  erDiagramLoading.value = true
372
- const payload = {
373
- show_fields: store.state.filter.showFields,
374
- show_module: store.state.filter.showModule,
375
- }
135
+ const payload = store.actions.buildErDiagramPayload()
376
136
  try {
377
137
  const res = await fetch("er-diagram", {
378
138
  method: "POST",
@@ -427,12 +187,28 @@ const app = createApp({
427
187
 
428
188
  store.state.rightDrawer.drawer = false
429
189
  store.state.routeDetail.show = false
430
- syncSelectionToUrl()
190
+ store.actions.syncSelectionToUrl()
191
+ }
192
+
193
+ function toggleTagNavigatorCollapse() {
194
+ if (store.state.leftPanel.collapsed) {
195
+ // Expand: restore previous width
196
+ const fallbackWidth = store.state.leftPanel.previousWidth || 300
197
+ store.state.leftPanel.width = fallbackWidth
198
+ store.state.leftPanel.collapsed = false
199
+ } else {
200
+ // Collapse: save current width and set to 0
201
+ if (store.state.leftPanel.width > 0) {
202
+ store.state.leftPanel.previousWidth = store.state.leftPanel.width
203
+ }
204
+ store.state.leftPanel.width = 0
205
+ store.state.leftPanel.collapsed = true
206
+ }
431
207
  }
432
208
 
433
209
  function selectRoute(routeId) {
434
210
  // find belonging tag
435
- const belongingTag = findTagByRoute(routeId)
211
+ const belongingTag = store.getters.findTagByRoute(routeId)
436
212
  if (belongingTag) {
437
213
  store.state.leftPanel.tag = belongingTag
438
214
  store.state.leftPanel._tag = belongingTag
@@ -447,45 +223,10 @@ const app = createApp({
447
223
  store.state.rightDrawer.drawer = false
448
224
  store.state.routeDetail.show = false
449
225
  store.state.schemaDetail.schemaCodeName = ""
450
- syncSelectionToUrl()
451
- onGenerate()
452
- }
453
-
454
- function toggleShowModule(val) {
455
- store.state.filter.showModule = val
456
- try {
457
- localStorage.setItem("show_module_cluster", JSON.stringify(val))
458
- } catch (e) {
459
- console.warn("Failed to save show_module_cluster to localStorage", e)
460
- }
461
- onGenerate()
462
- }
463
-
464
- function toggleShowField(field) {
465
- store.state.filter.showFields = field
466
- onGenerate(false)
467
- }
468
-
469
- function toggleBrief(val) {
470
- store.state.filter.brief = val
471
- try {
472
- localStorage.setItem("brief_mode", JSON.stringify(val))
473
- } catch (e) {
474
- console.warn("Failed to save brief_mode to localStorage", e)
475
- }
226
+ store.actions.syncSelectionToUrl()
476
227
  onGenerate()
477
228
  }
478
229
 
479
- function toggleHidePrimitiveRoute(val) {
480
- store.state.filter.hidePrimitiveRoute = val
481
- try {
482
- localStorage.setItem("hide_primitive", JSON.stringify(val))
483
- } catch (e) {
484
- console.warn("Failed to save hide_primitive to localStorage", e)
485
- }
486
- onGenerate(false)
487
- }
488
-
489
230
  function startDragDrawer(e) {
490
231
  const startX = e.clientX
491
232
  const startWidth = store.state.rightDrawer.width
@@ -513,7 +254,7 @@ const app = createApp({
513
254
  watch(
514
255
  () => store.state.graph.schemaMap,
515
256
  () => {
516
- rebuildSchemaOptions()
257
+ store.actions.rebuildSchemaOptions()
517
258
  },
518
259
  { deep: false }
519
260
  )
@@ -537,8 +278,8 @@ const app = createApp({
537
278
  watch(
538
279
  () => store.state.search.schemaName,
539
280
  (schemaId) => {
540
- store.state.search.schemaOptions = allSchemaOptions.value.slice()
541
- populateFieldOptions(schemaId)
281
+ store.state.search.schemaOptions = store.state.allSchemaOptions.slice()
282
+ store.actions.populateFieldOptions(schemaId)
542
283
  if (!schemaId) {
543
284
  store.state.search.mode = false
544
285
  }
@@ -555,20 +296,22 @@ const app = createApp({
555
296
  store,
556
297
  onSearch,
557
298
  resetSearch,
558
- filterSearchSchemas,
559
- onSearchSchemaChange,
299
+ filterSearchSchemas: (val, update) => store.actions.filterSearchSchemas(val, update),
300
+ onSearchSchemaChange: (val) => store.actions.onSearchSchemaChange(val, onSearch),
560
301
  toggleTag,
561
- toggleBrief,
562
- toggleHidePrimitiveRoute,
302
+ toggleTagNavigatorCollapse,
303
+ toggleBrief: (val) => store.actions.toggleBrief(val, onGenerate),
304
+ toggleHidePrimitiveRoute: (val) => store.actions.toggleHidePrimitiveRoute(val, onGenerate),
563
305
  selectRoute,
564
306
  onGenerate,
565
- onReset,
566
- toggleShowField,
307
+ onReset: () => store.actions.onReset(onGenerate),
308
+ toggleShowField: (field) => store.actions.toggleShowField(field, onGenerate),
567
309
  startDragDrawer,
568
- toggleShowModule,
310
+ toggleShowModule: (val) => store.actions.toggleShowModule(val, onGenerate),
569
311
  onModeChange,
570
312
  renderErDiagram,
571
- togglePydanticResolveMeta,
313
+ togglePydanticResolveMeta: (val) => store.actions.togglePydanticResolveMeta(val, onGenerate),
314
+ resetDetailPanels: () => store.actions.resetDetailPanels(),
572
315
  }
573
316
  },
574
317
  })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.15.1
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
@@ -9,7 +9,7 @@ fastapi_voyager/render_style.py,sha256=mPOuChEl71-3agCbPwkMt2sFmax2AEKDI6dK90eFP
9
9
  fastapi_voyager/server.py,sha256=MUc_ia3_QIbYQ8VenOxv3CEYVTiPDs7e_H40YoalxZs,8970
10
10
  fastapi_voyager/type.py,sha256=zluWvh5vpnjXJ9aAmyNJTSmXZPjAHCvgRT5oQRAjHrg,2104
11
11
  fastapi_voyager/type_helper.py,sha256=FmfrZAI3Z4uDdh3sH_kH7UGoY6yNVPapneSN86qY_wo,10209
12
- fastapi_voyager/version.py,sha256=BSYny8QjMZlFFaTEx2paWbmn9s5pXE5CYgdpnEf49W8,49
12
+ fastapi_voyager/version.py,sha256=mV_h7Kelwuchi4ZvsHaY1gjoGeeu7EK0TqrMYIGLwgI,49
13
13
  fastapi_voyager/voyager.py,sha256=4vonmL-xt54C5San-DRBq4mjoV8Q96eoWRy68MJ1IJw,14169
14
14
  fastapi_voyager/templates/dot/cluster.j2,sha256=I2z9KkfCzmAtqXe0gXBnxnOfBXUSpdlATs3uf-O8_B8,307
15
15
  fastapi_voyager/templates/dot/cluster_container.j2,sha256=2tH1mOJvPoVKE_aHVMR3t06TfH_dYa9OeH6DBqSHt_A,204
@@ -27,11 +27,11 @@ fastapi_voyager/templates/html/schema_table.j2,sha256=rzphiGk1il7uv4Gr2p_HLPHqyL
27
27
  fastapi_voyager/web/graph-ui.js,sha256=cmEcM35rQsR6eQA2Sc8zJ_MOA0UfEQ9WvGA1zNInUUc,6285
28
28
  fastapi_voyager/web/graphviz.svg.css,sha256=K218ov_mdSe3ga4KwhiBB92ynVvm5zaAk9_D9a3d8hE,1546
29
29
  fastapi_voyager/web/graphviz.svg.js,sha256=deI815RgxpZ3_MpELeV-TBYy2MVuUvZtQOHfS3aeXHY,18203
30
- fastapi_voyager/web/index.html,sha256=NQCbf3FDJVOOoep-nKh-RVsSlAK9E_eCYQ3LfGgJe-c,21987
30
+ fastapi_voyager/web/index.html,sha256=lA59Op4u1bb-B105Iadn2KN_n11-AtXFdJglUezZIDg,23225
31
31
  fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
32
32
  fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
33
- fastapi_voyager/web/store.js,sha256=wLI9k7q-NuprpGd_We_laTJ35te5XX5dRIIrPn257Rg,1934
34
- fastapi_voyager/web/vue-main.js,sha256=lWe_MUwYGE4q3s4eWhjLZlcYZTjxklp_1wGvW-eedj0,18657
33
+ fastapi_voyager/web/store.js,sha256=zjmtx1HGN_umfyOQVwQgET-2V5hvXbS50YgXCr294Ok,14370
34
+ fastapi_voyager/web/vue-main.js,sha256=1s11NOjILgjncJ4WbN1DGzTeLUb0a2-LYIA3lUIX8Bc,10564
35
35
  fastapi_voyager/web/component/demo.js,sha256=sAklFGhKGmMy9-ofgOw2oPIidAoIOgHu6yvV51L_MAA,350
36
36
  fastapi_voyager/web/component/render-graph.js,sha256=9wnO70n3eyPKTpa744idgs5PSwgvzbfv4InZ68eEOKs,2454
37
37
  fastapi_voyager/web/component/route-code-display.js,sha256=a823nBz3EEjutW2pfi73rcF3hodCBmgYNmuZi94sXE4,3615
@@ -43,8 +43,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
43
43
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
44
44
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
45
45
  fastapi_voyager/web/icon/site.webmanifest,sha256=GRozZ5suTykYcPMap1QhjrAB8PLW0mbT_phhzw_utvQ,316
46
- fastapi_voyager-0.15.1.dist-info/METADATA,sha256=DsiVefq6CHjYmQmRox7zjSKIehv3H8e7DMU_KZgXdGA,8513
47
- fastapi_voyager-0.15.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- fastapi_voyager-0.15.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
- fastapi_voyager-0.15.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
- fastapi_voyager-0.15.1.dist-info/RECORD,,
46
+ fastapi_voyager-0.15.3.dist-info/METADATA,sha256=_NFcPd07mWHADnTH01kf6c2y0Lw9AQgqQNnHMK0Y5xE,8513
47
+ fastapi_voyager-0.15.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ fastapi_voyager-0.15.3.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
+ fastapi_voyager-0.15.3.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
+ fastapi_voyager-0.15.3.dist-info/RECORD,,