@mongoosejs/studio 0.5.6 → 0.5.8
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/README.md +35 -3
- package/express.js +8 -1
- package/frontend/public/app.js +988 -216
- package/frontend/public/tw.css +113 -0
- package/frontend/src/_util/calendar.js +155 -0
- package/frontend/src/_util/document-search-autocomplete.js +105 -23
- package/frontend/src/custom-date-picker/custom-date-picker.html +183 -0
- package/frontend/src/custom-date-picker/custom-date-picker.js +407 -0
- package/frontend/src/document/document.css +62 -0
- package/frontend/src/document/document.html +113 -2
- package/frontend/src/document/document.js +171 -9
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html +1 -0
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js +1 -1
- package/frontend/src/document-details/document-details.css +1 -63
- package/frontend/src/document-details/document-details.html +0 -122
- package/frontend/src/document-details/document-details.js +2 -138
- package/frontend/src/document-details/document-property/document-property.html +3 -2
- package/frontend/src/edit-date/edit-date.html +8 -6
- package/frontend/src/edit-date/edit-date.js +0 -20
- package/frontend/src/models/document-search/document-search.html +78 -22
- package/frontend/src/models/document-search/document-search.js +64 -2
- package/nest.d.ts +27 -0
- package/nest.js +134 -0
- package/package.json +8 -1
package/frontend/public/tw.css
CHANGED
|
@@ -654,6 +654,11 @@ video {
|
|
|
654
654
|
inset: 0px;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
.inset-x-0 {
|
|
658
|
+
left: 0px;
|
|
659
|
+
right: 0px;
|
|
660
|
+
}
|
|
661
|
+
|
|
657
662
|
.inset-y-0 {
|
|
658
663
|
top: 0px;
|
|
659
664
|
bottom: 0px;
|
|
@@ -779,6 +784,10 @@ video {
|
|
|
779
784
|
z-index: 50;
|
|
780
785
|
}
|
|
781
786
|
|
|
787
|
+
.z-\[10000\] {
|
|
788
|
+
z-index: 10000;
|
|
789
|
+
}
|
|
790
|
+
|
|
782
791
|
.z-\[10001\] {
|
|
783
792
|
z-index: 10001;
|
|
784
793
|
}
|
|
@@ -1025,6 +1034,10 @@ video {
|
|
|
1025
1034
|
display: none;
|
|
1026
1035
|
}
|
|
1027
1036
|
|
|
1037
|
+
.aspect-square {
|
|
1038
|
+
aspect-ratio: 1 / 1;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1028
1041
|
.size-12 {
|
|
1029
1042
|
width: 3rem;
|
|
1030
1043
|
height: 3rem;
|
|
@@ -1112,6 +1125,10 @@ video {
|
|
|
1112
1125
|
height: 2rem;
|
|
1113
1126
|
}
|
|
1114
1127
|
|
|
1128
|
+
.h-9 {
|
|
1129
|
+
height: 2.25rem;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1115
1132
|
.h-\[1\.25em\] {
|
|
1116
1133
|
height: 1.25em;
|
|
1117
1134
|
}
|
|
@@ -1232,10 +1249,18 @@ video {
|
|
|
1232
1249
|
width: 2.5rem;
|
|
1233
1250
|
}
|
|
1234
1251
|
|
|
1252
|
+
.w-11 {
|
|
1253
|
+
width: 2.75rem;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1235
1256
|
.w-12 {
|
|
1236
1257
|
width: 3rem;
|
|
1237
1258
|
}
|
|
1238
1259
|
|
|
1260
|
+
.w-14 {
|
|
1261
|
+
width: 3.5rem;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1239
1264
|
.w-16 {
|
|
1240
1265
|
width: 4rem;
|
|
1241
1266
|
}
|
|
@@ -1304,6 +1329,10 @@ video {
|
|
|
1304
1329
|
width: 16rem;
|
|
1305
1330
|
}
|
|
1306
1331
|
|
|
1332
|
+
.w-7 {
|
|
1333
|
+
width: 1.75rem;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1307
1336
|
.w-8 {
|
|
1308
1337
|
width: 2rem;
|
|
1309
1338
|
}
|
|
@@ -1320,6 +1349,10 @@ video {
|
|
|
1320
1349
|
width: 40%;
|
|
1321
1350
|
}
|
|
1322
1351
|
|
|
1352
|
+
.w-\[400px\] {
|
|
1353
|
+
width: 400px;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1323
1356
|
.w-\[60\%\] {
|
|
1324
1357
|
width: 60%;
|
|
1325
1358
|
}
|
|
@@ -1392,6 +1425,18 @@ video {
|
|
|
1392
1425
|
max-width: 280px;
|
|
1393
1426
|
}
|
|
1394
1427
|
|
|
1428
|
+
.max-w-\[400px\] {
|
|
1429
|
+
max-width: 400px;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
.max-w-\[420px\] {
|
|
1433
|
+
max-width: 420px;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.max-w-\[calc\(100vw-2rem\)\] {
|
|
1437
|
+
max-width: calc(100vw - 2rem);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1395
1440
|
.max-w-\[calc\(100vw-4rem\)\] {
|
|
1396
1441
|
max-width: calc(100vw - 4rem);
|
|
1397
1442
|
}
|
|
@@ -1563,6 +1608,18 @@ video {
|
|
|
1563
1608
|
resize: vertical;
|
|
1564
1609
|
}
|
|
1565
1610
|
|
|
1611
|
+
.snap-y {
|
|
1612
|
+
scroll-snap-type: y var(--tw-scroll-snap-strictness);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.snap-mandatory {
|
|
1616
|
+
--tw-scroll-snap-strictness: mandatory;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.snap-center {
|
|
1620
|
+
scroll-snap-align: center;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1566
1623
|
.list-disc {
|
|
1567
1624
|
list-style-type: disc;
|
|
1568
1625
|
}
|
|
@@ -1581,6 +1638,10 @@ video {
|
|
|
1581
1638
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1582
1639
|
}
|
|
1583
1640
|
|
|
1641
|
+
.grid-cols-7 {
|
|
1642
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1584
1645
|
.grid-cols-\[1fr_auto_1fr\] {
|
|
1585
1646
|
grid-template-columns: 1fr auto 1fr;
|
|
1586
1647
|
}
|
|
@@ -1665,6 +1726,10 @@ video {
|
|
|
1665
1726
|
gap: 2rem;
|
|
1666
1727
|
}
|
|
1667
1728
|
|
|
1729
|
+
.gap-px {
|
|
1730
|
+
gap: 1px;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1668
1733
|
.gap-x-1 {
|
|
1669
1734
|
-moz-column-gap: 0.25rem;
|
|
1670
1735
|
column-gap: 0.25rem;
|
|
@@ -1782,6 +1847,10 @@ video {
|
|
|
1782
1847
|
overflow-y: auto;
|
|
1783
1848
|
}
|
|
1784
1849
|
|
|
1850
|
+
.overscroll-y-contain {
|
|
1851
|
+
overscroll-behavior-y: contain;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1785
1854
|
.truncate {
|
|
1786
1855
|
overflow: hidden;
|
|
1787
1856
|
text-overflow: ellipsis;
|
|
@@ -1886,6 +1955,11 @@ video {
|
|
|
1886
1955
|
border-width: 2px;
|
|
1887
1956
|
}
|
|
1888
1957
|
|
|
1958
|
+
.border-y {
|
|
1959
|
+
border-top-width: 1px;
|
|
1960
|
+
border-bottom-width: 1px;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1889
1963
|
.border-b {
|
|
1890
1964
|
border-bottom-width: 1px;
|
|
1891
1965
|
}
|
|
@@ -2144,6 +2218,11 @@ video {
|
|
|
2144
2218
|
background-color: rgb(176 32 43 / var(--tw-bg-opacity));
|
|
2145
2219
|
}
|
|
2146
2220
|
|
|
2221
|
+
.bg-merlot-800 {
|
|
2222
|
+
--tw-bg-opacity: 1;
|
|
2223
|
+
background-color: rgb(147 30 43 / var(--tw-bg-opacity));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2147
2226
|
.bg-muted {
|
|
2148
2227
|
background-color: var(--color-muted);
|
|
2149
2228
|
}
|
|
@@ -2237,6 +2316,10 @@ video {
|
|
|
2237
2316
|
background-color: var(--color-surface);
|
|
2238
2317
|
}
|
|
2239
2318
|
|
|
2319
|
+
.bg-surface-hover {
|
|
2320
|
+
background-color: var(--color-surface-hover);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2240
2323
|
.bg-teal-600 {
|
|
2241
2324
|
--tw-bg-opacity: 1;
|
|
2242
2325
|
background-color: rgb(0 168 165 / var(--tw-bg-opacity));
|
|
@@ -2617,6 +2700,10 @@ video {
|
|
|
2617
2700
|
font-size: 11px;
|
|
2618
2701
|
}
|
|
2619
2702
|
|
|
2703
|
+
.text-\[9px\] {
|
|
2704
|
+
font-size: 9px;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2620
2707
|
.text-base {
|
|
2621
2708
|
font-size: 1rem;
|
|
2622
2709
|
line-height: 1.5rem;
|
|
@@ -3124,6 +3211,11 @@ video {
|
|
|
3124
3211
|
--tw-ring-color: rgb(22 163 74 / 0.2);
|
|
3125
3212
|
}
|
|
3126
3213
|
|
|
3214
|
+
.ring-merlot-700 {
|
|
3215
|
+
--tw-ring-opacity: 1;
|
|
3216
|
+
--tw-ring-color: rgb(176 32 43 / var(--tw-ring-opacity));
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3127
3219
|
.ring-red-400 {
|
|
3128
3220
|
--tw-ring-opacity: 1;
|
|
3129
3221
|
--tw-ring-color: rgb(248 113 113 / var(--tw-ring-opacity));
|
|
@@ -3205,6 +3297,14 @@ video {
|
|
|
3205
3297
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
3206
3298
|
}
|
|
3207
3299
|
|
|
3300
|
+
.\[-ms-overflow-style\:none\] {
|
|
3301
|
+
-ms-overflow-style: none;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
.\[scrollbar-width\:none\] {
|
|
3305
|
+
scrollbar-width: none;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3208
3308
|
.placeholder\:text-content-tertiary::-moz-placeholder {
|
|
3209
3309
|
color: var(--color-content-tertiary);
|
|
3210
3310
|
}
|
|
@@ -3398,6 +3498,11 @@ video {
|
|
|
3398
3498
|
background-color: rgb(99 102 241 / var(--tw-bg-opacity));
|
|
3399
3499
|
}
|
|
3400
3500
|
|
|
3501
|
+
.hover\:bg-merlot-800:hover {
|
|
3502
|
+
--tw-bg-opacity: 1;
|
|
3503
|
+
background-color: rgb(147 30 43 / var(--tw-bg-opacity));
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3401
3506
|
.hover\:bg-muted:hover {
|
|
3402
3507
|
background-color: var(--color-muted);
|
|
3403
3508
|
}
|
|
@@ -3472,6 +3577,10 @@ video {
|
|
|
3472
3577
|
background-color: rgb(100 116 139 / var(--tw-bg-opacity));
|
|
3473
3578
|
}
|
|
3474
3579
|
|
|
3580
|
+
.hover\:bg-surface-hover:hover {
|
|
3581
|
+
background-color: var(--color-surface-hover);
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3475
3584
|
.hover\:bg-teal-500:hover {
|
|
3476
3585
|
--tw-bg-opacity: 1;
|
|
3477
3586
|
background-color: rgb(0 208 201 / var(--tw-bg-opacity));
|
|
@@ -4145,4 +4254,8 @@ video {
|
|
|
4145
4254
|
:is(:where(.dark) .dark\:text-gray-500) {
|
|
4146
4255
|
--tw-text-opacity: 1;
|
|
4147
4256
|
color: rgb(107 114 128 / var(--tw-text-opacity));
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar {
|
|
4260
|
+
display: none;
|
|
4148
4261
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function pad2(n) {
|
|
4
|
+
return String(n).padStart(2, '0');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function pad3(n) {
|
|
8
|
+
return String(n).padStart(3, '0');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {number} value
|
|
13
|
+
* @param {number} min
|
|
14
|
+
* @param {number} max
|
|
15
|
+
*/
|
|
16
|
+
function clampInt(value, min, max) {
|
|
17
|
+
const n = Math.floor(Number(value));
|
|
18
|
+
if (Number.isNaN(n)) {
|
|
19
|
+
return min;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(max, Math.max(min, n));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Date} date
|
|
26
|
+
* @returns {string} YYYY-MM-DDTHH:mm:ss.SSS
|
|
27
|
+
*/
|
|
28
|
+
function dateToDatetimeLocal(date) {
|
|
29
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
date.getFullYear(),
|
|
34
|
+
'-',
|
|
35
|
+
pad2(date.getMonth() + 1),
|
|
36
|
+
'-',
|
|
37
|
+
pad2(date.getDate()),
|
|
38
|
+
'T',
|
|
39
|
+
pad2(date.getHours()),
|
|
40
|
+
':',
|
|
41
|
+
pad2(date.getMinutes()),
|
|
42
|
+
':',
|
|
43
|
+
pad2(date.getSeconds()),
|
|
44
|
+
'.',
|
|
45
|
+
pad3(date.getMilliseconds())
|
|
46
|
+
].join('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Date} date
|
|
51
|
+
* @returns {string} YYYY-MM-DD
|
|
52
|
+
*/
|
|
53
|
+
function dateToDateOnly(date) {
|
|
54
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
date.getFullYear(),
|
|
59
|
+
'-',
|
|
60
|
+
pad2(date.getMonth() + 1),
|
|
61
|
+
'-',
|
|
62
|
+
pad2(date.getDate())
|
|
63
|
+
].join('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {unknown} value
|
|
68
|
+
* @returns {Date|null}
|
|
69
|
+
*/
|
|
70
|
+
function parseValueToDate(value) {
|
|
71
|
+
if (value == null || value === '') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const date = new Date(value);
|
|
75
|
+
if (Number.isNaN(date.getTime())) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return date;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} localValue - YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.SSS
|
|
83
|
+
* @returns {Date|null}
|
|
84
|
+
*/
|
|
85
|
+
function localStringToDate(localValue) {
|
|
86
|
+
if (!localValue) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const date = new Date(localValue);
|
|
90
|
+
if (Number.isNaN(date.getTime())) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return date;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isSameDay(a, b) {
|
|
97
|
+
if (!a || !b) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return (
|
|
101
|
+
a.getFullYear() === b.getFullYear()
|
|
102
|
+
&& a.getMonth() === b.getMonth()
|
|
103
|
+
&& a.getDate() === b.getDate()
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSameMonth(date, year, month) {
|
|
108
|
+
return date.getFullYear() === year && date.getMonth() === month;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build a 6-row calendar grid for the given month (local timezone).
|
|
113
|
+
* @param {number} year
|
|
114
|
+
* @param {number} month - 0-indexed
|
|
115
|
+
* @returns {Array<{ key: string, label: number, date: Date, inMonth: boolean }>}
|
|
116
|
+
*/
|
|
117
|
+
function buildCalendarDays(year, month) {
|
|
118
|
+
const firstOfMonth = new Date(year, month, 1);
|
|
119
|
+
const startOffset = firstOfMonth.getDay();
|
|
120
|
+
const gridStart = new Date(year, month, 1 - startOffset);
|
|
121
|
+
|
|
122
|
+
const days = [];
|
|
123
|
+
for (let i = 0; i < 42; i++) {
|
|
124
|
+
const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);
|
|
125
|
+
days.push({
|
|
126
|
+
key: `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
127
|
+
label: date.getDate(),
|
|
128
|
+
date,
|
|
129
|
+
inMonth: date.getMonth() === month
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return days;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
136
|
+
|
|
137
|
+
const MONTH_LABELS = [
|
|
138
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
139
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
pad2,
|
|
144
|
+
pad3,
|
|
145
|
+
clampInt,
|
|
146
|
+
dateToDatetimeLocal,
|
|
147
|
+
dateToDateOnly,
|
|
148
|
+
parseValueToDate,
|
|
149
|
+
localStringToDate,
|
|
150
|
+
isSameDay,
|
|
151
|
+
isSameMonth,
|
|
152
|
+
buildCalendarDays,
|
|
153
|
+
WEEKDAY_LABELS,
|
|
154
|
+
MONTH_LABELS
|
|
155
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { dateToDatetimeLocal } = require('./calendar');
|
|
4
|
+
|
|
3
5
|
const { Trie } = require('../models/trie');
|
|
4
6
|
|
|
5
7
|
const QUERY_SELECTORS = [
|
|
@@ -49,7 +51,7 @@ function buildAutocompleteTrie(schemaPaths) {
|
|
|
49
51
|
trie.bulkInsert(QUERY_SELECTORS, 5, 'operator');
|
|
50
52
|
trie.bulkInsert(QUERY_SELECTORS, 5, 'fieldName');
|
|
51
53
|
trie.bulkInsert(VALUE_HELPERS, 5, 'value');
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
if (Array.isArray(schemaPaths) && schemaPaths.length > 0) {
|
|
54
56
|
const paths = schemaPaths
|
|
55
57
|
.map(path => path?.path)
|
|
@@ -61,13 +63,13 @@ function buildAutocompleteTrie(schemaPaths) {
|
|
|
61
63
|
}
|
|
62
64
|
trie.bulkInsert(paths, 10, 'fieldName');
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
return trie;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
function getAutocompleteContext(searchText, cursorPos) {
|
|
69
71
|
const before = searchText.slice(0, cursorPos);
|
|
70
|
-
|
|
72
|
+
|
|
71
73
|
// Check if we're in a field name context (after { or ,)
|
|
72
74
|
// This takes precedence over value context to handle cases like { _id: { $gt
|
|
73
75
|
const fieldMatch = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
@@ -79,7 +81,7 @@ function getAutocompleteContext(searchText, cursorPos) {
|
|
|
79
81
|
startPos: cursorPos - token.length
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
|
-
|
|
84
|
+
|
|
83
85
|
// Check if we're in a value context (after a colon)
|
|
84
86
|
// Match the last colon followed by optional whitespace and capture everything after
|
|
85
87
|
const valueMatch = before.match(/:\s*(\{?\s*)([^\s,\}\]:]*)$/);
|
|
@@ -91,19 +93,19 @@ function getAutocompleteContext(searchText, cursorPos) {
|
|
|
91
93
|
startPos: cursorPos - token.length
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
|
-
|
|
96
|
+
|
|
95
97
|
return null;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
|
|
99
101
|
const context = getAutocompleteContext(searchText, cursorPos);
|
|
100
|
-
|
|
102
|
+
|
|
101
103
|
if (!context) {
|
|
102
104
|
return [];
|
|
103
105
|
}
|
|
104
|
-
|
|
106
|
+
|
|
105
107
|
const { token, role } = context;
|
|
106
|
-
|
|
108
|
+
|
|
107
109
|
// Extract the actual term without quotes
|
|
108
110
|
const leadingQuoteMatch = token.match(/^["']/);
|
|
109
111
|
const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
|
|
@@ -113,14 +115,14 @@ function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
|
|
|
113
115
|
.replace(/^["']/, '')
|
|
114
116
|
.replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
|
|
115
117
|
.trim();
|
|
116
|
-
|
|
118
|
+
|
|
117
119
|
if (!term) {
|
|
118
120
|
return [];
|
|
119
121
|
}
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
const primarySuggestions = trie.getSuggestions(term, 10, role);
|
|
122
124
|
const suggestionsSet = new Set(primarySuggestions);
|
|
123
|
-
|
|
125
|
+
|
|
124
126
|
// Add schema path suggestions for field names
|
|
125
127
|
if (role === 'fieldName' && Array.isArray(schemaPaths) && schemaPaths.length > 0) {
|
|
126
128
|
for (const schemaPath of schemaPaths) {
|
|
@@ -137,9 +139,9 @@ function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
|
-
|
|
142
|
+
|
|
141
143
|
let suggestions = Array.from(suggestionsSet);
|
|
142
|
-
|
|
144
|
+
|
|
143
145
|
// Preserve quotes if present
|
|
144
146
|
if (leadingQuoteMatch) {
|
|
145
147
|
const leadingQuote = leadingQuoteMatch[0];
|
|
@@ -150,14 +152,14 @@ function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
|
|
|
150
152
|
suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
|
|
151
153
|
);
|
|
152
154
|
}
|
|
153
|
-
|
|
155
|
+
|
|
154
156
|
return suggestions;
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
function applySuggestion(searchText, cursorPos, suggestion) {
|
|
158
160
|
const before = searchText.slice(0, cursorPos);
|
|
159
161
|
const after = searchText.slice(cursorPos);
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
// Check if we're in a value context
|
|
162
164
|
const valueMatch = before.match(/:\s*(\{?\s*)([^\s,\}\]:]*)$/);
|
|
163
165
|
if (valueMatch) {
|
|
@@ -165,30 +167,30 @@ function applySuggestion(searchText, cursorPos, suggestion) {
|
|
|
165
167
|
const start = cursorPos - token.length;
|
|
166
168
|
let replacement = suggestion;
|
|
167
169
|
let cursorOffset = replacement.length;
|
|
168
|
-
|
|
170
|
+
|
|
169
171
|
// Add parentheses for function helpers and position cursor inside
|
|
170
172
|
if (FUNCTION_HELPERS.has(suggestion)) {
|
|
171
173
|
replacement = `${suggestion}()`;
|
|
172
174
|
cursorOffset = suggestion.length + 1; // Position cursor between ()
|
|
173
175
|
}
|
|
174
|
-
|
|
176
|
+
|
|
175
177
|
return {
|
|
176
178
|
text: searchText.slice(0, start) + replacement + after,
|
|
177
179
|
newCursorPos: start + cursorOffset
|
|
178
180
|
};
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
|
|
181
183
|
// Check if we're in a field name context
|
|
182
184
|
const fieldMatch = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
183
185
|
if (fieldMatch) {
|
|
184
186
|
const token = fieldMatch[1];
|
|
185
187
|
const start = cursorPos - token.length;
|
|
186
188
|
let replacement = suggestion;
|
|
187
|
-
|
|
189
|
+
|
|
188
190
|
const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
|
|
189
191
|
const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
|
|
190
192
|
const colonNeeded = !/^\s*:/.test(after);
|
|
191
|
-
|
|
193
|
+
|
|
192
194
|
// If suggestion already has quotes, use it as-is
|
|
193
195
|
const suggestionHasQuotes = (suggestion.startsWith('"') || suggestion.startsWith('\'')) &&
|
|
194
196
|
(suggestion.endsWith('"') || suggestion.endsWith('\''));
|
|
@@ -202,27 +204,107 @@ function applySuggestion(searchText, cursorPos, suggestion) {
|
|
|
202
204
|
replacement = `${replacement}${trailingQuote}`;
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
|
-
|
|
207
|
+
|
|
206
208
|
// Only insert : if we know the user isn't entering in a nested path
|
|
207
209
|
// If suggestion has full quotes or user typed both quotes, add colon
|
|
208
210
|
if (colonNeeded && (suggestionHasQuotes || !leadingQuote || trailingQuote)) {
|
|
209
211
|
replacement = `${replacement}:`;
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
|
|
212
214
|
return {
|
|
213
215
|
text: searchText.slice(0, start) + replacement + after,
|
|
214
216
|
newCursorPos: start + replacement.length
|
|
215
217
|
};
|
|
216
218
|
}
|
|
217
|
-
|
|
219
|
+
|
|
218
220
|
return null;
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
/**
|
|
224
|
+
* When the cursor is inside a Date(…) or new Date(…) argument, returns the
|
|
225
|
+
* slice indices of that argument so a picker can replace it with a quoted ISO string.
|
|
226
|
+
*/
|
|
227
|
+
function getDatePickerInsertionRange(searchText, cursorPos) {
|
|
228
|
+
const before = searchText.slice(0, cursorPos);
|
|
229
|
+
const re = /((?:new\s+)?Date\s*\(\s*)([^)]*)$/i;
|
|
230
|
+
const m = before.match(re);
|
|
231
|
+
if (!m) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const innerStart = m.index + m[1].length;
|
|
235
|
+
const after = searchText.slice(cursorPos);
|
|
236
|
+
let closeIdx = -1;
|
|
237
|
+
let parenDepth = 0;
|
|
238
|
+
for (let k = 0; k < after.length; k++) {
|
|
239
|
+
const ch = after[k];
|
|
240
|
+
if (ch === '(') {
|
|
241
|
+
parenDepth++;
|
|
242
|
+
} else if (ch === ')') {
|
|
243
|
+
if (parenDepth === 0) {
|
|
244
|
+
closeIdx = cursorPos + k;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
parenDepth--;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const innerEnd = closeIdx >= 0 ? closeIdx : cursorPos;
|
|
251
|
+
const needsClosingParen = closeIdx < 0;
|
|
252
|
+
return { innerStart, innerEnd, needsClosingParen };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function dateArgumentSliceToDatetimeLocal(slice) {
|
|
256
|
+
const t = slice.trim();
|
|
257
|
+
if (!t) {
|
|
258
|
+
return '';
|
|
259
|
+
}
|
|
260
|
+
const unquoted = (t.startsWith('"') && t.endsWith('"')) || (t.startsWith('\'') && t.endsWith('\''))
|
|
261
|
+
? t.slice(1, -1)
|
|
262
|
+
: t;
|
|
263
|
+
const d = new Date(unquoted);
|
|
264
|
+
if (Number.isNaN(d.getTime())) {
|
|
265
|
+
return '';
|
|
266
|
+
}
|
|
267
|
+
return dateToDatetimeLocal(d);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {Date} date
|
|
272
|
+
* @param {'quoted'|'timestamp'} format
|
|
273
|
+
*/
|
|
274
|
+
function formatDateArgumentValue(date, format) {
|
|
275
|
+
if (format === 'quoted') {
|
|
276
|
+
return JSON.stringify(date.toISOString());
|
|
277
|
+
}
|
|
278
|
+
return String(date.getTime());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Inserts a Date argument value as timestamp or quoted ISO string.
|
|
283
|
+
* @param {string} searchText
|
|
284
|
+
* @param {{ innerStart: number, innerEnd: number, needsClosingParen?: boolean }} range
|
|
285
|
+
* @param {Date} date
|
|
286
|
+
* @param {'quoted'|'timestamp'} [format] - when omitted, inferred from the existing argument
|
|
287
|
+
*/
|
|
288
|
+
function insertDateInDateArgument(searchText, range, date, format) {
|
|
289
|
+
const slice = searchText.slice(range.innerStart, range.innerEnd);
|
|
290
|
+
const insertion = formatDateArgumentValue(date, format);
|
|
291
|
+
const { innerStart, innerEnd } = range;
|
|
292
|
+
const closing = range.needsClosingParen === true ? ')' : '';
|
|
293
|
+
return {
|
|
294
|
+
text: searchText.slice(0, innerStart) + insertion + closing + searchText.slice(innerEnd),
|
|
295
|
+
newCursorPos: innerStart + insertion.length + closing.length
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
221
299
|
module.exports = {
|
|
222
300
|
buildAutocompleteTrie,
|
|
223
301
|
getAutocompleteContext,
|
|
224
302
|
getAutocompleteSuggestions,
|
|
225
303
|
applySuggestion,
|
|
304
|
+
getDatePickerInsertionRange,
|
|
305
|
+
dateArgumentSliceToDatetimeLocal,
|
|
306
|
+
formatDateArgumentValue,
|
|
307
|
+
insertDateInDateArgument,
|
|
226
308
|
QUERY_SELECTORS,
|
|
227
309
|
VALUE_HELPERS,
|
|
228
310
|
FUNCTION_HELPERS
|