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.
- package/dist/client/assets/{drawio-editor-Br4qyh3r.js → drawio-editor-BMTwP5mD.js} +1 -1
- package/dist/client/assets/{drawioEditor-ClmnTx5J.js → drawioEditor-D6clB5OH.js} +2 -2
- package/dist/client/assets/{editor-session-Cf-iV5-3.js → editor-session-Dzy_85Wp.js} +1 -1
- package/dist/client/assets/{excalidraw-editor-CXAxVhlw.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-DoH_kMdu.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/{exportDocument-9wTrBZNS.css → exportDocument-BbkN85c4.css} +1 -1
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-C8QZYHvV.js → index-BLrtj9YM.js} +2 -2
- package/dist/client/assets/main-CfSHaGes.js +1368 -0
- package/dist/client/assets/{vault-api-client-Bf9tB_GW.js → vault-api-client-p7h7cXKM.js} +1 -1
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +6 -5
- package/src/client/application/app-shell/git-feature.js +44 -36
- package/src/client/application/app-shell/ui-feature-shell.js +84 -63
- package/src/client/bootstrap/collabmd-app-shell.js +6 -0
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -0
- package/src/client/infrastructure/vault-api-client.js +48 -0
- package/src/client/presentation/bases-preview-controller.js +1388 -28
- package/src/client/presentation/comments-panel.js +50 -5
- package/src/client/presentation/excalidraw-embed-controller.js +4 -1
- package/src/client/presentation/file-history-view-controller.js +9 -1
- package/src/client/presentation/git-panel-controller.js +101 -91
- package/src/client/styles/components/scrollbars.css +5 -0
- package/src/client/styles/features/preview-markdown.css +468 -23
- package/src/server/domain/backlink-index.js +202 -11
- package/src/server/domain/bases/base-definition.js +138 -11
- package/src/server/domain/bases/base-expression-runtime.js +54 -17
- package/src/server/domain/bases/base-index-snapshot-store.js +167 -20
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +18 -8
- package/src/server/domain/bases/base-query-service.js +360 -49
- package/src/server/domain/bases/base-transform.js +116 -0
- package/src/server/infrastructure/http/create-request-handler.js +63 -28
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +275 -256
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +258 -184
- package/src/server/infrastructure/persistence/vault-file-store.js +68 -12
- package/dist/client/assets/index-CxSbUTs2.css +0 -1
- package/dist/client/assets/main-BjHHMlv0.js +0 -1249
- /package/dist/client/assets/{exportDocument-Bia2hI25.js → exportDocument-Zfwq1XVx.js} +0 -0
|
@@ -66,8 +66,8 @@ function extractReferences(markdownText = '', wikiTargetIndex) {
|
|
|
66
66
|
return { embeds, links };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function isWorkspaceFileEntry(entry =
|
|
70
|
-
return entry
|
|
69
|
+
function isWorkspaceFileEntry(entry = null) {
|
|
70
|
+
return Boolean(entry) && (entry.nodeType === 'file' || entry.type !== 'directory');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function listWorkspaceFilePaths(workspaceState = {}) {
|
|
@@ -114,6 +114,23 @@ function collectWikiTargetKeysForFilePath(filePath = '') {
|
|
|
114
114
|
return [...new Set(keys)];
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function createBacklinkEntry(sourcePath = '') {
|
|
118
|
+
return createLinkValue(sourcePath, {
|
|
119
|
+
display: basename(sourcePath),
|
|
120
|
+
exists: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function addRawTargetSourceContribution(snapshot, sourcePath, rawTargetKey) {
|
|
125
|
+
if (!rawTargetKey) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sources = snapshot.rawTargetSourcesByKey.get(rawTargetKey) ?? new Set();
|
|
130
|
+
sources.add(sourcePath);
|
|
131
|
+
snapshot.rawTargetSourcesByKey.set(rawTargetKey, sources);
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
export class BaseIndexSnapshotStore {
|
|
118
135
|
constructor({
|
|
119
136
|
vaultFileStore,
|
|
@@ -176,6 +193,11 @@ export class BaseIndexSnapshotStore {
|
|
|
176
193
|
.map((link) => link?.path)
|
|
177
194
|
.filter((targetPath) => targetPath && targetPath !== filePath),
|
|
178
195
|
),
|
|
196
|
+
rawTargetKeys: new Set(
|
|
197
|
+
references.links
|
|
198
|
+
.map((link) => normalizeWikiTargetKey(link?.rawTarget))
|
|
199
|
+
.filter(Boolean),
|
|
200
|
+
),
|
|
179
201
|
row: {
|
|
180
202
|
file: fileValue,
|
|
181
203
|
noteProperties,
|
|
@@ -185,25 +207,99 @@ export class BaseIndexSnapshotStore {
|
|
|
185
207
|
}
|
|
186
208
|
|
|
187
209
|
rebuildBacklinks(snapshot) {
|
|
188
|
-
|
|
210
|
+
snapshot.backlinkSourcesByTarget = new Map();
|
|
211
|
+
snapshot.backlinksByPath = new Map(snapshot.filePaths.map((filePath) => [filePath, []]));
|
|
189
212
|
|
|
190
213
|
snapshot.rowsByPath.forEach((row, filePath) => {
|
|
191
214
|
const forwardLinks = snapshot.forwardLinksByPath.get(filePath) ?? new Set();
|
|
192
215
|
forwardLinks.forEach((targetPath) => {
|
|
193
|
-
if (!
|
|
216
|
+
if (!snapshot.rowsByPath.has(targetPath) || targetPath === row.file.path) {
|
|
194
217
|
return;
|
|
195
218
|
}
|
|
196
219
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}));
|
|
220
|
+
const sources = snapshot.backlinkSourcesByTarget.get(targetPath) ?? new Set();
|
|
221
|
+
sources.add(filePath);
|
|
222
|
+
snapshot.backlinkSourcesByTarget.set(targetPath, sources);
|
|
201
223
|
});
|
|
202
224
|
});
|
|
203
225
|
|
|
204
|
-
snapshot.
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
this.refreshSerializedBacklinks(snapshot, snapshot.filePaths);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
removeSourceBacklinkContributions(snapshot, sourcePath, affectedTargetPaths = new Set()) {
|
|
230
|
+
const forwardLinks = snapshot.forwardLinksByPath.get(sourcePath) ?? new Set();
|
|
231
|
+
forwardLinks.forEach((targetPath) => {
|
|
232
|
+
const sources = snapshot.backlinkSourcesByTarget.get(targetPath);
|
|
233
|
+
if (!sources) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
sources.delete(sourcePath);
|
|
238
|
+
if (sources.size === 0) {
|
|
239
|
+
snapshot.backlinkSourcesByTarget.delete(targetPath);
|
|
240
|
+
}
|
|
241
|
+
affectedTargetPaths.add(targetPath);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
removeRawTargetSourceContributions(snapshot, sourcePath) {
|
|
246
|
+
const rawTargetKeys = snapshot.rawTargetKeysBySourcePath.get(sourcePath) ?? new Set();
|
|
247
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
248
|
+
const sources = snapshot.rawTargetSourcesByKey.get(rawTargetKey);
|
|
249
|
+
if (!sources) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
sources.delete(sourcePath);
|
|
254
|
+
if (sources.size === 0) {
|
|
255
|
+
snapshot.rawTargetSourcesByKey.delete(rawTargetKey);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
snapshot.rawTargetKeysBySourcePath.delete(sourcePath);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
addRawTargetSourceContributions(snapshot, sourcePath, rawTargetKeys = new Set()) {
|
|
262
|
+
snapshot.rawTargetKeysBySourcePath.set(sourcePath, rawTargetKeys);
|
|
263
|
+
rawTargetKeys.forEach((rawTargetKey) => {
|
|
264
|
+
addRawTargetSourceContribution(snapshot, sourcePath, rawTargetKey);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
addSourceBacklinkContributions(snapshot, sourcePath, affectedTargetPaths = new Set()) {
|
|
269
|
+
if (!snapshot.rowsByPath.has(sourcePath)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const forwardLinks = snapshot.forwardLinksByPath.get(sourcePath) ?? new Set();
|
|
274
|
+
forwardLinks.forEach((targetPath) => {
|
|
275
|
+
if (!snapshot.rowsByPath.has(targetPath) || targetPath === sourcePath) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sources = snapshot.backlinkSourcesByTarget.get(targetPath) ?? new Set();
|
|
280
|
+
sources.add(sourcePath);
|
|
281
|
+
snapshot.backlinkSourcesByTarget.set(targetPath, sources);
|
|
282
|
+
affectedTargetPaths.add(targetPath);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
refreshSerializedBacklinks(snapshot, targetPaths = []) {
|
|
287
|
+
Array.from(new Set(targetPaths)).forEach((targetPath) => {
|
|
288
|
+
if (!snapshot.rowsByPath.has(targetPath)) {
|
|
289
|
+
snapshot.backlinkSourcesByTarget.delete(targetPath);
|
|
290
|
+
snapshot.backlinksByPath.delete(targetPath);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const sources = snapshot.backlinkSourcesByTarget.get(targetPath) ?? new Set();
|
|
295
|
+
const backlinks = dedupeLinkValues(
|
|
296
|
+
Array.from(sources, (sourcePath) => createBacklinkEntry(sourcePath)),
|
|
297
|
+
);
|
|
298
|
+
snapshot.backlinksByPath.set(targetPath, backlinks);
|
|
299
|
+
const row = snapshot.rowsByPath.get(targetPath);
|
|
300
|
+
if (row?.file) {
|
|
301
|
+
row.file.backlinks = backlinks;
|
|
302
|
+
}
|
|
207
303
|
});
|
|
208
304
|
}
|
|
209
305
|
|
|
@@ -219,17 +315,28 @@ export class BaseIndexSnapshotStore {
|
|
|
219
315
|
|
|
220
316
|
const rowsByPath = new Map();
|
|
221
317
|
const forwardLinksByPath = new Map();
|
|
318
|
+
const rawTargetKeysBySourcePath = new Map();
|
|
319
|
+
const rawTargetSourcesByKey = new Map();
|
|
222
320
|
fileEntries.forEach((filePath) => {
|
|
223
321
|
const markdownContent = markdownContents.get(filePath) ?? null;
|
|
224
322
|
const rowRecord = this.createSnapshotRow(filePath, resolvedWorkspaceState, wikiTargetIndex, markdownContent);
|
|
225
323
|
rowsByPath.set(filePath, rowRecord.row);
|
|
226
324
|
forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
|
|
325
|
+
rawTargetKeysBySourcePath.set(filePath, rowRecord.rawTargetKeys);
|
|
326
|
+
rowRecord.rawTargetKeys.forEach((rawTargetKey) => {
|
|
327
|
+
addRawTargetSourceContribution({
|
|
328
|
+
rawTargetSourcesByKey,
|
|
329
|
+
}, filePath, rawTargetKey);
|
|
330
|
+
});
|
|
227
331
|
});
|
|
228
332
|
|
|
229
333
|
const snapshot = {
|
|
334
|
+
backlinkSourcesByTarget: new Map(),
|
|
230
335
|
backlinksByPath: new Map(),
|
|
231
336
|
filePaths: fileEntries,
|
|
232
337
|
forwardLinksByPath,
|
|
338
|
+
rawTargetKeysBySourcePath,
|
|
339
|
+
rawTargetSourcesByKey,
|
|
233
340
|
rowsByPath,
|
|
234
341
|
scannedAt: resolvedWorkspaceState.scannedAt,
|
|
235
342
|
wikiTargetIndex,
|
|
@@ -246,8 +353,10 @@ export class BaseIndexSnapshotStore {
|
|
|
246
353
|
}
|
|
247
354
|
|
|
248
355
|
removeSnapshotPath(snapshot, filePath) {
|
|
356
|
+
this.removeRawTargetSourceContributions(snapshot, filePath);
|
|
249
357
|
snapshot.rowsByPath.delete(filePath);
|
|
250
358
|
snapshot.forwardLinksByPath.delete(filePath);
|
|
359
|
+
snapshot.backlinkSourcesByTarget.delete(filePath);
|
|
251
360
|
snapshot.backlinksByPath.delete(filePath);
|
|
252
361
|
snapshot.filePaths = snapshot.filePaths.filter((candidatePath) => candidatePath !== filePath);
|
|
253
362
|
}
|
|
@@ -274,13 +383,12 @@ export class BaseIndexSnapshotStore {
|
|
|
274
383
|
}
|
|
275
384
|
|
|
276
385
|
const impactedPaths = new Set();
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
386
|
+
affectedTargetKeys.forEach((targetKey) => {
|
|
387
|
+
const sourcePaths = snapshot.rawTargetSourcesByKey.get(targetKey);
|
|
388
|
+
sourcePaths?.forEach((filePath) => {
|
|
280
389
|
impactedPaths.add(filePath);
|
|
281
|
-
}
|
|
390
|
+
});
|
|
282
391
|
});
|
|
283
|
-
|
|
284
392
|
return impactedPaths;
|
|
285
393
|
}
|
|
286
394
|
|
|
@@ -296,8 +404,10 @@ export class BaseIndexSnapshotStore {
|
|
|
296
404
|
? await this.vaultFileStore.readMarkdownFile(filePath)
|
|
297
405
|
: null;
|
|
298
406
|
const rowRecord = this.createSnapshotRow(filePath, workspaceState, snapshot.wikiTargetIndex, markdownContent);
|
|
407
|
+
this.removeRawTargetSourceContributions(snapshot, filePath);
|
|
299
408
|
snapshot.rowsByPath.set(filePath, rowRecord.row);
|
|
300
409
|
snapshot.forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
|
|
410
|
+
this.addRawTargetSourceContributions(snapshot, filePath, rowRecord.rawTargetKeys);
|
|
301
411
|
});
|
|
302
412
|
}
|
|
303
413
|
|
|
@@ -377,6 +487,8 @@ export class BaseIndexSnapshotStore {
|
|
|
377
487
|
}
|
|
378
488
|
|
|
379
489
|
const snapshot = this.indexSnapshot;
|
|
490
|
+
const affectedTargetPaths = new Set();
|
|
491
|
+
const affectedSourcePaths = new Set(markdownPathsToRefresh);
|
|
380
492
|
if (membershipChanged) {
|
|
381
493
|
const membershipAffectedPaths = [
|
|
382
494
|
...deletedFilePaths,
|
|
@@ -385,6 +497,33 @@ export class BaseIndexSnapshotStore {
|
|
|
385
497
|
...renamedFileEntries.flatMap((entry) => [entry.oldPath, entry.newPath]),
|
|
386
498
|
];
|
|
387
499
|
const impactedSourcePaths = this.collectImpactedSourcesForMembershipChanges(snapshot, membershipAffectedPaths);
|
|
500
|
+
impactedSourcePaths.forEach((pathValue) => affectedSourcePaths.add(pathValue));
|
|
501
|
+
[...deletedFilePaths, ...removedChangedFilePaths].forEach((pathValue) => {
|
|
502
|
+
if (isMarkdownFilePath(pathValue)) {
|
|
503
|
+
affectedSourcePaths.add(pathValue);
|
|
504
|
+
}
|
|
505
|
+
affectedTargetPaths.add(pathValue);
|
|
506
|
+
});
|
|
507
|
+
renamedFileEntries.forEach((entry) => {
|
|
508
|
+
if (isMarkdownFilePath(entry.oldPath)) {
|
|
509
|
+
affectedSourcePaths.add(entry.oldPath);
|
|
510
|
+
}
|
|
511
|
+
if (isMarkdownFilePath(entry.newPath)) {
|
|
512
|
+
affectedSourcePaths.add(entry.newPath);
|
|
513
|
+
}
|
|
514
|
+
affectedTargetPaths.add(entry.oldPath);
|
|
515
|
+
affectedTargetPaths.add(entry.newPath);
|
|
516
|
+
});
|
|
517
|
+
createdFilePaths.forEach((pathValue) => {
|
|
518
|
+
if (isMarkdownFilePath(pathValue)) {
|
|
519
|
+
affectedSourcePaths.add(pathValue);
|
|
520
|
+
}
|
|
521
|
+
affectedTargetPaths.add(pathValue);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
affectedSourcePaths.forEach((pathValue) => {
|
|
525
|
+
this.removeSourceBacklinkContributions(snapshot, pathValue, affectedTargetPaths);
|
|
526
|
+
});
|
|
388
527
|
|
|
389
528
|
[...deletedFilePaths, ...removedChangedFilePaths].forEach((pathValue) => {
|
|
390
529
|
this.removeSnapshotPath(snapshot, pathValue);
|
|
@@ -401,18 +540,26 @@ export class BaseIndexSnapshotStore {
|
|
|
401
540
|
|
|
402
541
|
snapshot.wikiTargetIndex = createWikiTargetIndex(snapshot.filePaths);
|
|
403
542
|
await this.refreshSnapshotRows(snapshot, nextState, [
|
|
404
|
-
...markdownPathsToRefresh,
|
|
405
543
|
...createdFilePaths,
|
|
406
544
|
...renamedFileEntries.map((entry) => entry.newPath),
|
|
407
|
-
...
|
|
545
|
+
...affectedSourcePaths,
|
|
408
546
|
]);
|
|
409
547
|
} else {
|
|
410
|
-
|
|
548
|
+
affectedSourcePaths.forEach((pathValue) => {
|
|
549
|
+
this.removeSourceBacklinkContributions(snapshot, pathValue, affectedTargetPaths);
|
|
550
|
+
});
|
|
551
|
+
await this.refreshSnapshotRows(snapshot, nextState, [...affectedSourcePaths]);
|
|
411
552
|
}
|
|
412
553
|
|
|
554
|
+
affectedSourcePaths.forEach((pathValue) => {
|
|
555
|
+
this.addSourceBacklinkContributions(snapshot, pathValue, affectedTargetPaths);
|
|
556
|
+
});
|
|
413
557
|
snapshot.workspaceState = nextState;
|
|
414
558
|
snapshot.scannedAt = nextState?.scannedAt ?? snapshot.scannedAt;
|
|
415
|
-
this.
|
|
559
|
+
this.refreshSerializedBacklinks(snapshot, [
|
|
560
|
+
...snapshot.filePaths.filter((filePath) => !snapshot.backlinksByPath.has(filePath)),
|
|
561
|
+
...affectedTargetPaths,
|
|
562
|
+
]);
|
|
416
563
|
return snapshot;
|
|
417
564
|
}
|
|
418
565
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { isImageAttachmentFilePath } from '../../../domain/file-kind.js';
|
|
2
|
+
import { parseDateValue, toDisplayText } from './base-expression-runtime.js';
|
|
3
|
+
|
|
4
|
+
const FILE_PROPERTY_DEFINITIONS = [
|
|
5
|
+
{ id: 'file.name', kind: 'file', label: 'file name', valueType: 'text' },
|
|
6
|
+
{ id: 'file.basename', kind: 'file', label: 'file base name', valueType: 'text' },
|
|
7
|
+
{ id: 'file.backlinks', kind: 'file', label: 'file backlinks', valueType: 'list' },
|
|
8
|
+
{ id: 'file.ctime', kind: 'file', label: 'created time', valueType: 'date' },
|
|
9
|
+
{ id: 'file.embeds', kind: 'file', label: 'file embeds', valueType: 'list' },
|
|
10
|
+
{ id: 'file.ext', kind: 'file', label: 'file extension', valueType: 'text' },
|
|
11
|
+
{ id: 'file.folder', kind: 'file', label: 'folder', valueType: 'text' },
|
|
12
|
+
{ id: 'file.links', kind: 'file', label: 'file links', valueType: 'list' },
|
|
13
|
+
{ id: 'file.mtime', kind: 'file', label: 'modified time', valueType: 'date' },
|
|
14
|
+
{ id: 'file.path', kind: 'file', label: 'file path', valueType: 'text' },
|
|
15
|
+
{ id: 'file.size', kind: 'file', label: 'file size', valueType: 'number' },
|
|
16
|
+
{ id: 'file.tags', kind: 'file', label: 'file tags', valueType: 'list' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function simplifyType(type) {
|
|
20
|
+
if (!type || type === 'unknown') {
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return type;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mergeTypes(left = 'unknown', right = 'unknown') {
|
|
28
|
+
if (left === 'unknown') {
|
|
29
|
+
return right;
|
|
30
|
+
}
|
|
31
|
+
if (right === 'unknown' || left === right) {
|
|
32
|
+
return left;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const scalarTypes = new Set(['boolean', 'date', 'number', 'text', 'image', 'link']);
|
|
36
|
+
if (scalarTypes.has(left) && scalarTypes.has(right)) {
|
|
37
|
+
if ((left === 'image' && right === 'text') || (left === 'text' && right === 'image')) {
|
|
38
|
+
return 'image';
|
|
39
|
+
}
|
|
40
|
+
if ((left === 'link' && right === 'text') || (left === 'text' && right === 'link')) {
|
|
41
|
+
return 'link';
|
|
42
|
+
}
|
|
43
|
+
return 'text';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (left === 'list' || right === 'list') {
|
|
47
|
+
return 'list';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return 'unknown';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function inferValueType(value) {
|
|
54
|
+
if (value == null || value === '') {
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return 'list';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (value instanceof Date) {
|
|
63
|
+
return 'date';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value?.__baseType === 'link') {
|
|
67
|
+
return 'link';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (value?.__baseType === 'file') {
|
|
71
|
+
return 'link';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof value === 'boolean') {
|
|
75
|
+
return 'boolean';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'number') {
|
|
79
|
+
return 'number';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof value === 'string') {
|
|
83
|
+
if (isImageAttachmentFilePath(value)) {
|
|
84
|
+
return 'image';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (parseDateValue(value)) {
|
|
88
|
+
return 'date';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return 'text';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return 'unknown';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createSortDirections(valueType = 'text') {
|
|
98
|
+
switch (valueType) {
|
|
99
|
+
case 'date':
|
|
100
|
+
return [
|
|
101
|
+
{ id: 'asc', label: 'Old to new' },
|
|
102
|
+
{ id: 'desc', label: 'New to old' },
|
|
103
|
+
];
|
|
104
|
+
case 'number':
|
|
105
|
+
return [
|
|
106
|
+
{ id: 'asc', label: '0 → 1' },
|
|
107
|
+
{ id: 'desc', label: '1 → 0' },
|
|
108
|
+
];
|
|
109
|
+
default:
|
|
110
|
+
return [
|
|
111
|
+
{ id: 'asc', label: 'A → Z' },
|
|
112
|
+
{ id: 'desc', label: 'Z → A' },
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createFilterOperators(valueType = 'text') {
|
|
118
|
+
if (valueType === 'boolean') {
|
|
119
|
+
return ['is', 'is not', 'is empty', 'is not empty'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (valueType === 'number' || valueType === 'date') {
|
|
123
|
+
return ['is', 'is not', '>', '>=', '<', '<=', 'is empty', 'is not empty'];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (valueType === 'list') {
|
|
127
|
+
return ['contains', 'does not contain', 'is empty', 'is not empty'];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return ['is', 'is not', 'contains', 'does not contain', 'starts with', 'ends with', 'is empty', 'is not empty'];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createPropertyCatalogSnapshot(snapshot) {
|
|
134
|
+
const noteProperties = new Map();
|
|
135
|
+
|
|
136
|
+
snapshot?.rowsByPath?.forEach((row) => {
|
|
137
|
+
Object.entries(row?.noteProperties ?? {}).forEach(([propertyName, value]) => {
|
|
138
|
+
const propertyId = `note.${propertyName}`;
|
|
139
|
+
const current = noteProperties.get(propertyId) ?? {
|
|
140
|
+
id: propertyId,
|
|
141
|
+
kind: 'note',
|
|
142
|
+
label: propertyName,
|
|
143
|
+
valueType: 'unknown',
|
|
144
|
+
};
|
|
145
|
+
current.valueType = mergeTypes(current.valueType, inferValueType(value));
|
|
146
|
+
noteProperties.set(propertyId, current);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
fileProperties: FILE_PROPERTY_DEFINITIONS.map((entry) => ({ ...entry })),
|
|
152
|
+
noteProperties: Array.from(noteProperties.values()).sort((left, right) => left.label.localeCompare(right.label)),
|
|
153
|
+
scannedAt: snapshot?.scannedAt ?? '',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function buildAvailableProperties({
|
|
158
|
+
activeView,
|
|
159
|
+
columns = [],
|
|
160
|
+
definition,
|
|
161
|
+
formulaValueTypes = {},
|
|
162
|
+
propertyCatalog,
|
|
163
|
+
}) {
|
|
164
|
+
const visibleIds = new Set(
|
|
165
|
+
Array.isArray(activeView?.order) && activeView.order.length > 0
|
|
166
|
+
? activeView.order
|
|
167
|
+
: columns.map((column) => column.id),
|
|
168
|
+
);
|
|
169
|
+
const baseProperties = [
|
|
170
|
+
...(propertyCatalog?.fileProperties ?? []),
|
|
171
|
+
...(propertyCatalog?.noteProperties ?? []),
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const formulaProperties = Object.values(definition?.formulas ?? {}).map((formula) => ({
|
|
175
|
+
id: formula.id,
|
|
176
|
+
kind: 'formula',
|
|
177
|
+
label: definition?.properties?.[formula.id]?.displayName ?? formula.name,
|
|
178
|
+
valueType: simplifyType(formulaValueTypes[formula.id] ?? 'unknown'),
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
return [...baseProperties, ...formulaProperties]
|
|
182
|
+
.map((property) => {
|
|
183
|
+
const valueType = simplifyType(property.valueType);
|
|
184
|
+
return {
|
|
185
|
+
filterOperators: createFilterOperators(valueType),
|
|
186
|
+
groupable: true,
|
|
187
|
+
id: property.id,
|
|
188
|
+
kind: property.kind,
|
|
189
|
+
label: property.label,
|
|
190
|
+
sortable: true,
|
|
191
|
+
sortDirections: createSortDirections(valueType),
|
|
192
|
+
valueType,
|
|
193
|
+
visible: visibleIds.has(property.id),
|
|
194
|
+
};
|
|
195
|
+
})
|
|
196
|
+
.sort((left, right) => left.label.localeCompare(right.label));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function flattenValues(value) {
|
|
200
|
+
if (Array.isArray(value)) {
|
|
201
|
+
return value.flatMap((entry) => flattenValues(entry));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (value == null || value === '') {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return [value];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function collectDistinctPropertyValues(rows = [], propertyId = '', query = '') {
|
|
212
|
+
const counts = new Map();
|
|
213
|
+
const normalizedQuery = String(query ?? '').trim().toLowerCase();
|
|
214
|
+
|
|
215
|
+
rows.forEach((row) => {
|
|
216
|
+
flattenValues(row?.rawCells?.[propertyId]).forEach((value) => {
|
|
217
|
+
const text = toDisplayText(value);
|
|
218
|
+
if (!text) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (normalizedQuery && !text.toLowerCase().includes(normalizedQuery)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const entry = counts.get(text) ?? {
|
|
227
|
+
count: 0,
|
|
228
|
+
text,
|
|
229
|
+
value,
|
|
230
|
+
};
|
|
231
|
+
entry.count += 1;
|
|
232
|
+
counts.set(text, entry);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return Array.from(counts.values())
|
|
237
|
+
.sort((left, right) => (
|
|
238
|
+
right.count - left.count
|
|
239
|
+
|| left.text.localeCompare(right.text)
|
|
240
|
+
))
|
|
241
|
+
.slice(0, 100);
|
|
242
|
+
}
|
|
@@ -123,10 +123,10 @@ export function rowMatchesSearch(row, columns, query) {
|
|
|
123
123
|
return true;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const haystack =
|
|
127
|
-
row.
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
const haystack = columns
|
|
127
|
+
.map((column) => toDisplayText(row.rawCells[column.id]))
|
|
128
|
+
.join('\n')
|
|
129
|
+
.toLowerCase();
|
|
130
130
|
return haystack.includes(normalizedQuery);
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -134,18 +134,20 @@ export function buildQueryResultPayload({
|
|
|
134
134
|
activeView,
|
|
135
135
|
columns,
|
|
136
136
|
definition,
|
|
137
|
+
includeCsv = false,
|
|
137
138
|
rows,
|
|
138
139
|
snapshot,
|
|
139
140
|
thisFile,
|
|
140
141
|
}) {
|
|
141
142
|
const groupsByKey = new Map();
|
|
142
|
-
if (activeView.groupBy) {
|
|
143
|
+
if (activeView.groupBy?.property) {
|
|
143
144
|
rows.forEach((row) => {
|
|
144
|
-
const groupValue = row.rawCells[activeView.groupBy];
|
|
145
|
+
const groupValue = row.rawCells[activeView.groupBy.property];
|
|
145
146
|
const groupKey = toDisplayText(groupValue) || 'Empty';
|
|
146
147
|
const entry = groupsByKey.get(groupKey) ?? {
|
|
147
148
|
key: groupKey,
|
|
148
149
|
label: groupKey,
|
|
150
|
+
rawValue: groupValue,
|
|
149
151
|
rows: [],
|
|
150
152
|
value: serializeBaseValue(groupValue, {
|
|
151
153
|
rowFilePath: row.file.path,
|
|
@@ -183,7 +185,15 @@ export function buildQueryResultPayload({
|
|
|
183
185
|
}));
|
|
184
186
|
const rowsByPath = new Map(serializedRows.map((row) => [row.path, row]));
|
|
185
187
|
|
|
186
|
-
|
|
188
|
+
let groups = Array.from(groupsByKey.values());
|
|
189
|
+
if (activeView.groupBy?.explicitDirection) {
|
|
190
|
+
groups = groups.sort((left, right) => {
|
|
191
|
+
const delta = compareValues(left.rawValue, right.rawValue);
|
|
192
|
+
return activeView.groupBy.direction === 'desc' ? -delta : delta;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
groups = groups.map((group) => ({
|
|
187
197
|
key: group.key,
|
|
188
198
|
label: group.label,
|
|
189
199
|
summaries: createSummaryPayload(columns, group.rows, activeView.summaries, definition, snapshot, thisFile),
|
|
@@ -192,7 +202,7 @@ export function buildQueryResultPayload({
|
|
|
192
202
|
}));
|
|
193
203
|
|
|
194
204
|
return {
|
|
195
|
-
csv: createCsv(rows, columns),
|
|
205
|
+
...(includeCsv ? { csv: createCsv(rows, columns) } : {}),
|
|
196
206
|
groups,
|
|
197
207
|
rows: serializedRows,
|
|
198
208
|
summaries: createSummaryPayload(columns, rows, activeView.summaries, definition, snapshot, thisFile),
|