collabmd 0.1.32 → 0.1.34

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 (43) hide show
  1. package/dist/client/assets/{drawio-editor-Br4qyh3r.js → drawio-editor-BMTwP5mD.js} +1 -1
  2. package/dist/client/assets/{drawioEditor-ClmnTx5J.js → drawioEditor-D6clB5OH.js} +2 -2
  3. package/dist/client/assets/{editor-session-Cf-iV5-3.js → editor-session-Dzy_85Wp.js} +1 -1
  4. package/dist/client/assets/{excalidraw-editor-CXAxVhlw.js → excalidraw-editor-CAP9F8CB.js} +3 -3
  5. package/dist/client/assets/{excalidrawEditor-DoH_kMdu.js → excalidrawEditor-DD4AvzYP.js} +2 -2
  6. package/dist/client/assets/{exportDocument-9wTrBZNS.css → exportDocument-BbkN85c4.css} +1 -1
  7. package/dist/client/assets/index-BIt5EZXz.css +1 -0
  8. package/dist/client/assets/{index-C8QZYHvV.js → index-BLrtj9YM.js} +2 -2
  9. package/dist/client/assets/main-CfSHaGes.js +1368 -0
  10. package/dist/client/assets/{vault-api-client-Bf9tB_GW.js → vault-api-client-p7h7cXKM.js} +1 -1
  11. package/dist/client/drawio-editor.html +1 -1
  12. package/dist/client/excalidraw-editor.html +1 -1
  13. package/dist/client/export-document.html +2 -2
  14. package/dist/client/index.html +2 -2
  15. package/package.json +6 -5
  16. package/src/client/application/app-shell/git-feature.js +44 -36
  17. package/src/client/application/app-shell/ui-feature-shell.js +84 -63
  18. package/src/client/bootstrap/collabmd-app-shell.js +6 -0
  19. package/src/client/infrastructure/editor-session.js +4 -0
  20. package/src/client/infrastructure/editor-view-adapter.js +23 -0
  21. package/src/client/infrastructure/vault-api-client.js +48 -0
  22. package/src/client/presentation/bases-preview-controller.js +1388 -28
  23. package/src/client/presentation/comments-panel.js +50 -5
  24. package/src/client/presentation/excalidraw-embed-controller.js +4 -1
  25. package/src/client/presentation/file-history-view-controller.js +9 -1
  26. package/src/client/presentation/git-panel-controller.js +101 -91
  27. package/src/client/styles/components/scrollbars.css +5 -0
  28. package/src/client/styles/features/preview-markdown.css +468 -23
  29. package/src/server/domain/backlink-index.js +202 -11
  30. package/src/server/domain/bases/base-definition.js +138 -11
  31. package/src/server/domain/bases/base-expression-runtime.js +54 -17
  32. package/src/server/domain/bases/base-index-snapshot-store.js +167 -20
  33. package/src/server/domain/bases/base-query-metadata.js +242 -0
  34. package/src/server/domain/bases/base-query-results.js +18 -8
  35. package/src/server/domain/bases/base-query-service.js +360 -49
  36. package/src/server/domain/bases/base-transform.js +116 -0
  37. package/src/server/infrastructure/http/create-request-handler.js +63 -28
  38. package/src/server/infrastructure/http/create-vault-api-command-handler.js +275 -256
  39. package/src/server/infrastructure/http/create-vault-api-query-handler.js +258 -184
  40. package/src/server/infrastructure/persistence/vault-file-store.js +68 -12
  41. package/dist/client/assets/index-CxSbUTs2.css +0 -1
  42. package/dist/client/assets/main-BjHHMlv0.js +0 -1249
  43. /package/dist/client/assets/{exportDocument-Bia2hI25.js → exportDocument-Zfwq1XVx.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildColumns,
3
3
  collectEvaluatedPropertyIds,
4
+ findView,
4
5
  normalizeBaseDefinition,
5
6
  } from './base-definition.js';
6
7
  import {
@@ -8,12 +9,97 @@ import {
8
9
  createEvaluationRootContext,
9
10
  evaluateFilterNode,
10
11
  getPropertyValue,
12
+ serializeBaseValue,
11
13
  } from './base-expression-runtime.js';
12
14
  import { BaseIndexSnapshotStore } from './base-index-snapshot-store.js';
15
+ import {
16
+ buildAvailableProperties,
17
+ collectDistinctPropertyValues,
18
+ createPropertyCatalogSnapshot,
19
+ inferValueType,
20
+ } from './base-query-metadata.js';
13
21
  import { buildQueryResultPayload, rowMatchesSearch } from './base-query-results.js';
22
+ import { transformBaseSource } from './base-transform.js';
14
23
 
15
24
  export { serializeBaseDefinition } from './base-definition.js';
16
25
 
26
+ function buildSortChain(activeView) {
27
+ const entries = [];
28
+ (activeView.sort ?? []).forEach((sortConfig) => {
29
+ if (
30
+ sortConfig?.property
31
+ && !entries.some((entry) => entry.property === sortConfig.property)
32
+ ) {
33
+ entries.push(sortConfig);
34
+ }
35
+ });
36
+ return entries;
37
+ }
38
+
39
+ function sortRows(rows, sortChain = []) {
40
+ if (!Array.isArray(sortChain) || sortChain.length === 0) {
41
+ return rows;
42
+ }
43
+
44
+ return rows.slice().sort((left, right) => {
45
+ for (const sortConfig of sortChain) {
46
+ const delta = compareValues(left.rawCells[sortConfig.property], right.rawCells[sortConfig.property]);
47
+ if (delta !== 0) {
48
+ return sortConfig.direction === 'desc' ? -delta : delta;
49
+ }
50
+ }
51
+
52
+ return String(left.file.path ?? '').localeCompare(String(right.file.path ?? ''));
53
+ });
54
+ }
55
+
56
+ function escapeRegExp(value = '') {
57
+ return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
58
+ }
59
+
60
+ function pruneFilterNodeForProperty(filterNode, propertyId = '') {
61
+ if (!propertyId || !filterNode) {
62
+ return filterNode;
63
+ }
64
+
65
+ if (typeof filterNode === 'string') {
66
+ const escapedPropertyId = escapeRegExp(propertyId);
67
+ const propertyFilterPattern = new RegExp(
68
+ `^!?${escapedPropertyId}(?:\\.(?:contains|startsWith|endsWith|isEmpty)\\(.*\\)|\\s*(?:==|!=|>=|<=|>|<)\\s*.+)$`,
69
+ 'u',
70
+ );
71
+ return propertyFilterPattern.test(String(filterNode ?? '').trim()) ? null : filterNode;
72
+ }
73
+
74
+ if (Array.isArray(filterNode?.and)) {
75
+ const children = filterNode.and
76
+ .map((child) => pruneFilterNodeForProperty(child, propertyId))
77
+ .filter(Boolean);
78
+ return children.length > 0 ? { and: children } : null;
79
+ }
80
+
81
+ if (Array.isArray(filterNode?.or)) {
82
+ const children = filterNode.or
83
+ .map((child) => pruneFilterNodeForProperty(child, propertyId))
84
+ .filter(Boolean);
85
+ return children.length > 0 ? { or: children } : null;
86
+ }
87
+
88
+ if (filterNode?.not != null) {
89
+ if (Array.isArray(filterNode.not)) {
90
+ const children = filterNode.not
91
+ .map((child) => pruneFilterNodeForProperty(child, propertyId))
92
+ .filter(Boolean);
93
+ return children.length > 0 ? { not: children } : null;
94
+ }
95
+
96
+ const child = pruneFilterNodeForProperty(filterNode.not, propertyId);
97
+ return child ? { not: child } : null;
98
+ }
99
+
100
+ return filterNode;
101
+ }
102
+
17
103
  export class BaseQueryService {
18
104
  constructor({
19
105
  vaultFileStore,
@@ -26,6 +112,7 @@ export class BaseQueryService {
26
112
  workspaceStateProvider,
27
113
  workspaceStateSynchronizer,
28
114
  });
115
+ this.propertyCatalogCache = new Map();
29
116
  }
30
117
 
31
118
  get workspaceStateProvider() {
@@ -105,13 +192,27 @@ export class BaseQueryService {
105
192
  }
106
193
 
107
194
  async applyWorkspaceChange(...args) {
108
- return this.snapshotStore.applyWorkspaceChange(...args);
195
+ const result = await this.snapshotStore.applyWorkspaceChange(...args);
196
+ const scannedAt = this.snapshotStore.indexSnapshot?.scannedAt;
197
+ if (scannedAt) {
198
+ this.invalidatePropertyCatalogsExcept(scannedAt);
199
+ } else {
200
+ this.propertyCatalogCache.clear();
201
+ }
202
+ return result;
109
203
  }
110
204
 
111
- async query({
205
+ invalidatePropertyCatalogsExcept(scannedAt = '') {
206
+ Array.from(this.propertyCatalogCache.keys()).forEach((cacheKey) => {
207
+ if (cacheKey !== scannedAt) {
208
+ this.propertyCatalogCache.delete(cacheKey);
209
+ }
210
+ });
211
+ }
212
+
213
+ async resolveQueryContext({
112
214
  activeFilePath = '',
113
215
  basePath = '',
114
- search = '',
115
216
  source = null,
116
217
  sourcePath = '',
117
218
  view: requestedView = '',
@@ -126,74 +227,195 @@ export class BaseQueryService {
126
227
  basePath,
127
228
  sourcePath,
128
229
  });
230
+ this.invalidatePropertyCatalogsExcept(snapshot?.scannedAt ?? '');
231
+
129
232
  const thisFilePath = sourcePath || activeFilePath || basePath || '';
130
233
  const thisFile = snapshot.rowsByPath.get(thisFilePath)?.file ?? null;
131
- const activeView = definition.views.find((entry) => entry.name === requestedView || entry.id === requestedView) ?? definition.views[0];
234
+ const activeView = findView(definition, requestedView);
132
235
  const columns = buildColumns(definition, activeView);
133
236
  const evaluatedPropertyIds = collectEvaluatedPropertyIds(columns, activeView);
134
237
 
135
- let rows = snapshot.filePaths
136
- .map((filePath) => snapshot.rowsByPath.get(filePath))
137
- .filter(Boolean)
138
- .filter((row) => {
139
- const globalContext = createEvaluationRootContext({
140
- currentRow: row,
141
- definition,
142
- snapshot,
143
- thisFile,
144
- });
145
- if (!evaluateFilterNode(definition.filters, globalContext)) {
146
- return false;
147
- }
238
+ return {
239
+ activeView,
240
+ basePath,
241
+ baseSource,
242
+ columns,
243
+ definition,
244
+ evaluatedPropertyIds,
245
+ snapshot,
246
+ sourcePath,
247
+ thisFile,
248
+ };
249
+ }
148
250
 
149
- const viewContext = createEvaluationRootContext({
150
- currentRow: row,
151
- definition,
152
- snapshot,
153
- thisFile,
154
- });
155
- return evaluateFilterNode(activeView.filters, viewContext);
156
- })
157
- .map((row) => {
158
- const rawCells = {};
159
- evaluatedPropertyIds.forEach((propertyId) => {
160
- rawCells[propertyId] = getPropertyValue(propertyId, row, definition, snapshot, thisFile);
161
- });
162
- return {
163
- file: row.file,
164
- rawCells,
165
- };
251
+ collectCandidateRows({
252
+ activeView,
253
+ astCache = new Map(),
254
+ definition,
255
+ evaluatedPropertyIds,
256
+ snapshot,
257
+ thisFile,
258
+ }) {
259
+ const rows = [];
260
+
261
+ snapshot.filePaths.forEach((filePath) => {
262
+ const row = snapshot.rowsByPath.get(filePath);
263
+ if (!row) {
264
+ return;
265
+ }
266
+
267
+ const rowContext = this.createRowQueryContext({
268
+ astCache,
269
+ definition,
270
+ row,
271
+ snapshot,
272
+ thisFile,
166
273
  });
274
+ if (!evaluateFilterNode(definition.filters, rowContext) || !evaluateFilterNode(activeView.filters, rowContext)) {
275
+ return;
276
+ }
167
277
 
168
- activeView.sort.forEach((sortConfig) => {
169
- rows = rows.slice().sort((left, right) => {
170
- const delta = compareValues(left.rawCells[sortConfig.property], right.rawCells[sortConfig.property]);
171
- return sortConfig.direction === 'desc' ? -delta : delta;
278
+ const rawCells = {};
279
+ evaluatedPropertyIds.forEach((propertyId) => {
280
+ rawCells[propertyId] = getPropertyValue(propertyId, row, definition, snapshot, thisFile, rowContext);
281
+ });
282
+
283
+ rows.push({
284
+ file: row.file,
285
+ rawCells,
172
286
  });
173
287
  });
174
288
 
175
- if (activeView.limit != null) {
176
- rows = rows.slice(0, activeView.limit);
289
+ return rows;
290
+ }
291
+
292
+ createRowQueryContext({
293
+ astCache = new Map(),
294
+ definition,
295
+ row,
296
+ snapshot,
297
+ thisFile,
298
+ }) {
299
+ return createEvaluationRootContext({
300
+ astCache,
301
+ currentRow: row,
302
+ definition,
303
+ snapshot,
304
+ thisFile,
305
+ });
306
+ }
307
+
308
+ getPropertyCatalog(snapshot) {
309
+ const cacheKey = snapshot?.scannedAt ?? 'unknown';
310
+ if (!this.propertyCatalogCache.has(cacheKey)) {
311
+ this.propertyCatalogCache.set(cacheKey, createPropertyCatalogSnapshot(snapshot));
177
312
  }
178
313
 
179
- rows = rows.filter((row) => rowMatchesSearch(row, columns, search));
314
+ return this.propertyCatalogCache.get(cacheKey);
315
+ }
316
+
317
+ inferFormulaValueTypes(definition, rows = []) {
318
+ const valueTypes = {};
319
+
320
+ Object.keys(definition.formulas ?? {}).forEach((propertyId) => {
321
+ let inferred = 'unknown';
322
+ for (const row of rows) {
323
+ inferred = inferValueType(row?.rawCells?.[propertyId]);
324
+ if (inferred !== 'unknown') {
325
+ break;
326
+ }
327
+ }
328
+ valueTypes[propertyId] = inferred;
329
+ });
330
+
331
+ return valueTypes;
332
+ }
333
+
334
+ buildMeta({
335
+ activeView,
336
+ basePath = '',
337
+ columns,
338
+ definition,
339
+ rows,
340
+ snapshot,
341
+ }) {
342
+ const propertyCatalog = this.getPropertyCatalog(snapshot);
343
+ const formulaValueTypes = this.inferFormulaValueTypes(definition, rows);
180
344
 
181
345
  return {
182
- columns,
183
- definition,
184
- ...buildQueryResultPayload({
346
+ activeViewConfig: {
347
+ filters: activeView.filters ?? null,
348
+ groupBy: activeView.groupBy ?? null,
349
+ order: [...(activeView.order ?? [])],
350
+ sort: (activeView.sort ?? []).map((entry) => ({ ...entry })),
351
+ },
352
+ availableProperties: buildAvailableProperties({
185
353
  activeView,
186
354
  columns,
187
355
  definition,
356
+ formulaValueTypes,
357
+ propertyCatalog,
358
+ }),
359
+ editable: Boolean(basePath),
360
+ };
361
+ }
362
+
363
+ async query({
364
+ activeFilePath = '',
365
+ basePath = '',
366
+ includeCsv = false,
367
+ search = '',
368
+ source = null,
369
+ sourcePath = '',
370
+ view: requestedView = '',
371
+ } = {}) {
372
+ const context = await this.resolveQueryContext({
373
+ activeFilePath,
374
+ basePath,
375
+ source,
376
+ sourcePath,
377
+ view: requestedView,
378
+ });
379
+
380
+ const astCache = new Map();
381
+ let rows = this.collectCandidateRows({
382
+ ...context,
383
+ astCache,
384
+ });
385
+ rows = rows.filter((row) => rowMatchesSearch(row, context.columns, search));
386
+ rows = sortRows(rows, buildSortChain(context.activeView));
387
+
388
+ if (context.activeView.limit != null) {
389
+ rows = rows.slice(0, context.activeView.limit);
390
+ }
391
+
392
+ const payload = buildQueryResultPayload({
393
+ activeView: context.activeView,
394
+ columns: context.columns,
395
+ definition: context.definition,
396
+ includeCsv,
397
+ rows,
398
+ snapshot: context.snapshot,
399
+ thisFile: context.thisFile,
400
+ });
401
+
402
+ return {
403
+ columns: context.columns,
404
+ definition: context.definition,
405
+ meta: this.buildMeta({
406
+ activeView: context.activeView,
407
+ basePath,
408
+ columns: context.columns,
409
+ definition: context.definition,
188
410
  rows,
189
- snapshot,
190
- thisFile,
411
+ snapshot: context.snapshot,
191
412
  }),
413
+ ...payload,
192
414
  view: {
193
- ...activeView,
194
- supported: activeView.supported,
415
+ ...context.activeView,
416
+ supported: context.activeView.supported,
195
417
  },
196
- views: definition.views.map((view) => ({
418
+ views: context.definition.views.map((view) => ({
197
419
  id: view.id,
198
420
  name: view.name,
199
421
  supported: view.supported,
@@ -201,4 +423,93 @@ export class BaseQueryService {
201
423
  })),
202
424
  };
203
425
  }
426
+
427
+ async propertyValues({
428
+ activeFilePath = '',
429
+ basePath = '',
430
+ propertyId = '',
431
+ query = '',
432
+ source = null,
433
+ sourcePath = '',
434
+ view: requestedView = '',
435
+ } = {}) {
436
+ if (!propertyId) {
437
+ throw new Error('Missing propertyId');
438
+ }
439
+
440
+ const context = await this.resolveQueryContext({
441
+ activeFilePath,
442
+ basePath,
443
+ source,
444
+ sourcePath,
445
+ view: requestedView,
446
+ });
447
+ const propertyValueContext = {
448
+ ...context,
449
+ activeView: {
450
+ ...context.activeView,
451
+ filters: pruneFilterNodeForProperty(context.activeView?.filters, propertyId),
452
+ },
453
+ definition: {
454
+ ...context.definition,
455
+ filters: pruneFilterNodeForProperty(context.definition?.filters, propertyId),
456
+ },
457
+ };
458
+ const astCache = new Map();
459
+ const rows = this.collectCandidateRows({
460
+ ...propertyValueContext,
461
+ astCache,
462
+ evaluatedPropertyIds: [...new Set([
463
+ ...(context.evaluatedPropertyIds ?? []),
464
+ propertyId,
465
+ ])],
466
+ });
467
+ const values = collectDistinctPropertyValues(rows, propertyId, query).map((entry) => ({
468
+ count: entry.count,
469
+ text: entry.text,
470
+ value: serializeBaseValue(entry.value, {
471
+ snapshot: context.snapshot,
472
+ sourcePath: context.thisFile?.path ?? '',
473
+ }),
474
+ }));
475
+
476
+ return {
477
+ ok: true,
478
+ propertyId,
479
+ values,
480
+ };
481
+ }
482
+
483
+ async transform({
484
+ activeFilePath = '',
485
+ basePath = '',
486
+ mutation = null,
487
+ source = null,
488
+ sourcePath = '',
489
+ view = '',
490
+ } = {}) {
491
+ if (!basePath) {
492
+ throw new Error('Only .base files can be transformed');
493
+ }
494
+
495
+ const baseSource = source ?? await this.vaultFileStore.readBaseFile(basePath);
496
+ if (typeof baseSource !== 'string') {
497
+ throw new Error('Base source not found');
498
+ }
499
+
500
+ const nextSource = transformBaseSource(baseSource, mutation);
501
+ const result = await this.query({
502
+ activeFilePath,
503
+ basePath,
504
+ source: nextSource,
505
+ sourcePath,
506
+ view,
507
+ });
508
+
509
+ return {
510
+ meta: result.meta,
511
+ result,
512
+ source: nextSource,
513
+ };
514
+ }
204
515
  }
@@ -0,0 +1,116 @@
1
+ import yaml from 'js-yaml';
2
+
3
+ import {
4
+ findView,
5
+ normalizeBaseDefinition,
6
+ normalizeRawDefinitionForWrite,
7
+ } from './base-definition.js';
8
+
9
+ function isPlainObject(value) {
10
+ return Object.prototype.toString.call(value) === '[object Object]';
11
+ }
12
+
13
+ function cloneRawSource(source = '') {
14
+ const parsed = yaml.load(String(source ?? '')) ?? {};
15
+ return isPlainObject(parsed) ? parsed : {};
16
+ }
17
+
18
+ function ensureViews(rawDefinition) {
19
+ if (!Array.isArray(rawDefinition.views) || rawDefinition.views.length === 0) {
20
+ rawDefinition.views = [{ name: 'Table', type: 'table' }];
21
+ }
22
+
23
+ return rawDefinition.views;
24
+ }
25
+
26
+ function findViewIndex(definition, requestedView = '') {
27
+ const activeView = findView(definition, requestedView);
28
+ return definition.views.findIndex((view) => view.id === activeView.id);
29
+ }
30
+
31
+ function serializeGroupBy(groupBy) {
32
+ if (!groupBy?.property) {
33
+ return null;
34
+ }
35
+
36
+ return {
37
+ direction: String(groupBy.direction ?? 'asc').toUpperCase(),
38
+ property: groupBy.property,
39
+ };
40
+ }
41
+
42
+ function serializeSort(sort = []) {
43
+ return sort
44
+ .filter((entry) => entry?.property)
45
+ .map((entry) => ({
46
+ direction: String(entry.direction ?? 'asc').toUpperCase(),
47
+ property: entry.property,
48
+ }));
49
+ }
50
+
51
+ function applySetViewConfig(rawDefinition, definition, mutation = {}) {
52
+ const viewIndex = findViewIndex(definition, mutation.view ?? '');
53
+ if (viewIndex < 0) {
54
+ throw new Error('Base view not found');
55
+ }
56
+
57
+ const rawViews = ensureViews(rawDefinition);
58
+ const nextView = isPlainObject(rawViews[viewIndex])
59
+ ? { ...rawViews[viewIndex] }
60
+ : {};
61
+ const nextConfig = isPlainObject(mutation.config) ? mutation.config : {};
62
+
63
+ if (Object.hasOwn(nextConfig, 'order')) {
64
+ nextView.order = Array.isArray(nextConfig.order)
65
+ ? nextConfig.order.filter((entry) => typeof entry === 'string' && entry.trim())
66
+ : [];
67
+ }
68
+
69
+ if (Object.hasOwn(nextConfig, 'sort')) {
70
+ const sort = serializeSort(nextConfig.sort);
71
+ if (sort.length > 0) {
72
+ nextView.sort = sort;
73
+ } else {
74
+ delete nextView.sort;
75
+ delete nextView.sorts;
76
+ }
77
+ }
78
+
79
+ if (Object.hasOwn(nextConfig, 'groupBy')) {
80
+ const groupBy = serializeGroupBy(nextConfig.groupBy);
81
+ if (groupBy) {
82
+ nextView.groupBy = groupBy;
83
+ } else {
84
+ delete nextView.groupBy;
85
+ delete nextView.group_by;
86
+ }
87
+ }
88
+
89
+ if (Object.hasOwn(nextConfig, 'filters')) {
90
+ if (nextConfig.filters == null || nextConfig.filters === '') {
91
+ delete nextView.filters;
92
+ } else {
93
+ nextView.filters = nextConfig.filters;
94
+ }
95
+ }
96
+
97
+ rawViews[viewIndex] = nextView;
98
+ }
99
+
100
+ export function transformBaseSource(source = '', mutation = {}) {
101
+ const definition = normalizeBaseDefinition(source);
102
+ const rawDefinition = normalizeRawDefinitionForWrite({
103
+ raw: cloneRawSource(source),
104
+ });
105
+ const mutationType = String(mutation?.type ?? '');
106
+
107
+ switch (mutationType) {
108
+ case 'set-view-config':
109
+ applySetViewConfig(rawDefinition, definition, mutation);
110
+ break;
111
+ default:
112
+ throw new Error(`Unsupported base mutation: ${mutationType || 'unknown'}`);
113
+ }
114
+
115
+ return `${yaml.dump(normalizeRawDefinitionForWrite(rawDefinition), { lineWidth: -1, noRefs: true }).trim()}\n`;
116
+ }