@antelopejs/dms-database 0.0.13 → 0.1.0

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 (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +9 -6
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/types/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,WAAW,GAAG,cAAc,CAAC;AAE7B,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAE3B,QAAA,uBAAuB,GAAG,oBAAoB,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/types/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,WAAW,GAAG,cAAc,CAAC;AAE7B,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAE3B,QAAA,uBAAuB,GAAG,oBAAoB,CAAC;AAC/C,QAAA,mBAAmB,GAAG,CAAC,CAAC;AAGxB,QAAA,aAAa,GAAG,GAAG,CAAC"}
@@ -10,8 +10,6 @@ export declare function parseFilter(raw: unknown): ParsedFilter | undefined;
10
10
  export declare function unwrapIsFilter(raw: unknown): string | undefined;
11
11
  export declare function decodeInstanceFilter(raw: unknown): InstanceId | undefined;
12
12
  export declare function applyFilterToList<T>(rows: T[], field: keyof T, rawFilter: unknown): T[];
13
- export declare function applySearchToList<T>(rows: T[], field: keyof T, search: unknown): T[];
14
13
  export declare function cellToText(value: unknown): string;
15
- export declare function applySortToList<T>(rows: T[], sortKey: unknown, sortDirection: unknown): T[];
16
14
  export type SortDirection = "asc" | "desc";
17
15
  export declare function normalizeSortDirection(value: unknown): SortDirection;
@@ -7,9 +7,7 @@ exports.parseFilter = parseFilter;
7
7
  exports.unwrapIsFilter = unwrapIsFilter;
8
8
  exports.decodeInstanceFilter = decodeInstanceFilter;
9
9
  exports.applyFilterToList = applyFilterToList;
10
- exports.applySearchToList = applySearchToList;
11
10
  exports.cellToText = cellToText;
12
- exports.applySortToList = applySortToList;
13
11
  exports.normalizeSortDirection = normalizeSortDirection;
14
12
  const interface_database_1 = require("@antelopejs/interface-database");
15
13
  const constants_1 = require("../types/constants");
@@ -69,12 +67,6 @@ function applyFilterToList(rows, field, rawFilter) {
69
67
  return rows;
70
68
  return rows.filter((row) => predicate(String(row[field] ?? ""), parsed.value));
71
69
  }
72
- function applySearchToList(rows, field, search) {
73
- const needle = asNonEmptyString(search)?.toLowerCase();
74
- if (!needle)
75
- return rows;
76
- return rows.filter((row) => cellToText(row[field]).toLowerCase().includes(needle));
77
- }
78
70
  function cellToText(value) {
79
71
  if (value === null || value === undefined)
80
72
  return "";
@@ -97,18 +89,6 @@ function cellToText(value) {
97
89
  return "";
98
90
  }
99
91
  }
100
- function compareValues(a, b) {
101
- if (typeof a === "number" && typeof b === "number")
102
- return a - b;
103
- return cellToText(a).localeCompare(cellToText(b));
104
- }
105
- function applySortToList(rows, sortKey, sortDirection) {
106
- const key = asNonEmptyString(sortKey);
107
- if (!key)
108
- return rows;
109
- const direction = asNonEmptyString(sortDirection) === "desc" ? -1 : 1;
110
- return [...rows].sort((a, b) => compareValues(a[key], b[key]) * direction);
111
- }
112
92
  const SORT_DIRECTION_DESC = "desc";
113
93
  function normalizeSortDirection(value) {
114
94
  return asNonEmptyString(value) === SORT_DIRECTION_DESC ? "desc" : "asc";
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/utils/query.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/utils/query.ts"],"names":[],"mappings":"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"}
@@ -1,88 +1,103 @@
1
+ import { type BrowserTab, DEFAULT_INSTANCE_VALUE } from './useDataBrowserTabs'
1
2
  // Per-tab grid state (paging, sort, filters, column widths) and a data cache
2
3
  // so switching tabs restores instantly without a refetch. Keyed by
3
4
  // BrowserTab.id.
4
5
 
5
6
  export interface ColumnFilterState {
6
- mode: "is" | "contains";
7
- value: string;
7
+ mode: 'is' | 'contains'
8
+ value: string
8
9
  }
9
10
 
10
- export interface GridState {
11
- page: number;
12
- pageSize: number;
13
- sortKey: string | null;
14
- sortDirection: "asc" | "desc";
15
- search: string;
16
- filters: Record<string, ColumnFilterState>;
17
- columnWidths: Record<string, number>;
11
+ interface GridState {
12
+ page: number
13
+ pageSize: number
14
+ sortKey: string | null
15
+ sortDirection: 'asc' | 'desc'
16
+ search: string
17
+ filters: Record<string, ColumnFilterState>
18
+ columnWidths: Record<string, number>
18
19
  }
19
20
 
20
21
  export interface GridColumn {
21
- name: string;
22
+ name: string
22
23
  // Coarse runtime type sampled by the backend: string | number | boolean |
23
24
  // object | array | null | undefined.
24
- type: string;
25
- isPrimaryKey: boolean;
25
+ type: string
26
+ isPrimaryKey: boolean
26
27
  }
27
28
 
28
29
  export interface GridData {
29
- columns: GridColumn[];
30
- rows: Record<string, unknown>[];
31
- total: number;
32
- primaryKey: string | null;
30
+ columns: GridColumn[]
31
+ rows: Record<string, unknown>[]
32
+ total: number
33
+ primaryKey: string | null
33
34
  // Serialised query parameters that produced this payload; lets the grid
34
35
  // skip refetching when nothing changed.
35
- paramsKey: string;
36
+ paramsKey: string
36
37
  }
37
38
 
38
- export const GRID_PAGE_SIZES = [25, 50, 100, 200];
39
+ export const GRID_PAGE_SIZES = [25, 50, 100, 200]
39
40
 
40
41
  function defaultGridState(): GridState {
41
42
  return {
42
43
  page: 0,
43
- pageSize: 25,
44
+ pageSize: 50,
44
45
  sortKey: null,
45
- sortDirection: "asc",
46
- search: "",
46
+ sortDirection: 'asc',
47
+ search: '',
47
48
  filters: {},
48
49
  columnWidths: {},
49
- };
50
+ }
50
51
  }
51
52
 
52
53
  export function useDataBrowserGrid() {
53
54
  const states = useDmsState<Record<string, GridState>>(
54
- "dms-database-grid-states",
55
+ 'dms-database-grid-states',
55
56
  () => ({}),
56
- );
57
+ )
57
58
  const cache = useDmsState<Record<string, GridData>>(
58
- "dms-database-grid-cache",
59
+ 'dms-database-grid-cache',
59
60
  () => ({}),
60
- );
61
+ )
61
62
 
62
63
  function getState(tabId: string): GridState {
63
- if (!states.value[tabId]) states.value[tabId] = defaultGridState();
64
- return states.value[tabId];
64
+ if (!states.value[tabId]) states.value[tabId] = defaultGridState()
65
+ return states.value[tabId]
65
66
  }
66
67
 
67
68
  function getCache(tabId: string): GridData | null {
68
- return cache.value[tabId] ?? null;
69
+ return cache.value[tabId] ?? null
69
70
  }
70
71
 
71
72
  function setCache(tabId: string, data: GridData) {
72
- cache.value[tabId] = data;
73
+ cache.value[tabId] = data
73
74
  }
74
75
 
75
76
  function invalidate(tabId: string) {
76
- Reflect.deleteProperty(cache.value, tabId);
77
+ Reflect.deleteProperty(cache.value, tabId)
77
78
  }
78
79
 
79
80
  // Forget everything about a closed tab.
80
81
  function drop(tabId: string) {
81
- Reflect.deleteProperty(states.value, tabId);
82
- Reflect.deleteProperty(cache.value, tabId);
82
+ Reflect.deleteProperty(states.value, tabId)
83
+ Reflect.deleteProperty(cache.value, tabId)
84
+ }
85
+
86
+ // A tab switched to another instance keeps what the user narrowed the
87
+ // rows to (search, filters, sort, page size, widths), from its first page.
88
+ function carry(fromId: string, toId: string) {
89
+ const from = states.value[fromId]
90
+ if (!from) return
91
+ states.value[toId] = {
92
+ ...from,
93
+ filters: { ...from.filters },
94
+ columnWidths: { ...from.columnWidths },
95
+ page: 0,
96
+ }
97
+ Reflect.deleteProperty(cache.value, toId)
83
98
  }
84
99
 
85
- return { getState, getCache, setCache, invalidate, drop };
100
+ return { getState, getCache, setCache, invalidate, drop, carry }
86
101
  }
87
102
 
88
103
  // --- query helpers shared by the grid ---
@@ -91,10 +106,10 @@ export function useDataBrowserGrid() {
91
106
  // SELECTION_FILTER_KEYS): a data filter on a column literally named `schema`,
92
107
  // `table` or `instance` would clobber the selection parameters, so it is never
93
108
  // emitted and the filter UI is disabled for these columns.
94
- const RESERVED_FILTER_FIELDS = new Set(["schema", "table", "instance"]);
109
+ const RESERVED_FILTER_FIELDS = new Set(['schema', 'table', 'instance'])
95
110
 
96
111
  export function isReservedFilterField(field: string): boolean {
97
- return RESERVED_FILTER_FIELDS.has(field);
112
+ return RESERVED_FILTER_FIELDS.has(field)
98
113
  }
99
114
 
100
115
  // Normalized (empty-pruned, reserved-pruned, name-sorted) filter entries: the
@@ -105,18 +120,18 @@ function activeFilterEntries(
105
120
  ): Array<[string, ColumnFilterState]> {
106
121
  return Object.entries(state.filters)
107
122
  .filter(([field, filter]) => filter.value && !isReservedFilterField(field))
108
- .sort(([a], [b]) => a.localeCompare(b));
123
+ .sort(([a], [b]) => a.localeCompare(b))
109
124
  }
110
125
 
111
126
  // Single encoder for the `<mode>:<value>` wire tokens parseFilter decodes
112
127
  // server-side (src/utils/query.ts) — the grammar is defined here once instead
113
128
  // of scattered string literals.
114
- export function filterToken(mode: "is" | "contains", value: string): string {
115
- return `${mode}:${value}`;
129
+ function filterToken(mode: 'is' | 'contains', value: string): string {
130
+ return `${mode}:${value}`
116
131
  }
117
132
 
118
133
  export function isFilterToken(value: string): string {
119
- return filterToken("is", value);
134
+ return filterToken('is', value)
120
135
  }
121
136
 
122
137
  // Selection filters identifying the table, in the wire format the browse
@@ -125,11 +140,11 @@ export function browseSelectionQuery(tab: BrowserTab): Record<string, string> {
125
140
  const query: Record<string, string> = {
126
141
  filter_schema: isFilterToken(tab.schema),
127
142
  filter_table: isFilterToken(tab.table),
128
- };
143
+ }
129
144
  if (tab.instance !== DEFAULT_INSTANCE_VALUE) {
130
- query.filter_instance = isFilterToken(tab.instance);
145
+ query.filter_instance = isFilterToken(tab.instance)
131
146
  }
132
- return query;
147
+ return query
133
148
  }
134
149
 
135
150
  export function browseListQuery(
@@ -139,13 +154,13 @@ export function browseListQuery(
139
154
  offset: state.page * state.pageSize,
140
155
  limit: state.pageSize,
141
156
  sortDirection: state.sortDirection,
142
- };
143
- if (state.sortKey) query.sortKey = state.sortKey;
144
- if (state.search) query.search = state.search;
157
+ }
158
+ if (state.sortKey) query.sortKey = state.sortKey
159
+ if (state.search) query.search = state.search
145
160
  for (const [field, filter] of activeFilterEntries(state)) {
146
- query[`filter_${field}`] = filterToken(filter.mode, filter.value);
161
+ query[`filter_${field}`] = filterToken(filter.mode, filter.value)
147
162
  }
148
- return query;
163
+ return query
149
164
  }
150
165
 
151
166
  // Everything that changes the server payload (column widths deliberately not).
@@ -158,5 +173,5 @@ export function browseParamsKey(tab: BrowserTab, state: GridState): string {
158
173
  state.sortDirection,
159
174
  state.search,
160
175
  activeFilterEntries(state),
161
- ]);
176
+ ])
162
177
  }
@@ -0,0 +1,366 @@
1
+ import { CROSS_INSTANCE_SENTINEL } from '../utils/stagedSerialization'
2
+ import { useDataBrowserGrid } from './useDataBrowserGrid'
3
+ import type { InstanceChoice } from '../data/instanceOptions'
4
+ import { rememberTable } from '../data/recentTables'
5
+ import { useStagedEdits } from '../data/stagedEdits'
6
+ import {
7
+ decodeMatch,
8
+ instanceFromUrl,
9
+ instanceToUrl,
10
+ } from '../utils/databaseLinks'
11
+
12
+ // Shared state for the data-browser tabs: each tab pins a (schema, instance,
13
+ // table) triplet. The active tab is mirrored to the URL query (deep-links) and
14
+ // the whole tab set is persisted to sessionStorage (stays per browser tab); a
15
+ // page refresh brings every tab back, the preview one still a preview.
16
+
17
+ // A tab's instance as its id and the browse routes' `filter_instance` carry
18
+ // it: these two values, or a named instance id.
19
+ export const DEFAULT_INSTANCE_VALUE = '__DEFAULT__'
20
+ export const CROSS_INSTANCE_VALUE = CROSS_INSTANCE_SENTINEL
21
+
22
+ /**
23
+ * A tab's instance as a choice ("all": read-only), as pickers and the URL
24
+ * (see instanceFromUrl) carry it.
25
+ */
26
+ export function instanceChoiceOf(instance: string): InstanceChoice {
27
+ if (instance === DEFAULT_INSTANCE_VALUE) return { kind: 'default' }
28
+ if (instance === CROSS_INSTANCE_VALUE) return { kind: 'all' }
29
+ return { kind: 'named', id: instance }
30
+ }
31
+
32
+ /** The tab instance a choice stands for. */
33
+ export function tabInstanceOf(choice: InstanceChoice): string {
34
+ if (choice.kind === 'default') return DEFAULT_INSTANCE_VALUE
35
+ if (choice.kind === 'all') return CROSS_INSTANCE_VALUE
36
+ return choice.id
37
+ }
38
+
39
+ /** How a tab names its instance: null for the default one, "@eu", "@all". */
40
+ export function instanceBadge(
41
+ instance: string,
42
+ allLabel: string,
43
+ ): string | null {
44
+ if (instance === DEFAULT_INSTANCE_VALUE) return null
45
+ return `@${instance === CROSS_INSTANCE_VALUE ? allLabel : instance}`
46
+ }
47
+
48
+ export interface BrowserTab {
49
+ id: string
50
+ schema: string
51
+ // DEFAULT_INSTANCE_VALUE, CROSS_INSTANCE_VALUE or a named instance id.
52
+ instance: string
53
+ table: string
54
+ // VS Code-style preview tab: the next openTab reuses it instead of adding
55
+ // a new tab. At most one per session; pinning (double-click, pin icon,
56
+ // drag-reorder, editing a cell) clears the flag.
57
+ preview?: boolean
58
+ }
59
+
60
+ const STORAGE_KEY = 'dms-database:data-browser:tabs'
61
+ const ACTIVATIONS_STATE = 'dms-database-browser-activations'
62
+
63
+ function browserTabId(schema: string, instance: string, table: string): string {
64
+ return `${schema}::${instance}::${table}`
65
+ }
66
+
67
+ interface PersistedTabs {
68
+ tabs: BrowserTab[]
69
+ activeId: string | null
70
+ }
71
+
72
+ function readPersisted(): PersistedTabs | null {
73
+ if (typeof window === 'undefined') return null
74
+ try {
75
+ const raw = window.sessionStorage.getItem(STORAGE_KEY)
76
+ if (!raw) return null
77
+ const parsed = JSON.parse(raw) as PersistedTabs
78
+ if (!Array.isArray(parsed.tabs)) return null
79
+ const tabs = parsed.tabs
80
+ .filter(
81
+ (tab) =>
82
+ tab &&
83
+ typeof tab.schema === 'string' &&
84
+ typeof tab.instance === 'string' &&
85
+ typeof tab.table === 'string',
86
+ )
87
+ .map((tab) => ({
88
+ ...tab,
89
+ id: browserTabId(tab.schema, tab.instance, tab.table),
90
+ preview: tab.preview === true,
91
+ }))
92
+ // Recomputed ids can collide on a legacy/hand-edited payload; duplicate
93
+ // ids would break the tab bar's v-for keys and closeTab.
94
+ .filter(
95
+ (tab, index, all) => all.findIndex((t) => t.id === tab.id) === index,
96
+ )
97
+ // The preview slot is a singleton; a hand-edited payload could carry
98
+ // several — honour only the first.
99
+ const firstPreview = tabs.findIndex((tab) => tab.preview)
100
+ for (const [index, tab] of tabs.entries()) {
101
+ if (index !== firstPreview) tab.preview = false
102
+ }
103
+ const activeId = tabs.some((tab) => tab.id === parsed.activeId)
104
+ ? parsed.activeId
105
+ : (tabs[0]?.id ?? null)
106
+ return { tabs, activeId }
107
+ } catch {
108
+ return null
109
+ }
110
+ }
111
+
112
+ export function useDataBrowserTabs() {
113
+ const tabs = useDmsState<BrowserTab[]>('dms-database-browser-tabs', () => [])
114
+ const activeId = useDmsState<string | null>(
115
+ 'dms-database-browser-active',
116
+ () => null,
117
+ )
118
+ // Counts the activations, a tab already active included: the sidebar
119
+ // follows the tab the user goes to, even back to the same one.
120
+ const activations = useDmsState<number>(ACTIVATIONS_STATE, () => 0)
121
+ const route = useDmsRoute()
122
+ const router = useDmsRouter()
123
+ // Captured at setup so closeTab can drop the closed tab's
124
+ // grid state itself instead of relying on every caller to remember to.
125
+ const grid = useDataBrowserGrid()
126
+ const staged = useStagedEdits()
127
+
128
+ const activeTab = computed(
129
+ () => tabs.value.find((tab) => tab.id === activeId.value) ?? null,
130
+ )
131
+ // When tabs span several schemas the tab bar disambiguates its labels.
132
+ const hasMultipleSchemas = computed(
133
+ () => new Set(tabs.value.map((tab) => tab.schema)).size > 1,
134
+ )
135
+
136
+ function persist() {
137
+ if (typeof window === 'undefined') return
138
+ try {
139
+ window.sessionStorage.setItem(
140
+ STORAGE_KEY,
141
+ JSON.stringify({ tabs: tabs.value, activeId: activeId.value }),
142
+ )
143
+ } catch {
144
+ // Quota/private-mode failures only cost tab restoration.
145
+ }
146
+ }
147
+
148
+ // The URL carries the ACTIVE tab only (deep-links); per-tab grid state
149
+ // deliberately stays out of it.
150
+ function syncUrl() {
151
+ const tab = activeTab.value
152
+ const query = { ...route.query }
153
+ delete query.schema
154
+ delete query.instance
155
+ delete query.table
156
+ delete query.match
157
+ if (tab) {
158
+ query.schema = tab.schema
159
+ query.table = tab.table
160
+ const instance = instanceToUrl(instanceChoiceOf(tab.instance))
161
+ if (instance) query.instance = instance
162
+ }
163
+ if (
164
+ Object.keys(query).length === Object.keys(route.query).length &&
165
+ Object.entries(query).every(([key, value]) => route.query[key] === value)
166
+ )
167
+ return
168
+ void router.replace({ query })
169
+ }
170
+
171
+ function activateTab(id: string) {
172
+ const tab = tabs.value.find((candidate) => candidate.id === id)
173
+ if (!tab) return
174
+ rememberTable({
175
+ schema: tab.schema,
176
+ instance: tab.instance,
177
+ table: tab.table,
178
+ })
179
+ activeId.value = id
180
+ activations.value += 1
181
+ syncUrl()
182
+ persist()
183
+ }
184
+
185
+ // Promote a preview tab to a permanent one (double-click, pin icon, drag).
186
+ function pinTab(id: string) {
187
+ const tab = tabs.value.find((t) => t.id === id)
188
+ if (!tab?.preview) return
189
+ tabs.value = tabs.value.map((t) =>
190
+ t.id === id ? { ...t, preview: false } : t,
191
+ )
192
+ persist()
193
+ }
194
+
195
+ // Opens as a preview by default (VS Code-style: single-click reuses the
196
+ // preview slot). Pass preview = false for an explicitly permanent open
197
+ // (sidebar double-click, deep-links) — it also pins the tab if already
198
+ // open as preview.
199
+ function openTab(
200
+ schema: string,
201
+ instance: string,
202
+ table: string,
203
+ preview = true,
204
+ ) {
205
+ const id = browserTabId(schema, instance, table)
206
+ const existing = tabs.value.find((tab) => tab.id === id)
207
+ if (existing) {
208
+ if (!preview) pinTab(id)
209
+ activateTab(id)
210
+ return
211
+ }
212
+ const tab: BrowserTab = { id, schema, instance, table, preview }
213
+ let previewIndex = tabs.value.findIndex((t) => t.preview)
214
+ // A tab holding staged edits is never replaced: it is kept instead
215
+ // (staging a cell already keeps it; this guards any other path).
216
+ if (previewIndex >= 0 && staged.count(tabs.value[previewIndex]!.id) > 0) {
217
+ pinTab(tabs.value[previewIndex]!.id)
218
+ previewIndex = -1
219
+ }
220
+ if (preview && previewIndex >= 0) {
221
+ // Reuse the preview slot in place: swap its content, drop the replaced
222
+ // tab's grid state (its id dies with it).
223
+ const replaced = tabs.value[previewIndex]!
224
+ const next = [...tabs.value]
225
+ next[previewIndex] = tab
226
+ tabs.value = next
227
+ grid.drop(replaced.id)
228
+ } else {
229
+ tabs.value = [...tabs.value, tab]
230
+ }
231
+ activateTab(id)
232
+ }
233
+
234
+ /**
235
+ * Puts another table in a tab's place (an instance switch): same position,
236
+ * same preview state, and the grid's search, filters and sort carried
237
+ * over. A tab already open on that table is activated instead, and the
238
+ * replaced one closed.
239
+ */
240
+ function replaceTab(
241
+ oldId: string,
242
+ schema: string,
243
+ instance: string,
244
+ table: string,
245
+ ) {
246
+ const index = tabs.value.findIndex((tab) => tab.id === oldId)
247
+ const old = tabs.value[index]
248
+ if (!old) return openTab(schema, instance, table, false)
249
+ const id = browserTabId(schema, instance, table)
250
+ if (id === oldId) return activateTab(id)
251
+ if (tabs.value.some((tab) => tab.id === id)) {
252
+ tabs.value = tabs.value.filter((tab) => tab.id !== oldId)
253
+ grid.drop(oldId)
254
+ activateTab(id)
255
+ return
256
+ }
257
+ const next = [...tabs.value]
258
+ next[index] = { id, schema, instance, table, preview: old.preview }
259
+ tabs.value = next
260
+ grid.carry(oldId, id)
261
+ grid.drop(oldId)
262
+ activateTab(id)
263
+ }
264
+
265
+ function closeTab(id: string) {
266
+ const index = tabs.value.findIndex((tab) => tab.id === id)
267
+ if (index < 0) return
268
+ const next = tabs.value.filter((tab) => tab.id !== id)
269
+ tabs.value = next
270
+ if (activeId.value === id) {
271
+ // Right neighbour first, then left — mirrors browser tab behaviour.
272
+ activeId.value = (next[index] ?? next[index - 1])?.id ?? null
273
+ }
274
+ // The closed tab's grid state and cached rows go with it — otherwise they
275
+ // leak for the rest of the session.
276
+ grid.drop(id)
277
+ syncUrl()
278
+ persist()
279
+ }
280
+
281
+ // Reorder (drag & drop): move the dragged tab to the target tab's position.
282
+ // Pure reorder — pin-on-drag is a gesture policy and lives at the drop site
283
+ // (the tab bar's onDrop), not in this primitive.
284
+ function moveTab(draggedId: string, targetId: string) {
285
+ if (draggedId === targetId) return
286
+ const next = [...tabs.value]
287
+ const from = next.findIndex((tab) => tab.id === draggedId)
288
+ const to = next.findIndex((tab) => tab.id === targetId)
289
+ if (from < 0 || to < 0) return
290
+ const [moved] = next.splice(from, 1)
291
+ if (!moved) return
292
+ next.splice(to, 0, moved)
293
+ tabs.value = next
294
+ persist()
295
+ }
296
+
297
+ // Opens the table the URL names (overview links, the inspector's "Browse
298
+ // data", relation links, shared URLs): permanently, since the preview
299
+ // slot is for casual browsing from the list.
300
+ // The table the URL names, if any.
301
+ function routeTable() {
302
+ const schema = route.query.schema
303
+ const table = route.query.table
304
+ if (
305
+ typeof schema !== 'string' ||
306
+ !schema ||
307
+ typeof table !== 'string' ||
308
+ !table
309
+ )
310
+ return null
311
+ const instance = tabInstanceOf(instanceFromUrl(route.query.instance))
312
+ return {
313
+ schema,
314
+ instance,
315
+ table,
316
+ id: browserTabId(schema, instance, table),
317
+ // A link to one row (a relation, a reference) opens the table filtered
318
+ // on it; the filter shows as a chip the user removes.
319
+ match: decodeMatch(route.query.match),
320
+ }
321
+ }
322
+
323
+ function openFromRoute() {
324
+ const target = routeTable()
325
+ if (!target) return false
326
+ const { schema, instance, table, id, match } = target
327
+ // The route names the active tab: the browser wrote it itself (a tab
328
+ // activated, a reload), so it opens nothing and a preview tab stays one.
329
+ if (!match && id === activeId.value) return true
330
+ if (match) {
331
+ const state = grid.getState(id)
332
+ state.filters = { [match.field]: { mode: 'is', value: match.value } }
333
+ state.page = 0
334
+ }
335
+ openTab(schema, instance, table, false)
336
+ return true
337
+ }
338
+
339
+ // Restore persisted tabs, preview included, then let the URL win for the
340
+ // active selection. Call once, from the page's onMounted.
341
+ function restore() {
342
+ const persisted = readPersisted()
343
+ if (persisted) {
344
+ tabs.value = persisted.tabs
345
+ activeId.value = persisted.activeId
346
+ }
347
+ // A schema-only link (?schema=X) is the sidebar's to honour.
348
+ if (!openFromRoute() && !route.query.schema && activeTab.value) syncUrl()
349
+ }
350
+
351
+ return {
352
+ tabs,
353
+ activeId,
354
+ activeTab,
355
+ activations,
356
+ hasMultipleSchemas,
357
+ openTab,
358
+ replaceTab,
359
+ closeTab,
360
+ activateTab,
361
+ pinTab,
362
+ moveTab,
363
+ restore,
364
+ openFromRoute,
365
+ }
366
+ }