collabmd 0.1.31 → 0.1.33
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-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
- package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-DeowJOSp.js +1368 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
- 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 +8 -5
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +182 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +6 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +115 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +1413 -34
- package/src/client/presentation/comments-panel.js +50 -5
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/components/scrollbars.css +5 -0
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +559 -39
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/backlink-index.js +177 -5
- package/src/server/domain/bases/base-definition.js +382 -0
- package/src/server/domain/bases/base-expression-runtime.js +1196 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +215 -0
- package/src/server/domain/bases/base-query-service.js +406 -1992
- package/src/server/domain/bases/base-transform.js +116 -0
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isImageAttachmentFilePath } from '../../domain/file-kind.js';
|
|
2
2
|
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
3
3
|
import { escapeHtml } from '../domain/vault-utils.js';
|
|
4
|
+
import { buttonClassNames } from './components/ui/button.js';
|
|
5
|
+
import { inputClassNames } from './components/ui/input.js';
|
|
6
|
+
import { segmentedButtonClassNames, segmentedControlClassNames } from './components/ui/segmented-control.js';
|
|
4
7
|
|
|
5
8
|
function createShellKey() {
|
|
6
9
|
return `base-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -14,6 +17,18 @@ function looksLikeExternalImageUrl(value = '') {
|
|
|
14
17
|
return /^https?:\/\/.+\.(?:png|jpe?g|webp|gif|svg)(?:[?#].*)?$/iu.test(String(value ?? '').trim());
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createEmptyFilterGroup(conjunction = 'and') {
|
|
25
|
+
return {
|
|
26
|
+
children: [],
|
|
27
|
+
conjunction,
|
|
28
|
+
type: 'group',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
function getCardImageCell(view, columns, row) {
|
|
18
33
|
const preferred = view.image
|
|
19
34
|
|| columns.find((column) => ['cover', 'image'].includes(String(column.id ?? '').toLowerCase()))?.id
|
|
@@ -73,11 +88,11 @@ function renderValue(cell = {}) {
|
|
|
73
88
|
return `<a class="bases-link" href="${escapeHtml(cell.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(formatCellText(cell))}</a>`;
|
|
74
89
|
}
|
|
75
90
|
if (cell.path) {
|
|
76
|
-
return `<button type="button" class="bases-link bases-link-button" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
91
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact', pill: true, extra: ['bases-link', 'bases-link-button'] }))}" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
if (type === 'file' && cell.path) {
|
|
80
|
-
return `<button type="button" class="bases-link bases-link-button" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
95
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact', pill: true, extra: ['bases-link', 'bases-link-button'] }))}" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
const text = formatCellText(cell);
|
|
@@ -92,7 +107,7 @@ function renderRowValue(column, row) {
|
|
|
92
107
|
&& cell.type !== 'empty'
|
|
93
108
|
&& (column.id === 'file.name' || column.id === 'file.basename')
|
|
94
109
|
) {
|
|
95
|
-
return `<button type="button" class="bases-link bases-link-button" data-base-open-file="${escapeHtml(row.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
110
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact', pill: true, extra: ['bases-link', 'bases-link-button'] }))}" data-base-open-file="${escapeHtml(row.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
return renderValue(cell);
|
|
@@ -171,25 +186,876 @@ function renderViewBody(result) {
|
|
|
171
186
|
|
|
172
187
|
function renderViewTabs(result) {
|
|
173
188
|
return (result.views ?? []).map((view) => (
|
|
174
|
-
`<button type="button" class="
|
|
189
|
+
`<button type="button" class="${escapeHtml(segmentedButtonClassNames({ active: view.id === result.view.id, extra: 'bases-view-tab' }))}" data-base-view="${escapeHtml(view.id)}">${escapeHtml(view.name)}</button>`
|
|
175
190
|
)).join('');
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
function
|
|
193
|
+
function getMeta(result) {
|
|
194
|
+
return result?.meta ?? {};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getAvailableProperties(result) {
|
|
198
|
+
return getMeta(result).availableProperties ?? [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getVisiblePropertyIds(result) {
|
|
202
|
+
const explicitOrder = getMeta(result).activeViewConfig?.order;
|
|
203
|
+
if (Array.isArray(explicitOrder) && explicitOrder.length > 0) {
|
|
204
|
+
return [...explicitOrder];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Array.isArray(result?.columns)
|
|
208
|
+
? result.columns.map((column) => column.id).filter(Boolean)
|
|
209
|
+
: [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function findPropertyMeta(result, propertyId = '') {
|
|
213
|
+
return getAvailableProperties(result).find((entry) => entry.id === propertyId) ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getPropertyFilterOperators(result, propertyId = '') {
|
|
217
|
+
return findPropertyMeta(result, propertyId)?.filterOperators
|
|
218
|
+
?? ['is', 'is not', 'contains', 'does not contain', 'is empty', 'is not empty'];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getEditableViewConfig(result) {
|
|
222
|
+
const config = getMeta(result).activeViewConfig ?? {};
|
|
223
|
+
return {
|
|
224
|
+
filters: config.filters ?? null,
|
|
225
|
+
groupBy: config.groupBy ?? null,
|
|
226
|
+
order: Array.isArray(config.order) ? [...config.order] : [],
|
|
227
|
+
sort: Array.isArray(config.sort) ? config.sort.map((entry) => ({ ...entry })) : [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isStandaloneBaseEntry(entry) {
|
|
232
|
+
return Boolean(
|
|
233
|
+
entry?.payload?.path
|
|
234
|
+
&& entry?.payload?.sourcePath
|
|
235
|
+
&& entry.payload.path === entry.payload.sourcePath,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildToolbarButton(label, panel, entry, result) {
|
|
240
|
+
const active = entry.ui?.openPanel === panel;
|
|
241
|
+
const editable = Boolean(getMeta(result).editable);
|
|
242
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: active ? 'primary' : 'secondary', pill: true, extra: ['bases-toolbar-btn', active ? 'is-active' : ''] }))}" data-base-panel="${escapeHtml(panel)}"${editable ? '' : ' disabled'}>${escapeHtml(label)}</button>`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function pruneFilterGroupByProperty(group, propertyId = '') {
|
|
246
|
+
if (!group || group.type !== 'group' || !propertyId) {
|
|
247
|
+
return group;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const children = (group.children ?? []).flatMap((child) => {
|
|
251
|
+
if (child?.type === 'group') {
|
|
252
|
+
const nextGroup = pruneFilterGroupByProperty(child, propertyId);
|
|
253
|
+
return nextGroup ? [nextGroup] : [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return child?.propertyId === propertyId ? [] : [child];
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
...group,
|
|
261
|
+
children,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createPropertyValuesCacheKey(entry, propertyId = '', result = entry?.result) {
|
|
266
|
+
const rawFilters = getMeta(result).activeViewConfig?.filters ?? null;
|
|
267
|
+
const parsedFilters = parseFilterNode(rawFilters);
|
|
268
|
+
const cacheFilters = propertyId && parsedFilters
|
|
269
|
+
? compileFilterGroup(pruneFilterGroupByProperty(parsedFilters, propertyId), result)
|
|
270
|
+
: rawFilters;
|
|
271
|
+
return JSON.stringify({
|
|
272
|
+
filters: cacheFilters,
|
|
273
|
+
path: entry?.payload?.path ?? '',
|
|
274
|
+
source: entry?.payload?.source ?? null,
|
|
275
|
+
sourcePath: entry?.payload?.sourcePath ?? '',
|
|
276
|
+
view: result?.view?.id ?? entry?.payload?.view ?? '',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getCachedPropertyValueOptions(entry, propertyId = '', result = entry?.result) {
|
|
281
|
+
const cachedEntry = entry?.propertyValueOptions?.get(propertyId) ?? null;
|
|
282
|
+
if (!cachedEntry) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return cachedEntry.cacheKey === createPropertyValuesCacheKey(entry, propertyId, result)
|
|
287
|
+
? (cachedEntry.values ?? [])
|
|
288
|
+
: [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderActionButtons(result, entry) {
|
|
292
|
+
return [
|
|
293
|
+
buildToolbarButton('Sort', 'sort', entry, result),
|
|
294
|
+
buildToolbarButton('Filter', 'filter', entry, result),
|
|
295
|
+
buildToolbarButton('Properties', 'properties', entry, result),
|
|
296
|
+
].join('');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function createSelectOptions(result, {
|
|
300
|
+
allowBlank = false,
|
|
301
|
+
selected = '',
|
|
302
|
+
} = {}) {
|
|
303
|
+
const options = [];
|
|
304
|
+
if (allowBlank) {
|
|
305
|
+
options.push(`<option value=""${selected ? '' : ' selected'}>Property</option>`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getAvailableProperties(result).forEach((property) => {
|
|
309
|
+
options.push(`<option value="${escapeHtml(property.id)}"${property.id === selected ? ' selected' : ''}>${escapeHtml(property.label)}</option>`);
|
|
310
|
+
});
|
|
311
|
+
return options.join('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderSortPanel(result) {
|
|
315
|
+
const viewConfig = getEditableViewConfig(result);
|
|
316
|
+
const groupDirections = findPropertyMeta(result, viewConfig.groupBy?.property)?.sortDirections
|
|
317
|
+
?? [
|
|
318
|
+
{ id: 'asc', label: 'A → Z' },
|
|
319
|
+
{ id: 'desc', label: 'Z → A' },
|
|
320
|
+
];
|
|
321
|
+
const sorts = viewConfig.sort ?? [];
|
|
322
|
+
|
|
179
323
|
return `
|
|
180
|
-
<section class="bases-
|
|
324
|
+
<section class="bases-panel-card">
|
|
325
|
+
<div class="bases-panel-section">
|
|
326
|
+
<div class="bases-panel-title">Group by</div>
|
|
327
|
+
<div class="bases-sort-row">
|
|
328
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-sort-property' }))}" data-base-group-by-property>
|
|
329
|
+
${createSelectOptions(result, { allowBlank: true, selected: viewConfig.groupBy?.property ?? '' })}
|
|
330
|
+
</select>
|
|
331
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-sort-direction' }))}" data-base-group-by-direction${viewConfig.groupBy?.property ? '' : ' disabled'}>
|
|
332
|
+
${groupDirections.map((direction) => `<option value="${escapeHtml(direction.id)}"${direction.id === (viewConfig.groupBy?.direction ?? 'asc') ? ' selected' : ''}>${escapeHtml(direction.label)}</option>`).join('')}
|
|
333
|
+
</select>
|
|
334
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-clear-group-by${viewConfig.groupBy?.property ? '' : ' disabled'}>Clear</button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="bases-panel-section">
|
|
338
|
+
<div class="bases-panel-title">Sort by</div>
|
|
339
|
+
<div class="bases-sort-list">
|
|
340
|
+
${sorts.map((sortConfig, index) => {
|
|
341
|
+
const propertyMeta = findPropertyMeta(result, sortConfig.property);
|
|
342
|
+
const directions = propertyMeta?.sortDirections ?? [
|
|
343
|
+
{ id: 'asc', label: 'A → Z' },
|
|
344
|
+
{ id: 'desc', label: 'Z → A' },
|
|
345
|
+
];
|
|
346
|
+
return `
|
|
347
|
+
<div class="bases-sort-row">
|
|
348
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-sort-property' }))}" data-base-sort-property="${index}">
|
|
349
|
+
${createSelectOptions(result, { selected: sortConfig.property })}
|
|
350
|
+
</select>
|
|
351
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-sort-direction' }))}" data-base-sort-direction="${index}">
|
|
352
|
+
${directions.map((direction) => `<option value="${escapeHtml(direction.id)}"${direction.id === sortConfig.direction ? ' selected' : ''}>${escapeHtml(direction.label)}</option>`).join('')}
|
|
353
|
+
</select>
|
|
354
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-sort-move="${index}:up"${index === 0 ? ' disabled' : ''}>↑</button>
|
|
355
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-sort-move="${index}:down"${index === sorts.length - 1 ? ' disabled' : ''}>↓</button>
|
|
356
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-sort-delete="${index}">Delete</button>
|
|
357
|
+
</div>
|
|
358
|
+
`;
|
|
359
|
+
}).join('')}
|
|
360
|
+
</div>
|
|
361
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-sort-add>Add sort</button>
|
|
362
|
+
</div>
|
|
363
|
+
</section>
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function encodePath(path = []) {
|
|
368
|
+
return path.join('.');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function decodePath(value = '') {
|
|
372
|
+
return String(value ?? '')
|
|
373
|
+
.split('.')
|
|
374
|
+
.filter((segment) => segment !== '')
|
|
375
|
+
.map((segment) => Number.parseInt(segment, 10))
|
|
376
|
+
.filter(Number.isFinite);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function escapeExpressionString(value = '') {
|
|
380
|
+
return JSON.stringify(String(value ?? ''));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function formatExpressionLiteral(value = '', valueType = 'text', operator = 'is') {
|
|
384
|
+
const normalized = String(value ?? '').trim();
|
|
385
|
+
if (!normalized && operator !== 'is' && operator !== 'is not') {
|
|
386
|
+
return '""';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (valueType === 'boolean') {
|
|
390
|
+
return normalized === 'false' ? 'false' : 'true';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (valueType === 'number') {
|
|
394
|
+
return Number.isFinite(Number(normalized))
|
|
395
|
+
? String(Number(normalized))
|
|
396
|
+
: escapeExpressionString(normalized);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (valueType === 'date') {
|
|
400
|
+
return escapeExpressionString(normalized);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return escapeExpressionString(normalized);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function compileFilterRule(rule, propertyMeta = null) {
|
|
407
|
+
if (!rule?.propertyId || !rule?.operator) {
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const propertyId = rule.propertyId;
|
|
412
|
+
const valueType = propertyMeta?.valueType ?? 'text';
|
|
413
|
+
|
|
414
|
+
switch (rule.operator) {
|
|
415
|
+
case 'contains':
|
|
416
|
+
return `${propertyId}.contains(${formatExpressionLiteral(rule.value, valueType, rule.operator)})`;
|
|
417
|
+
case 'does not contain':
|
|
418
|
+
return `!${propertyId}.contains(${formatExpressionLiteral(rule.value, valueType, rule.operator)})`;
|
|
419
|
+
case 'starts with':
|
|
420
|
+
return `${propertyId}.startsWith(${formatExpressionLiteral(rule.value, valueType, rule.operator)})`;
|
|
421
|
+
case 'ends with':
|
|
422
|
+
return `${propertyId}.endsWith(${formatExpressionLiteral(rule.value, valueType, rule.operator)})`;
|
|
423
|
+
case 'is':
|
|
424
|
+
return `${propertyId} == ${formatExpressionLiteral(rule.value, valueType, rule.operator)}`;
|
|
425
|
+
case 'is not':
|
|
426
|
+
return `${propertyId} != ${formatExpressionLiteral(rule.value, valueType, rule.operator)}`;
|
|
427
|
+
case '>':
|
|
428
|
+
case '>=':
|
|
429
|
+
case '<':
|
|
430
|
+
case '<=':
|
|
431
|
+
return `${propertyId} ${rule.operator} ${formatExpressionLiteral(rule.value, valueType, rule.operator)}`;
|
|
432
|
+
case 'is empty':
|
|
433
|
+
return `${propertyId}.isEmpty()`;
|
|
434
|
+
case 'is not empty':
|
|
435
|
+
return `!${propertyId}.isEmpty()`;
|
|
436
|
+
default:
|
|
437
|
+
return '';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function compileFilterGroup(group, result) {
|
|
442
|
+
if (!group || group.type !== 'group') {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const compiledChildren = (group.children ?? [])
|
|
447
|
+
.map((child) => {
|
|
448
|
+
if (child?.type === 'group') {
|
|
449
|
+
return compileFilterGroup(child, result);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const propertyMeta = findPropertyMeta(result, child?.propertyId);
|
|
453
|
+
return compileFilterRule(child, propertyMeta);
|
|
454
|
+
})
|
|
455
|
+
.filter(Boolean);
|
|
456
|
+
|
|
457
|
+
if (compiledChildren.length === 0) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (group.conjunction === 'not') {
|
|
462
|
+
return {
|
|
463
|
+
not: compiledChildren,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
[group.conjunction]: compiledChildren,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function deserializeExpressionValue(value = '') {
|
|
473
|
+
const normalized = String(value ?? '').trim();
|
|
474
|
+
if (!normalized) {
|
|
475
|
+
return '';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (normalized === 'true' || normalized === 'false') {
|
|
479
|
+
return normalized;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (normalized === 'null') {
|
|
483
|
+
return '';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (/^-?\d+(?:\.\d+)?$/u.test(normalized)) {
|
|
487
|
+
return normalized;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if ((normalized.startsWith('"') && normalized.endsWith('"')) || (normalized.startsWith('\'') && normalized.endsWith('\''))) {
|
|
491
|
+
return normalized.slice(1, -1).replace(/\\(["'])/gu, '$1');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return normalized;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function parseFilterRule(expression = '') {
|
|
498
|
+
const text = String(expression ?? '').trim();
|
|
499
|
+
const patterns = [
|
|
500
|
+
[/^!([A-Za-z0-9_.]+)\.isEmpty\(\)$/u, (_, propertyId) => ({ operator: 'is not empty', propertyId, type: 'rule', value: '' })],
|
|
501
|
+
[/^([A-Za-z0-9_.]+)\.isEmpty\(\)$/u, (_, propertyId) => ({ operator: 'is empty', propertyId, type: 'rule', value: '' })],
|
|
502
|
+
[/^!([A-Za-z0-9_.]+)\.contains\((.+)\)$/u, (_, propertyId, value) => ({ operator: 'does not contain', propertyId, type: 'rule', value: deserializeExpressionValue(value) })],
|
|
503
|
+
[/^([A-Za-z0-9_.]+)\.contains\((.+)\)$/u, (_, propertyId, value) => ({ operator: 'contains', propertyId, type: 'rule', value: deserializeExpressionValue(value) })],
|
|
504
|
+
[/^([A-Za-z0-9_.]+)\.startsWith\((.+)\)$/u, (_, propertyId, value) => ({ operator: 'starts with', propertyId, type: 'rule', value: deserializeExpressionValue(value) })],
|
|
505
|
+
[/^([A-Za-z0-9_.]+)\.endsWith\((.+)\)$/u, (_, propertyId, value) => ({ operator: 'ends with', propertyId, type: 'rule', value: deserializeExpressionValue(value) })],
|
|
506
|
+
[/^([A-Za-z0-9_.]+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/u, (_, propertyId, operator, value) => ({
|
|
507
|
+
operator: {
|
|
508
|
+
'!=': 'is not',
|
|
509
|
+
'==': 'is',
|
|
510
|
+
'<': '<',
|
|
511
|
+
'<=': '<=',
|
|
512
|
+
'>': '>',
|
|
513
|
+
'>=': '>=',
|
|
514
|
+
}[operator] ?? operator,
|
|
515
|
+
propertyId,
|
|
516
|
+
type: 'rule',
|
|
517
|
+
value: deserializeExpressionValue(value),
|
|
518
|
+
})],
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
for (const [pattern, build] of patterns) {
|
|
522
|
+
const match = text.match(pattern);
|
|
523
|
+
if (match) {
|
|
524
|
+
return build(...match);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function parseFilterNode(filterNode) {
|
|
532
|
+
if (!filterNode) {
|
|
533
|
+
return createEmptyFilterGroup('and');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (typeof filterNode === 'string') {
|
|
537
|
+
const rule = parseFilterRule(filterNode);
|
|
538
|
+
return rule
|
|
539
|
+
? {
|
|
540
|
+
children: [rule],
|
|
541
|
+
conjunction: 'and',
|
|
542
|
+
type: 'group',
|
|
543
|
+
}
|
|
544
|
+
: null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!isPlainObject(filterNode)) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (Array.isArray(filterNode.and)) {
|
|
552
|
+
const children = filterNode.and.map((child) => parseFilterNode(child)).flatMap((child) => (
|
|
553
|
+
child?.type === 'group' && child.conjunction === 'and' && child.children?.length === 1 && child.children[0]?.type === 'rule'
|
|
554
|
+
? child.children
|
|
555
|
+
: [child]
|
|
556
|
+
));
|
|
557
|
+
return children.every(Boolean)
|
|
558
|
+
? { children, conjunction: 'and', type: 'group' }
|
|
559
|
+
: null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (Array.isArray(filterNode.or)) {
|
|
563
|
+
const children = filterNode.or.map((child) => parseFilterNode(child)).flatMap((child) => (
|
|
564
|
+
child?.type === 'group' && child.conjunction === 'and' && child.children?.length === 1 && child.children[0]?.type === 'rule'
|
|
565
|
+
? child.children
|
|
566
|
+
: [child]
|
|
567
|
+
));
|
|
568
|
+
return children.every(Boolean)
|
|
569
|
+
? { children, conjunction: 'or', type: 'group' }
|
|
570
|
+
: null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (filterNode.not != null) {
|
|
574
|
+
const notChildren = Array.isArray(filterNode.not)
|
|
575
|
+
? filterNode.not
|
|
576
|
+
: (() => {
|
|
577
|
+
const parsed = parseFilterNode(filterNode.not);
|
|
578
|
+
if (!parsed) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
return parsed.type === 'group' && parsed.conjunction === 'or'
|
|
582
|
+
? (parsed.children ?? [])
|
|
583
|
+
: [parsed];
|
|
584
|
+
})();
|
|
585
|
+
const children = notChildren.flatMap((child) => {
|
|
586
|
+
const parsedChild = child?.type ? child : parseFilterNode(child);
|
|
587
|
+
if (!parsedChild) {
|
|
588
|
+
return [null];
|
|
589
|
+
}
|
|
590
|
+
return parsedChild.type === 'group' && parsedChild.conjunction === 'and' && parsedChild.children?.length === 1 && parsedChild.children[0]?.type === 'rule'
|
|
591
|
+
? parsedChild.children
|
|
592
|
+
: [parsedChild];
|
|
593
|
+
});
|
|
594
|
+
return children.every(Boolean)
|
|
595
|
+
? { children, conjunction: 'not', type: 'group' }
|
|
596
|
+
: null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function serializeRawFilterText(filters) {
|
|
603
|
+
if (!filters) {
|
|
604
|
+
return '';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return typeof filters === 'string'
|
|
608
|
+
? filters
|
|
609
|
+
: JSON.stringify(filters, null, 2);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function parseRawFilterText(text = '') {
|
|
613
|
+
const normalized = String(text ?? '').trim();
|
|
614
|
+
if (!normalized) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (normalized.startsWith('{') || normalized.startsWith('[')) {
|
|
619
|
+
return JSON.parse(normalized);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return normalized;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function getNodeAtPath(group, path = []) {
|
|
626
|
+
let current = group;
|
|
627
|
+
for (const index of path) {
|
|
628
|
+
current = current?.children?.[index];
|
|
629
|
+
}
|
|
630
|
+
return current ?? null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function updateNodeAtPath(group, path = [], updater) {
|
|
634
|
+
if (path.length === 0) {
|
|
635
|
+
return updater(group);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const [head, ...rest] = path;
|
|
639
|
+
return {
|
|
640
|
+
...group,
|
|
641
|
+
children: (group.children ?? []).map((child, index) => (
|
|
642
|
+
index === head
|
|
643
|
+
? updateNodeAtPath(child, rest, updater)
|
|
644
|
+
: child
|
|
645
|
+
)),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function removeNodeAtPath(group, path = []) {
|
|
650
|
+
if (path.length === 0) {
|
|
651
|
+
return group;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const parentPath = path.slice(0, -1);
|
|
655
|
+
const removeIndex = path[path.length - 1];
|
|
656
|
+
return updateNodeAtPath(group, parentPath, (node) => ({
|
|
657
|
+
...node,
|
|
658
|
+
children: (node.children ?? []).filter((_, index) => index !== removeIndex),
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function addNodeAtPath(group, path = [], node) {
|
|
663
|
+
return updateNodeAtPath(group, path, (target) => ({
|
|
664
|
+
...target,
|
|
665
|
+
children: [...(target.children ?? []), node],
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function getPropertyOptionsMarkup(result, selected = '') {
|
|
670
|
+
return createSelectOptions(result, { allowBlank: true, selected });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function getPropertyValueOptionText(option = {}) {
|
|
674
|
+
return String(option?.text ?? option?.value ?? '').trim();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function getBuilderFilterState(entry, result) {
|
|
678
|
+
return entry.ui.builderFilter
|
|
679
|
+
?? parseFilterNode(getEditableViewConfig(result).filters)
|
|
680
|
+
?? createEmptyFilterGroup('and');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function buildFilterValuePresentation(result, rule, path, entry) {
|
|
684
|
+
const encodedPath = encodePath(path);
|
|
685
|
+
const operators = getPropertyFilterOperators(result, rule.propertyId);
|
|
686
|
+
const suggestionQuery = String(rule.value ?? '').trim().toLowerCase();
|
|
687
|
+
const propertyValueOptions = getCachedPropertyValueOptions(entry, rule.propertyId, result)
|
|
688
|
+
.map((option) => ({
|
|
689
|
+
count: option?.count,
|
|
690
|
+
text: getPropertyValueOptionText(option),
|
|
691
|
+
}))
|
|
692
|
+
.filter((option, index, items) => (
|
|
693
|
+
option.text
|
|
694
|
+
&& items.findIndex((candidate) => candidate.text === option.text) === index
|
|
695
|
+
));
|
|
696
|
+
const hasExactSuggestionMatch = propertyValueOptions.some((option) => (
|
|
697
|
+
option.text.toLowerCase() === suggestionQuery
|
|
698
|
+
));
|
|
699
|
+
const matchingValueSuggestions = propertyValueOptions.filter((option) => (
|
|
700
|
+
!suggestionQuery || option.text.toLowerCase().includes(suggestionQuery)
|
|
701
|
+
));
|
|
702
|
+
const valueSuggestions = !suggestionQuery || hasExactSuggestionMatch
|
|
703
|
+
? propertyValueOptions
|
|
704
|
+
: matchingValueSuggestions.length > 0
|
|
705
|
+
? matchingValueSuggestions
|
|
706
|
+
: propertyValueOptions;
|
|
707
|
+
const suggestionsEnabled = valueSuggestions.length > 0 && !['is empty', 'is not empty'].includes(rule.operator);
|
|
708
|
+
const suggestionListId = `bases-filter-suggestions-${escapeHtml(entry.key)}-${escapeHtml(encodedPath.replace(/\./g, '-'))}`;
|
|
709
|
+
const valueInputClasses = inputClassNames({
|
|
710
|
+
extra: suggestionsEnabled
|
|
711
|
+
? 'bases-filter-value bases-filter-value--suggestions'
|
|
712
|
+
: 'bases-filter-value',
|
|
713
|
+
});
|
|
714
|
+
return {
|
|
715
|
+
encodedPath,
|
|
716
|
+
operators,
|
|
717
|
+
suggestionListId,
|
|
718
|
+
suggestionsEnabled,
|
|
719
|
+
valueInputClasses,
|
|
720
|
+
valueSuggestions,
|
|
721
|
+
valueDisabled: ['is empty', 'is not empty'].includes(rule.operator),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function renderFilterSuggestionList(presentation, currentValue = '') {
|
|
726
|
+
if (!presentation.suggestionsEnabled) {
|
|
727
|
+
return '';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return `
|
|
731
|
+
<div class="bases-filter-suggestion-list" id="${presentation.suggestionListId}" role="listbox">
|
|
732
|
+
${presentation.valueSuggestions.slice(0, 8).map((option) => `
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
class="bases-filter-suggestion"
|
|
736
|
+
data-base-filter-suggestion="${escapeHtml(presentation.encodedPath)}"
|
|
737
|
+
data-base-filter-suggestion-value="${escapeHtml(option.text)}"
|
|
738
|
+
role="option"
|
|
739
|
+
aria-selected="${option.text === String(currentValue ?? '') ? 'true' : 'false'}"
|
|
740
|
+
>
|
|
741
|
+
<span class="bases-filter-suggestion-label">${escapeHtml(option.text)}</span>
|
|
742
|
+
${option.count == null ? '' : `<span class="bases-filter-suggestion-meta">${escapeHtml(String(option.count))}</span>`}
|
|
743
|
+
</button>
|
|
744
|
+
`).join('')}
|
|
745
|
+
</div>
|
|
746
|
+
`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderFilterValueCombobox(result, rule, path, entry) {
|
|
750
|
+
const presentation = buildFilterValuePresentation(result, rule, path, entry);
|
|
751
|
+
return `
|
|
752
|
+
<div class="bases-filter-value-combobox${presentation.suggestionsEnabled ? ' has-suggestions' : ''}">
|
|
753
|
+
<input class="${escapeHtml(presentation.valueInputClasses)}" type="text" value="${escapeHtml(rule.value ?? '')}" data-base-filter-value="${escapeHtml(presentation.encodedPath)}"${presentation.suggestionsEnabled ? ` aria-controls="${presentation.suggestionListId}" aria-haspopup="listbox" aria-autocomplete="list" autocomplete="off"` : ''}${presentation.valueDisabled ? ' disabled' : ''}>
|
|
754
|
+
${renderFilterSuggestionList(presentation, rule.value ?? '')}
|
|
755
|
+
</div>
|
|
756
|
+
`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function renderFilterRule(result, rule, path, entry) {
|
|
760
|
+
const presentation = buildFilterValuePresentation(result, rule, path, entry);
|
|
761
|
+
|
|
762
|
+
return `
|
|
763
|
+
<div class="bases-filter-row">
|
|
764
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-filter-property' }))}" data-base-filter-property="${escapeHtml(presentation.encodedPath)}">
|
|
765
|
+
${getPropertyOptionsMarkup(result, rule.propertyId)}
|
|
766
|
+
</select>
|
|
767
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-filter-operator' }))}" data-base-filter-operator="${escapeHtml(presentation.encodedPath)}">
|
|
768
|
+
${presentation.operators.map((operator) => `<option value="${escapeHtml(operator)}"${operator === rule.operator ? ' selected' : ''}>${escapeHtml(operator)}</option>`).join('')}
|
|
769
|
+
</select>
|
|
770
|
+
${renderFilterValueCombobox(result, rule, path, entry)}
|
|
771
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-filter-remove="${escapeHtml(presentation.encodedPath)}">Delete</button>
|
|
772
|
+
</div>
|
|
773
|
+
`;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function syncFilterValueCombobox(entry, result, path, { fallbackToFullRender = true } = {}) {
|
|
777
|
+
const shell = findShellElement(entry);
|
|
778
|
+
const panelSlot = shell?.querySelector?.('[data-base-panel-slot]') ?? null;
|
|
779
|
+
const encodedPath = encodePath(path);
|
|
780
|
+
const input = panelSlot?.querySelector?.(`[data-base-filter-value="${encodedPath}"]`) ?? null;
|
|
781
|
+
const combobox = input?.parentElement?.classList?.contains('bases-filter-value-combobox')
|
|
782
|
+
? input.parentElement
|
|
783
|
+
: null;
|
|
784
|
+
const rule = getNodeAtPath(getBuilderFilterState(entry, result), path);
|
|
785
|
+
|
|
786
|
+
if (!input || !combobox || !rule || rule.type !== 'rule' || typeof input.insertAdjacentHTML !== 'function') {
|
|
787
|
+
if (fallbackToFullRender) {
|
|
788
|
+
updateShellContent(entry, result);
|
|
789
|
+
}
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const presentation = buildFilterValuePresentation(result, rule, path, entry);
|
|
794
|
+
combobox.classList.toggle('has-suggestions', presentation.suggestionsEnabled);
|
|
795
|
+
input.className = presentation.valueInputClasses;
|
|
796
|
+
input.disabled = presentation.valueDisabled;
|
|
797
|
+
if (input.value !== String(rule.value ?? '')) {
|
|
798
|
+
input.value = String(rule.value ?? '');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (presentation.suggestionsEnabled) {
|
|
802
|
+
input.setAttribute('aria-controls', presentation.suggestionListId);
|
|
803
|
+
input.setAttribute('aria-haspopup', 'listbox');
|
|
804
|
+
input.setAttribute('aria-autocomplete', 'list');
|
|
805
|
+
input.setAttribute('autocomplete', 'off');
|
|
806
|
+
} else {
|
|
807
|
+
input.removeAttribute('aria-controls');
|
|
808
|
+
input.removeAttribute('aria-haspopup');
|
|
809
|
+
input.removeAttribute('aria-autocomplete');
|
|
810
|
+
input.removeAttribute('autocomplete');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const suggestionList = combobox.querySelector('.bases-filter-suggestion-list');
|
|
814
|
+
suggestionList?.remove?.();
|
|
815
|
+
if (presentation.suggestionsEnabled) {
|
|
816
|
+
input.insertAdjacentHTML('afterend', renderFilterSuggestionList(presentation, rule.value ?? ''));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function forEachFilterRule(node, callback, path = []) {
|
|
823
|
+
if (!node) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (node.type === 'rule') {
|
|
828
|
+
callback(node, path);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
(node.children ?? []).forEach((child, index) => {
|
|
833
|
+
forEachFilterRule(child, callback, [...path, index]);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function renderFilterGroup(result, group, path, entry, { isRoot = false } = {}) {
|
|
838
|
+
const encodedPath = encodePath(path);
|
|
839
|
+
return `
|
|
840
|
+
<div class="bases-filter-group"${isRoot ? ' data-base-filter-root' : ''}>
|
|
841
|
+
<div class="bases-filter-group-header">
|
|
842
|
+
<select class="${escapeHtml(inputClassNames({ extra: 'bases-select bases-filter-conjunction' }))}" data-base-filter-conjunction="${escapeHtml(encodedPath)}">
|
|
843
|
+
${['and', 'or', 'not'].map((conjunction) => `<option value="${conjunction}"${group.conjunction === conjunction ? ' selected' : ''}>${escapeHtml({
|
|
844
|
+
and: 'All the following are true',
|
|
845
|
+
not: 'None of the following are true',
|
|
846
|
+
or: 'Any of the following are true',
|
|
847
|
+
}[conjunction])}</option>`).join('')}
|
|
848
|
+
</select>
|
|
849
|
+
${isRoot ? '' : `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-filter-remove="${escapeHtml(encodedPath)}">Delete group</button>`}
|
|
850
|
+
</div>
|
|
851
|
+
<div class="bases-filter-children">
|
|
852
|
+
${(group.children ?? []).map((child, index) => (
|
|
853
|
+
child?.type === 'group'
|
|
854
|
+
? renderFilterGroup(result, child, [...path, index], entry)
|
|
855
|
+
: renderFilterRule(result, child, [...path, index], entry)
|
|
856
|
+
)).join('')}
|
|
857
|
+
</div>
|
|
858
|
+
<div class="bases-filter-actions-row">
|
|
859
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-filter-add-rule="${escapeHtml(encodedPath)}">Add filter</button>
|
|
860
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact' }))}" data-base-filter-add-group="${escapeHtml(encodedPath)}">Add filter group</button>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function renderFilterPanel(result, entry) {
|
|
867
|
+
const rawFilters = getEditableViewConfig(result).filters;
|
|
868
|
+
const parsed = entry.ui.builderFilter ?? parseFilterNode(rawFilters);
|
|
869
|
+
if (entry.ui.builderFilter == null) {
|
|
870
|
+
entry.ui.builderFilter = parsed;
|
|
871
|
+
}
|
|
872
|
+
const mode = parsed ? (entry.ui.filterMode ?? 'builder') : 'advanced';
|
|
873
|
+
const rawText = entry.ui.rawFilterText ?? serializeRawFilterText(rawFilters);
|
|
874
|
+
|
|
875
|
+
return `
|
|
876
|
+
<section class="bases-panel-card">
|
|
877
|
+
<div class="bases-panel-title-row">
|
|
878
|
+
<div class="bases-panel-title">This view</div>
|
|
879
|
+
${parsed ? `
|
|
880
|
+
<div class="bases-toggle-row">
|
|
881
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: mode === 'builder' ? 'primary' : 'secondary', size: 'compact' }))}" data-base-filter-mode="builder">Builder</button>
|
|
882
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: mode === 'advanced' ? 'primary' : 'secondary', size: 'compact' }))}" data-base-filter-mode="advanced">Advanced</button>
|
|
883
|
+
</div>
|
|
884
|
+
` : ''}
|
|
885
|
+
</div>
|
|
886
|
+
${mode === 'advanced'
|
|
887
|
+
? `
|
|
888
|
+
<label class="bases-filter-advanced-label">
|
|
889
|
+
<textarea class="${escapeHtml(inputClassNames({ extra: 'bases-filter-advanced' }))}" data-base-filter-advanced>${escapeHtml(rawText)}</textarea>
|
|
890
|
+
</label>
|
|
891
|
+
<div class="bases-filter-actions-row">
|
|
892
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'primary', size: 'compact' }))}" data-base-filter-save-advanced>Save filter</button>
|
|
893
|
+
</div>
|
|
894
|
+
`
|
|
895
|
+
: renderFilterGroup(result, parsed ?? createEmptyFilterGroup('and'), [], entry, { isRoot: true })}
|
|
896
|
+
</section>
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function renderPropertiesPanel(result, entry) {
|
|
901
|
+
const propertySearch = String(entry.ui.propertySearch ?? '').trim().toLowerCase();
|
|
902
|
+
const visibleIds = new Set(getVisiblePropertyIds(result));
|
|
903
|
+
const properties = getAvailableProperties(result).filter((property) => (
|
|
904
|
+
!propertySearch
|
|
905
|
+
|| property.label.toLowerCase().includes(propertySearch)
|
|
906
|
+
|| property.id.toLowerCase().includes(propertySearch)
|
|
907
|
+
));
|
|
908
|
+
|
|
909
|
+
return `
|
|
910
|
+
<section class="bases-panel-card">
|
|
911
|
+
<label class="bases-properties-search">
|
|
912
|
+
<input class="${escapeHtml(inputClassNames({ extra: 'bases-properties-search-input' }))}" type="search" value="${escapeHtml(entry.ui.propertySearch ?? '')}" placeholder="Find property" data-base-properties-search>
|
|
913
|
+
</label>
|
|
914
|
+
<div class="bases-properties-list" data-base-properties-list>
|
|
915
|
+
${properties.map((property) => `
|
|
916
|
+
<label class="bases-property-option">
|
|
917
|
+
<input type="checkbox" data-base-property-toggle="${escapeHtml(property.id)}"${visibleIds.has(property.id) ? ' checked' : ''}>
|
|
918
|
+
<span class="bases-property-option-label">${escapeHtml(property.label)}</span>
|
|
919
|
+
<span class="bases-property-option-meta">${escapeHtml(property.kind)}</span>
|
|
920
|
+
</label>
|
|
921
|
+
`).join('')}
|
|
922
|
+
</div>
|
|
923
|
+
</section>
|
|
924
|
+
`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function capturePanelState(panelSlot) {
|
|
928
|
+
const propertiesList = panelSlot?.querySelector?.('[data-base-properties-list]') ?? null;
|
|
929
|
+
const propertiesSearch = panelSlot?.querySelector?.('[data-base-properties-search]') ?? null;
|
|
930
|
+
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
|
|
931
|
+
let focusedControl = null;
|
|
932
|
+
|
|
933
|
+
if (propertiesSearch && activeElement === propertiesSearch) {
|
|
934
|
+
const selectionStart = typeof propertiesSearch.selectionStart === 'number'
|
|
935
|
+
? propertiesSearch.selectionStart
|
|
936
|
+
: null;
|
|
937
|
+
const selectionEnd = typeof propertiesSearch.selectionEnd === 'number'
|
|
938
|
+
? propertiesSearch.selectionEnd
|
|
939
|
+
: selectionStart;
|
|
940
|
+
focusedControl = {
|
|
941
|
+
selectionEnd,
|
|
942
|
+
selectionStart,
|
|
943
|
+
type: 'properties-search',
|
|
944
|
+
};
|
|
945
|
+
} else if (activeElement?.dataset?.baseFilterValue != null) {
|
|
946
|
+
const filterPath = String(activeElement.dataset.baseFilterValue);
|
|
947
|
+
const filterValue = panelSlot?.querySelector?.(`[data-base-filter-value="${filterPath}"]`) ?? null;
|
|
948
|
+
if (filterValue && activeElement === filterValue) {
|
|
949
|
+
const selectionStart = typeof filterValue.selectionStart === 'number'
|
|
950
|
+
? filterValue.selectionStart
|
|
951
|
+
: null;
|
|
952
|
+
const selectionEnd = typeof filterValue.selectionEnd === 'number'
|
|
953
|
+
? filterValue.selectionEnd
|
|
954
|
+
: selectionStart;
|
|
955
|
+
focusedControl = {
|
|
956
|
+
path: filterPath,
|
|
957
|
+
selectionEnd,
|
|
958
|
+
selectionStart,
|
|
959
|
+
type: 'filter-value',
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
focusedControl,
|
|
966
|
+
propertiesListScrollTop: propertiesList ? propertiesList.scrollTop : null,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function restorePanelState(panelSlot, panelState) {
|
|
971
|
+
if (!panelState) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (panelState.propertiesListScrollTop != null) {
|
|
976
|
+
const propertiesList = panelSlot?.querySelector?.('[data-base-properties-list]') ?? null;
|
|
977
|
+
if (propertiesList) {
|
|
978
|
+
propertiesList.scrollTop = panelState.propertiesListScrollTop;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (panelState.focusedControl?.type === 'properties-search') {
|
|
983
|
+
const propertiesSearch = panelSlot?.querySelector?.('[data-base-properties-search]') ?? null;
|
|
984
|
+
if (propertiesSearch) {
|
|
985
|
+
propertiesSearch.focus?.();
|
|
986
|
+
if (
|
|
987
|
+
typeof propertiesSearch.setSelectionRange === 'function'
|
|
988
|
+
&& panelState.focusedControl.selectionStart != null
|
|
989
|
+
) {
|
|
990
|
+
propertiesSearch.setSelectionRange(
|
|
991
|
+
panelState.focusedControl.selectionStart,
|
|
992
|
+
panelState.focusedControl.selectionEnd ?? panelState.focusedControl.selectionStart,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (panelState.focusedControl?.type === 'filter-value' && panelState.focusedControl.path != null) {
|
|
1000
|
+
const filterValue = panelSlot?.querySelector?.(`[data-base-filter-value="${panelState.focusedControl.path}"]`) ?? null;
|
|
1001
|
+
if (filterValue) {
|
|
1002
|
+
filterValue.focus?.();
|
|
1003
|
+
if (
|
|
1004
|
+
typeof filterValue.setSelectionRange === 'function'
|
|
1005
|
+
&& panelState.focusedControl.selectionStart != null
|
|
1006
|
+
) {
|
|
1007
|
+
filterValue.setSelectionRange(
|
|
1008
|
+
panelState.focusedControl.selectionStart,
|
|
1009
|
+
panelState.focusedControl.selectionEnd ?? panelState.focusedControl.selectionStart,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function renderPanel(result, entry) {
|
|
1017
|
+
if (!entry.ui?.openPanel) {
|
|
1018
|
+
return '';
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (!getMeta(result).editable) {
|
|
1022
|
+
return '<section class="bases-panel-card"><div class="bases-readonly-note">Inline base previews are read-only. Open the .base file to edit its sort, filter, and properties.</div></section>';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
switch (entry.ui.openPanel) {
|
|
1026
|
+
case 'filter':
|
|
1027
|
+
return renderFilterPanel(result, entry);
|
|
1028
|
+
case 'properties':
|
|
1029
|
+
return renderPropertiesPanel(result, entry);
|
|
1030
|
+
case 'sort':
|
|
1031
|
+
default:
|
|
1032
|
+
return renderSortPanel(result);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function renderShellHtml(result, entry) {
|
|
1037
|
+
return `
|
|
1038
|
+
<section class="bases-shell" data-base-shell-key="${escapeHtml(entry.key)}">
|
|
181
1039
|
<header class="bases-toolbar">
|
|
182
1040
|
<div class="bases-toolbar-main">
|
|
183
|
-
<div class="bases-tabs" data-base-tabs>
|
|
1041
|
+
<div class="${escapeHtml(segmentedControlClassNames({ pill: true, extra: 'bases-tabs' }))}" data-base-tabs>
|
|
184
1042
|
${renderViewTabs(result)}
|
|
185
1043
|
</div>
|
|
186
1044
|
<div class="bases-meta" data-base-meta>${escapeHtml(String(result.totalRows ?? 0))} results</div>
|
|
187
1045
|
</div>
|
|
188
1046
|
<div class="bases-toolbar-actions">
|
|
189
|
-
<
|
|
190
|
-
<
|
|
1047
|
+
<div class="bases-toolbar-edit-actions" data-base-action-slot>${renderActionButtons(result, entry)}</div>
|
|
1048
|
+
<label class="bases-search-shell" aria-label="Search this base">
|
|
1049
|
+
<svg class="bases-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1050
|
+
<circle cx="11" cy="11" r="7"></circle>
|
|
1051
|
+
<path d="m20 20-3.5-3.5"></path>
|
|
1052
|
+
</svg>
|
|
1053
|
+
<input class="${escapeHtml(inputClassNames({ extra: 'bases-search-input' }))}" type="search" value="${escapeHtml(entry.search)}" placeholder="Search this base" aria-label="Search this base">
|
|
1054
|
+
</label>
|
|
1055
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', pill: true, extra: 'bases-export-btn' }))}">Export CSV</button>
|
|
191
1056
|
</div>
|
|
192
1057
|
</header>
|
|
1058
|
+
<div class="bases-panels" data-base-panel-slot>${renderPanel(result, entry)}</div>
|
|
193
1059
|
<div data-base-summary-slot>${renderSummaryBar(result.summaries)}</div>
|
|
194
1060
|
<div class="bases-content" data-base-content>${renderViewBody(result)}</div>
|
|
195
1061
|
</section>
|
|
@@ -205,10 +1071,20 @@ function findShellElement(entry) {
|
|
|
205
1071
|
?? entry.placeholder.querySelector('.bases-shell');
|
|
206
1072
|
}
|
|
207
1073
|
|
|
1074
|
+
function markPlaceholderHydrated(placeholder) {
|
|
1075
|
+
if (!placeholder?.classList) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
placeholder.classList.add('is-hydrated');
|
|
1080
|
+
placeholder.classList.remove('diagram-preview-shell');
|
|
1081
|
+
}
|
|
1082
|
+
|
|
208
1083
|
function updateShellContent(entry, result) {
|
|
209
1084
|
const shell = findShellElement(entry);
|
|
210
1085
|
if (!shell?.querySelector) {
|
|
211
1086
|
entry.placeholder.innerHTML = renderShellHtml(result, entry);
|
|
1087
|
+
markPlaceholderHydrated(entry.placeholder);
|
|
212
1088
|
return;
|
|
213
1089
|
}
|
|
214
1090
|
|
|
@@ -222,6 +1098,18 @@ function updateShellContent(entry, result) {
|
|
|
222
1098
|
meta.textContent = `${String(result.totalRows ?? 0)} results`;
|
|
223
1099
|
}
|
|
224
1100
|
|
|
1101
|
+
const actionSlot = shell.querySelector('[data-base-action-slot]');
|
|
1102
|
+
if (actionSlot) {
|
|
1103
|
+
actionSlot.innerHTML = renderActionButtons(result, entry);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const panelSlot = shell.querySelector('[data-base-panel-slot]');
|
|
1107
|
+
if (panelSlot) {
|
|
1108
|
+
const panelState = capturePanelState(panelSlot);
|
|
1109
|
+
panelSlot.innerHTML = renderPanel(result, entry);
|
|
1110
|
+
restorePanelState(panelSlot, panelState);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
225
1113
|
const summarySlot = shell.querySelector('[data-base-summary-slot]');
|
|
226
1114
|
if (summarySlot) {
|
|
227
1115
|
summarySlot.innerHTML = renderSummaryBar(result.summaries);
|
|
@@ -261,19 +1149,57 @@ function parseDownloadFileName(contentDisposition = '') {
|
|
|
261
1149
|
export class BasesPreviewController {
|
|
262
1150
|
constructor({
|
|
263
1151
|
getActiveFilePath = () => '',
|
|
1152
|
+
getSession = () => null,
|
|
264
1153
|
onOpenFile,
|
|
265
1154
|
previewElement,
|
|
1155
|
+
replaceBaseSource = null,
|
|
266
1156
|
toastController,
|
|
267
1157
|
vaultApiClient,
|
|
268
1158
|
}) {
|
|
269
1159
|
this.getActiveFilePath = getActiveFilePath;
|
|
1160
|
+
this.getSession = getSession;
|
|
270
1161
|
this.onOpenFile = onOpenFile;
|
|
271
1162
|
this.previewElement = previewElement;
|
|
1163
|
+
this.replaceBaseSource = replaceBaseSource;
|
|
272
1164
|
this.toastController = toastController;
|
|
273
1165
|
this.vaultApiClient = vaultApiClient;
|
|
274
1166
|
this.entries = new Map();
|
|
275
1167
|
this.searchTimers = new Map();
|
|
276
1168
|
|
|
1169
|
+
this.applyFilterSuggestion = (entry, path, nextValue) => {
|
|
1170
|
+
void this.updateBuilderFilter(entry, (group) => updateNodeAtPath(group, path, (node) => ({
|
|
1171
|
+
...node,
|
|
1172
|
+
value: nextValue,
|
|
1173
|
+
})));
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
this.persistBuilderFilter = (entry) => {
|
|
1177
|
+
const nextGroup = getBuilderFilterState(entry, entry.result);
|
|
1178
|
+
entry.ui.builderFilter = nextGroup;
|
|
1179
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1180
|
+
config.filters = compileFilterGroup(nextGroup, entry.result);
|
|
1181
|
+
return config;
|
|
1182
|
+
});
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
this.handlePointerDown = (event) => {
|
|
1186
|
+
const filterSuggestion = event.target.closest('[data-base-filter-suggestion]');
|
|
1187
|
+
if (!filterSuggestion) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const shell = event.target.closest('[data-base-shell-key]');
|
|
1192
|
+
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
1193
|
+
if (!entry) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
event.preventDefault();
|
|
1198
|
+
const path = decodePath(filterSuggestion.dataset.baseFilterSuggestion || '');
|
|
1199
|
+
const nextValue = filterSuggestion.dataset.baseFilterSuggestionValue || '';
|
|
1200
|
+
this.applyFilterSuggestion(entry, path, nextValue);
|
|
1201
|
+
};
|
|
1202
|
+
|
|
277
1203
|
this.handleClick = (event) => {
|
|
278
1204
|
const openButton = event.target.closest('[data-base-open-file]');
|
|
279
1205
|
if (openButton) {
|
|
@@ -282,58 +1208,378 @@ export class BasesPreviewController {
|
|
|
282
1208
|
return;
|
|
283
1209
|
}
|
|
284
1210
|
|
|
1211
|
+
const shell = event.target.closest('[data-base-shell-key]');
|
|
1212
|
+
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
1213
|
+
if (!entry) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
285
1217
|
const tab = event.target.closest('.bases-view-tab');
|
|
286
1218
|
if (tab) {
|
|
287
|
-
|
|
1219
|
+
entry.payload.view = tab.dataset.baseView || '';
|
|
1220
|
+
entry.ui.openPanel = '';
|
|
1221
|
+
void this.renderEntry(entry);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const panelButton = event.target.closest('[data-base-panel]');
|
|
1226
|
+
if (panelButton) {
|
|
1227
|
+
const panel = panelButton.dataset.basePanel || '';
|
|
1228
|
+
entry.ui.openPanel = entry.ui.openPanel === panel ? '' : panel;
|
|
1229
|
+
entry.ui.rawFilterText = serializeRawFilterText(getEditableViewConfig(entry.result).filters);
|
|
1230
|
+
updateShellContent(entry, entry.result);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const exportButton = event.target.closest('.bases-export-btn');
|
|
1235
|
+
if (exportButton) {
|
|
1236
|
+
void this.exportEntry(entry);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (event.target.closest('[data-base-sort-add]')) {
|
|
1241
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1242
|
+
const used = new Set((config.sort ?? []).map((sort) => sort.property));
|
|
1243
|
+
const nextProperty = getAvailableProperties(entry.result).find((property) => !used.has(property.id))?.id ?? 'file.name';
|
|
1244
|
+
config.sort = [...(config.sort ?? []), { direction: 'asc', property: nextProperty }];
|
|
1245
|
+
return config;
|
|
1246
|
+
});
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const sortDelete = event.target.closest('[data-base-sort-delete]');
|
|
1251
|
+
if (sortDelete) {
|
|
1252
|
+
const index = Number.parseInt(sortDelete.dataset.baseSortDelete || '', 10);
|
|
1253
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1254
|
+
config.sort = (config.sort ?? []).filter((_, sortIndex) => sortIndex !== index);
|
|
1255
|
+
return config;
|
|
1256
|
+
});
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const sortMove = event.target.closest('[data-base-sort-move]');
|
|
1261
|
+
if (sortMove) {
|
|
1262
|
+
const [indexText, direction] = String(sortMove.dataset.baseSortMove || '').split(':');
|
|
1263
|
+
const index = Number.parseInt(indexText, 10);
|
|
1264
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1265
|
+
const nextSorts = [...(config.sort ?? [])];
|
|
1266
|
+
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
1267
|
+
if (targetIndex < 0 || targetIndex >= nextSorts.length) {
|
|
1268
|
+
return config;
|
|
1269
|
+
}
|
|
1270
|
+
[nextSorts[index], nextSorts[targetIndex]] = [nextSorts[targetIndex], nextSorts[index]];
|
|
1271
|
+
config.sort = nextSorts;
|
|
1272
|
+
return config;
|
|
1273
|
+
});
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (event.target.closest('[data-base-clear-group-by]')) {
|
|
1278
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1279
|
+
config.groupBy = null;
|
|
1280
|
+
return config;
|
|
1281
|
+
});
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const filterModeButton = event.target.closest('[data-base-filter-mode]');
|
|
1286
|
+
if (filterModeButton) {
|
|
1287
|
+
entry.ui.filterMode = filterModeButton.dataset.baseFilterMode || 'builder';
|
|
1288
|
+
if (entry.ui.filterMode === 'advanced') {
|
|
1289
|
+
entry.ui.rawFilterText = serializeRawFilterText(getEditableViewConfig(entry.result).filters);
|
|
1290
|
+
entry.ui.builderFilter = null;
|
|
1291
|
+
}
|
|
1292
|
+
updateShellContent(entry, entry.result);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (event.target.closest('[data-base-filter-save-advanced]')) {
|
|
1297
|
+
void this.saveAdvancedFilter(entry);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const addFilterRule = event.target.closest('[data-base-filter-add-rule]');
|
|
1302
|
+
if (addFilterRule) {
|
|
1303
|
+
const path = decodePath(addFilterRule.dataset.baseFilterAddRule || '');
|
|
1304
|
+
void this.updateBuilderFilter(entry, (group) => addNodeAtPath(group, path, {
|
|
1305
|
+
operator: 'is',
|
|
1306
|
+
propertyId: getAvailableProperties(entry.result)[0]?.id ?? 'file.name',
|
|
1307
|
+
type: 'rule',
|
|
1308
|
+
value: '',
|
|
1309
|
+
}));
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const addFilterGroup = event.target.closest('[data-base-filter-add-group]');
|
|
1314
|
+
if (addFilterGroup) {
|
|
1315
|
+
const path = decodePath(addFilterGroup.dataset.baseFilterAddGroup || '');
|
|
1316
|
+
void this.updateBuilderFilter(entry, (group) => addNodeAtPath(group, path, createEmptyFilterGroup('and')));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const removeFilter = event.target.closest('[data-base-filter-remove]');
|
|
1321
|
+
if (removeFilter) {
|
|
1322
|
+
const path = decodePath(removeFilter.dataset.baseFilterRemove || '');
|
|
1323
|
+
void this.updateBuilderFilter(entry, (group) => removeNodeAtPath(group, path));
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const filterSuggestion = event.target.closest('[data-base-filter-suggestion]');
|
|
1328
|
+
if (filterSuggestion) {
|
|
1329
|
+
event.preventDefault?.();
|
|
1330
|
+
if (event.detail != null && event.detail !== 0) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const path = decodePath(filterSuggestion.dataset.baseFilterSuggestion || '');
|
|
1334
|
+
const nextValue = filterSuggestion.dataset.baseFilterSuggestionValue || '';
|
|
1335
|
+
this.applyFilterSuggestion(entry, path, nextValue);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
this.handleInput = (event) => {
|
|
1340
|
+
const input = event.target.closest('.bases-search-input');
|
|
1341
|
+
if (input) {
|
|
1342
|
+
const shell = input.closest('[data-base-shell-key]');
|
|
288
1343
|
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
289
1344
|
if (!entry) {
|
|
290
1345
|
return;
|
|
291
1346
|
}
|
|
292
1347
|
|
|
293
|
-
entry.
|
|
294
|
-
|
|
1348
|
+
entry.search = input.value || '';
|
|
1349
|
+
clearTimeout(this.searchTimers.get(entry.key));
|
|
1350
|
+
this.searchTimers.set(entry.key, setTimeout(() => {
|
|
1351
|
+
entry.payload.search = entry.search;
|
|
1352
|
+
void this.renderEntry(entry);
|
|
1353
|
+
}, 180));
|
|
295
1354
|
return;
|
|
296
1355
|
}
|
|
297
1356
|
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
const shell =
|
|
1357
|
+
const propertiesSearch = event.target.closest('[data-base-properties-search]');
|
|
1358
|
+
if (propertiesSearch) {
|
|
1359
|
+
const shell = propertiesSearch.closest('[data-base-shell-key]');
|
|
301
1360
|
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
302
1361
|
if (!entry) {
|
|
303
1362
|
return;
|
|
304
1363
|
}
|
|
305
1364
|
|
|
306
|
-
|
|
1365
|
+
entry.ui.propertySearch = propertiesSearch.value || '';
|
|
1366
|
+
updateShellContent(entry, entry.result);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const advancedFilter = event.target.closest('[data-base-filter-advanced]');
|
|
1371
|
+
if (advancedFilter) {
|
|
1372
|
+
const shell = advancedFilter.closest('[data-base-shell-key]');
|
|
1373
|
+
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
1374
|
+
if (!entry) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
entry.ui.rawFilterText = advancedFilter.value || '';
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const filterValue = event.target.closest('[data-base-filter-value]');
|
|
1383
|
+
if (filterValue) {
|
|
1384
|
+
const shell = filterValue.closest('[data-base-shell-key]');
|
|
1385
|
+
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
1386
|
+
if (!entry) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const path = decodePath(filterValue.dataset.baseFilterValue || '');
|
|
1391
|
+
const parsed = entry.ui.builderFilter
|
|
1392
|
+
?? parseFilterNode(getEditableViewConfig(entry.result).filters)
|
|
1393
|
+
?? createEmptyFilterGroup('and');
|
|
1394
|
+
entry.ui.builderFilter = updateNodeAtPath(parsed, path, (node) => ({
|
|
1395
|
+
...node,
|
|
1396
|
+
value: filterValue.value || '',
|
|
1397
|
+
}));
|
|
1398
|
+
syncFilterValueCombobox(entry, entry.result, path);
|
|
307
1399
|
}
|
|
308
1400
|
};
|
|
309
1401
|
|
|
310
|
-
this.
|
|
311
|
-
const
|
|
312
|
-
|
|
1402
|
+
this.handleChange = (event) => {
|
|
1403
|
+
const shell = event.target.closest('[data-base-shell-key]');
|
|
1404
|
+
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
1405
|
+
if (!entry) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const groupByProperty = event.target.closest('[data-base-group-by-property]');
|
|
1410
|
+
if (groupByProperty) {
|
|
1411
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1412
|
+
config.groupBy = groupByProperty.value
|
|
1413
|
+
? {
|
|
1414
|
+
direction: config.groupBy?.direction ?? 'asc',
|
|
1415
|
+
property: groupByProperty.value,
|
|
1416
|
+
}
|
|
1417
|
+
: null;
|
|
1418
|
+
return config;
|
|
1419
|
+
});
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const groupByDirection = event.target.closest('[data-base-group-by-direction]');
|
|
1424
|
+
if (groupByDirection) {
|
|
1425
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1426
|
+
config.groupBy = config.groupBy?.property
|
|
1427
|
+
? {
|
|
1428
|
+
direction: groupByDirection.value || 'asc',
|
|
1429
|
+
property: config.groupBy.property,
|
|
1430
|
+
}
|
|
1431
|
+
: null;
|
|
1432
|
+
return config;
|
|
1433
|
+
});
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const sortProperty = event.target.closest('[data-base-sort-property]');
|
|
1438
|
+
if (sortProperty) {
|
|
1439
|
+
const index = Number.parseInt(sortProperty.dataset.baseSortProperty || '', 10);
|
|
1440
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1441
|
+
config.sort = (config.sort ?? []).map((sortConfig, sortIndex) => (
|
|
1442
|
+
sortIndex === index
|
|
1443
|
+
? { ...sortConfig, property: sortProperty.value }
|
|
1444
|
+
: sortConfig
|
|
1445
|
+
));
|
|
1446
|
+
return config;
|
|
1447
|
+
});
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const sortDirection = event.target.closest('[data-base-sort-direction]');
|
|
1452
|
+
if (sortDirection) {
|
|
1453
|
+
const index = Number.parseInt(sortDirection.dataset.baseSortDirection || '', 10);
|
|
1454
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1455
|
+
config.sort = (config.sort ?? []).map((sortConfig, sortIndex) => (
|
|
1456
|
+
sortIndex === index
|
|
1457
|
+
? { ...sortConfig, direction: sortDirection.value || 'asc' }
|
|
1458
|
+
: sortConfig
|
|
1459
|
+
));
|
|
1460
|
+
return config;
|
|
1461
|
+
});
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const propertyToggle = event.target.closest('[data-base-property-toggle]');
|
|
1466
|
+
if (propertyToggle) {
|
|
1467
|
+
const propertyId = propertyToggle.dataset.basePropertyToggle || '';
|
|
1468
|
+
void this.updateViewConfig(entry, (config) => {
|
|
1469
|
+
const nextOrder = config.order?.length > 0
|
|
1470
|
+
? [...config.order]
|
|
1471
|
+
: getVisiblePropertyIds(entry.result);
|
|
1472
|
+
const existingIndex = nextOrder.indexOf(propertyId);
|
|
1473
|
+
if (propertyToggle.checked && existingIndex === -1) {
|
|
1474
|
+
nextOrder.push(propertyId);
|
|
1475
|
+
}
|
|
1476
|
+
if (!propertyToggle.checked && existingIndex >= 0) {
|
|
1477
|
+
nextOrder.splice(existingIndex, 1);
|
|
1478
|
+
}
|
|
1479
|
+
config.order = nextOrder;
|
|
1480
|
+
return config;
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const filterConjunction = event.target.closest('[data-base-filter-conjunction]');
|
|
1486
|
+
if (filterConjunction) {
|
|
1487
|
+
const path = decodePath(filterConjunction.dataset.baseFilterConjunction || '');
|
|
1488
|
+
void this.updateBuilderFilter(entry, (group) => updateNodeAtPath(group, path, (node) => ({
|
|
1489
|
+
...node,
|
|
1490
|
+
conjunction: filterConjunction.value || 'and',
|
|
1491
|
+
})));
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const filterProperty = event.target.closest('[data-base-filter-property]');
|
|
1496
|
+
if (filterProperty) {
|
|
1497
|
+
const path = decodePath(filterProperty.dataset.baseFilterProperty || '');
|
|
1498
|
+
void (async () => {
|
|
1499
|
+
await this.updateBuilderFilter(entry, (group) => updateNodeAtPath(group, path, (node) => {
|
|
1500
|
+
const nextOperators = getPropertyFilterOperators(entry.result, filterProperty.value);
|
|
1501
|
+
const nextOperator = nextOperators[0] ?? 'is';
|
|
1502
|
+
return {
|
|
1503
|
+
...node,
|
|
1504
|
+
operator: nextOperator,
|
|
1505
|
+
propertyId: filterProperty.value,
|
|
1506
|
+
value: '',
|
|
1507
|
+
};
|
|
1508
|
+
}));
|
|
1509
|
+
await this.ensurePropertyValues(entry, filterProperty.value);
|
|
1510
|
+
})();
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const filterOperator = event.target.closest('[data-base-filter-operator]');
|
|
1515
|
+
if (filterOperator) {
|
|
1516
|
+
const path = decodePath(filterOperator.dataset.baseFilterOperator || '');
|
|
1517
|
+
void this.updateBuilderFilter(entry, (group) => updateNodeAtPath(group, path, (node) => ({
|
|
1518
|
+
...node,
|
|
1519
|
+
operator: filterOperator.value || 'is',
|
|
1520
|
+
})));
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const filterValue = event.target.closest('[data-base-filter-value]');
|
|
1525
|
+
if (filterValue) {
|
|
1526
|
+
this.persistBuilderFilter(entry);
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
this.handleFocusIn = (event) => {
|
|
1531
|
+
const filterValue = event.target.closest('[data-base-filter-value]');
|
|
1532
|
+
if (!filterValue) {
|
|
313
1533
|
return;
|
|
314
1534
|
}
|
|
315
1535
|
|
|
316
|
-
const shell =
|
|
1536
|
+
const shell = filterValue.closest('[data-base-shell-key]');
|
|
317
1537
|
const entry = shell ? this.entries.get(shell.dataset.baseShellKey || '') : null;
|
|
318
1538
|
if (!entry) {
|
|
319
1539
|
return;
|
|
320
1540
|
}
|
|
321
1541
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
void this.
|
|
327
|
-
}
|
|
1542
|
+
const path = decodePath(filterValue.dataset.baseFilterValue || '');
|
|
1543
|
+
const parsed = parseFilterNode(getEditableViewConfig(entry.result).filters);
|
|
1544
|
+
const node = parsed ? getNodeAtPath(parsed, path) : null;
|
|
1545
|
+
if (node?.propertyId) {
|
|
1546
|
+
void this.ensurePropertyValues(entry, node.propertyId);
|
|
1547
|
+
}
|
|
328
1548
|
};
|
|
329
1549
|
|
|
1550
|
+
this.previewElement?.addEventListener('pointerdown', this.handlePointerDown);
|
|
330
1551
|
this.previewElement?.addEventListener('click', this.handleClick);
|
|
331
1552
|
this.previewElement?.addEventListener('input', this.handleInput);
|
|
1553
|
+
this.previewElement?.addEventListener('change', this.handleChange);
|
|
1554
|
+
this.previewElement?.addEventListener('focusin', this.handleFocusIn);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
ensureEntryState(entry) {
|
|
1558
|
+
if (!entry.ui) {
|
|
1559
|
+
entry.ui = {
|
|
1560
|
+
builderFilter: null,
|
|
1561
|
+
filterMode: 'builder',
|
|
1562
|
+
openPanel: '',
|
|
1563
|
+
propertySearch: '',
|
|
1564
|
+
rawFilterText: '',
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!Object.prototype.hasOwnProperty.call(entry.ui, 'builderFilter')) {
|
|
1569
|
+
entry.ui.builderFilter = null;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (!entry.propertyValueOptions) {
|
|
1573
|
+
entry.propertyValueOptions = new Map();
|
|
1574
|
+
}
|
|
332
1575
|
}
|
|
333
1576
|
|
|
334
1577
|
destroy() {
|
|
1578
|
+
this.previewElement?.removeEventListener('pointerdown', this.handlePointerDown);
|
|
335
1579
|
this.previewElement?.removeEventListener('click', this.handleClick);
|
|
336
1580
|
this.previewElement?.removeEventListener('input', this.handleInput);
|
|
1581
|
+
this.previewElement?.removeEventListener('change', this.handleChange);
|
|
1582
|
+
this.previewElement?.removeEventListener('focusin', this.handleFocusIn);
|
|
337
1583
|
this.searchTimers.forEach((timer) => clearTimeout(timer));
|
|
338
1584
|
this.searchTimers.clear();
|
|
339
1585
|
this.entries.clear();
|
|
@@ -354,14 +1600,22 @@ export class BasesPreviewController {
|
|
|
354
1600
|
sourcePath: placeholder.dataset.baseSourcePath || '',
|
|
355
1601
|
view: placeholder.dataset.baseView || '',
|
|
356
1602
|
};
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1603
|
+
const entry = existing ?? {
|
|
1604
|
+
key,
|
|
1605
|
+
payload,
|
|
1606
|
+
placeholder,
|
|
1607
|
+
propertyValueOptions: new Map(),
|
|
1608
|
+
requestVersion: 0,
|
|
1609
|
+
result: null,
|
|
1610
|
+
search: '',
|
|
1611
|
+
ui: {
|
|
1612
|
+
builderFilter: null,
|
|
1613
|
+
filterMode: 'builder',
|
|
1614
|
+
openPanel: '',
|
|
1615
|
+
propertySearch: '',
|
|
1616
|
+
rawFilterText: '',
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
365
1619
|
entry.payload = payload;
|
|
366
1620
|
entry.placeholder = placeholder;
|
|
367
1621
|
this.entries.set(key, entry);
|
|
@@ -381,9 +1635,17 @@ export class BasesPreviewController {
|
|
|
381
1635
|
view: '',
|
|
382
1636
|
},
|
|
383
1637
|
placeholder: renderHost,
|
|
1638
|
+
propertyValueOptions: new Map(),
|
|
384
1639
|
requestVersion: 0,
|
|
385
1640
|
result: null,
|
|
386
1641
|
search: '',
|
|
1642
|
+
ui: {
|
|
1643
|
+
builderFilter: null,
|
|
1644
|
+
filterMode: 'builder',
|
|
1645
|
+
openPanel: '',
|
|
1646
|
+
propertySearch: '',
|
|
1647
|
+
rawFilterText: '',
|
|
1648
|
+
},
|
|
387
1649
|
};
|
|
388
1650
|
entry.placeholder = renderHost;
|
|
389
1651
|
entry.payload.path = filePath;
|
|
@@ -394,6 +1656,7 @@ export class BasesPreviewController {
|
|
|
394
1656
|
}
|
|
395
1657
|
|
|
396
1658
|
async renderEntry(entry) {
|
|
1659
|
+
this.ensureEntryState(entry);
|
|
397
1660
|
if (!entry?.placeholder?.isConnected) {
|
|
398
1661
|
return;
|
|
399
1662
|
}
|
|
@@ -413,6 +1676,10 @@ export class BasesPreviewController {
|
|
|
413
1676
|
return;
|
|
414
1677
|
}
|
|
415
1678
|
entry.result = response.result;
|
|
1679
|
+
entry.ui.builderFilter = parseFilterNode(getEditableViewConfig(entry.result).filters);
|
|
1680
|
+
if (!entry.ui.rawFilterText) {
|
|
1681
|
+
entry.ui.rawFilterText = serializeRawFilterText(getEditableViewConfig(entry.result).filters);
|
|
1682
|
+
}
|
|
416
1683
|
updateShellContent(entry, entry.result);
|
|
417
1684
|
} catch (error) {
|
|
418
1685
|
if (requestVersion !== entry.requestVersion || !entry?.placeholder?.isConnected) {
|
|
@@ -431,6 +1698,7 @@ export class BasesPreviewController {
|
|
|
431
1698
|
}
|
|
432
1699
|
|
|
433
1700
|
async exportEntry(entry) {
|
|
1701
|
+
this.ensureEntryState(entry);
|
|
434
1702
|
try {
|
|
435
1703
|
const response = await this.vaultApiClient.exportBaseCsv({
|
|
436
1704
|
activeFilePath: this.getActiveFilePath?.() ?? '',
|
|
@@ -441,4 +1709,115 @@ export class BasesPreviewController {
|
|
|
441
1709
|
this.toastController?.show?.(error.message || 'Failed to export base CSV');
|
|
442
1710
|
}
|
|
443
1711
|
}
|
|
1712
|
+
|
|
1713
|
+
async ensurePropertyValues(entry, propertyId = '') {
|
|
1714
|
+
this.ensureEntryState(entry);
|
|
1715
|
+
if (!propertyId) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const cacheKey = createPropertyValuesCacheKey(entry, propertyId);
|
|
1720
|
+
const cachedEntry = entry.propertyValueOptions.get(propertyId) ?? null;
|
|
1721
|
+
if (cachedEntry?.cacheKey === cacheKey) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
try {
|
|
1726
|
+
const response = await this.vaultApiClient.queryBasePropertyValues({
|
|
1727
|
+
activeFilePath: this.getActiveFilePath?.() ?? '',
|
|
1728
|
+
...entry.payload,
|
|
1729
|
+
propertyId,
|
|
1730
|
+
});
|
|
1731
|
+
entry.propertyValueOptions.set(propertyId, {
|
|
1732
|
+
cacheKey,
|
|
1733
|
+
values: response.result?.values ?? [],
|
|
1734
|
+
});
|
|
1735
|
+
if (entry.result && entry.ui.openPanel === 'filter') {
|
|
1736
|
+
let synced = false;
|
|
1737
|
+
forEachFilterRule(getBuilderFilterState(entry, entry.result), (rule, path) => {
|
|
1738
|
+
if (rule.propertyId === propertyId) {
|
|
1739
|
+
synced = syncFilterValueCombobox(entry, entry.result, path, { fallbackToFullRender: false }) || synced;
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
if (!synced) {
|
|
1743
|
+
updateShellContent(entry, entry.result);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
} catch {
|
|
1747
|
+
entry.propertyValueOptions.set(propertyId, {
|
|
1748
|
+
cacheKey,
|
|
1749
|
+
values: [],
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
async applyTransformedSource(entry, transformed) {
|
|
1755
|
+
this.ensureEntryState(entry);
|
|
1756
|
+
if (isStandaloneBaseEntry(entry)) {
|
|
1757
|
+
entry.payload.source = transformed.source;
|
|
1758
|
+
this.replaceBaseSource?.({
|
|
1759
|
+
path: entry.payload.path,
|
|
1760
|
+
source: transformed.source,
|
|
1761
|
+
});
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
await this.vaultApiClient.writeFile({
|
|
1766
|
+
content: transformed.source,
|
|
1767
|
+
path: entry.payload.path,
|
|
1768
|
+
});
|
|
1769
|
+
entry.payload.source = null;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
async updateViewConfig(entry, updater) {
|
|
1773
|
+
this.ensureEntryState(entry);
|
|
1774
|
+
if (!entry?.result || !getMeta(entry.result).editable) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
const nextConfig = updater(getEditableViewConfig(entry.result));
|
|
1779
|
+
try {
|
|
1780
|
+
const response = await this.vaultApiClient.transformBase({
|
|
1781
|
+
activeFilePath: this.getActiveFilePath?.() ?? '',
|
|
1782
|
+
mutation: {
|
|
1783
|
+
config: nextConfig,
|
|
1784
|
+
type: 'set-view-config',
|
|
1785
|
+
view: entry.result.view.id,
|
|
1786
|
+
},
|
|
1787
|
+
...entry.payload,
|
|
1788
|
+
});
|
|
1789
|
+
const transformed = response.result;
|
|
1790
|
+
await this.applyTransformedSource(entry, transformed);
|
|
1791
|
+
entry.result = transformed.result;
|
|
1792
|
+
entry.ui.rawFilterText = serializeRawFilterText(getEditableViewConfig(entry.result).filters);
|
|
1793
|
+
updateShellContent(entry, entry.result);
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
this.toastController?.show?.(error.message || 'Failed to update base');
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
async updateBuilderFilter(entry, updater) {
|
|
1800
|
+
this.ensureEntryState(entry);
|
|
1801
|
+
const parsed = entry.ui.builderFilter ?? parseFilterNode(getEditableViewConfig(entry.result).filters) ?? createEmptyFilterGroup('and');
|
|
1802
|
+
const nextGroup = updater(parsed);
|
|
1803
|
+
entry.ui.builderFilter = nextGroup;
|
|
1804
|
+
await this.updateViewConfig(entry, (config) => {
|
|
1805
|
+
config.filters = compileFilterGroup(nextGroup, entry.result);
|
|
1806
|
+
return config;
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async saveAdvancedFilter(entry) {
|
|
1811
|
+
this.ensureEntryState(entry);
|
|
1812
|
+
try {
|
|
1813
|
+
const parsed = parseRawFilterText(entry.ui.rawFilterText ?? '');
|
|
1814
|
+
entry.ui.builderFilter = parseFilterNode(parsed);
|
|
1815
|
+
await this.updateViewConfig(entry, (config) => {
|
|
1816
|
+
config.filters = parsed;
|
|
1817
|
+
return config;
|
|
1818
|
+
});
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
this.toastController?.show?.(error.message || 'Invalid advanced filter');
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
444
1823
|
}
|