@mongoosejs/studio 0.2.12 → 0.2.13

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.
@@ -647,10 +647,22 @@ video {
647
647
  left: -0.75rem;
648
648
  }
649
649
 
650
+ .bottom-4 {
651
+ bottom: 1rem;
652
+ }
653
+
654
+ .bottom-full {
655
+ bottom: 100%;
656
+ }
657
+
650
658
  .left-0 {
651
659
  left: 0px;
652
660
  }
653
661
 
662
+ .left-4 {
663
+ left: 1rem;
664
+ }
665
+
654
666
  .left-full {
655
667
  left: 100%;
656
668
  }
@@ -1002,6 +1014,10 @@ video {
1002
1014
  height: 1.25em;
1003
1015
  }
1004
1016
 
1017
+ .h-\[240px\] {
1018
+ height: 240px;
1019
+ }
1020
+
1005
1021
  .h-\[300px\] {
1006
1022
  height: 300px;
1007
1023
  }
@@ -1058,10 +1074,6 @@ video {
1058
1074
  width: 12rem !important;
1059
1075
  }
1060
1076
 
1061
- .\!w-64 {
1062
- width: 16rem !important;
1063
- }
1064
-
1065
1077
  .\!w-\[90vw\] {
1066
1078
  width: 90vw !important;
1067
1079
  }
@@ -1122,6 +1134,10 @@ video {
1122
1134
  width: 2rem;
1123
1135
  }
1124
1136
 
1137
+ .w-\[240px\] {
1138
+ width: 240px;
1139
+ }
1140
+
1125
1141
  .w-\[50\%\] {
1126
1142
  width: 50%;
1127
1143
  }
@@ -1138,6 +1154,10 @@ video {
1138
1154
  min-width: 140px;
1139
1155
  }
1140
1156
 
1157
+ .min-w-\[220px\] {
1158
+ min-width: 220px;
1159
+ }
1160
+
1141
1161
  .min-w-\[80px\] {
1142
1162
  min-width: 80px;
1143
1163
  }
@@ -1150,6 +1170,10 @@ video {
1150
1170
  max-width: 28rem !important;
1151
1171
  }
1152
1172
 
1173
+ .max-w-4xl {
1174
+ max-width: 56rem;
1175
+ }
1176
+
1153
1177
  .max-w-5xl {
1154
1178
  max-width: 64rem;
1155
1179
  }
@@ -1206,6 +1230,11 @@ video {
1206
1230
  transform-origin: top right;
1207
1231
  }
1208
1232
 
1233
+ .-translate-x-full {
1234
+ --tw-translate-x: -100%;
1235
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1236
+ }
1237
+
1209
1238
  .-translate-y-1\/2 {
1210
1239
  --tw-translate-y: -50%;
1211
1240
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1221,6 +1250,11 @@ video {
1221
1250
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1222
1251
  }
1223
1252
 
1253
+ .translate-y-1 {
1254
+ --tw-translate-y: 0.25rem;
1255
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1256
+ }
1257
+
1224
1258
  .rotate-0 {
1225
1259
  --tw-rotate: 0deg;
1226
1260
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1348,6 +1382,10 @@ video {
1348
1382
  gap: 1rem;
1349
1383
  }
1350
1384
 
1385
+ .gap-6 {
1386
+ gap: 1.5rem;
1387
+ }
1388
+
1351
1389
  .gap-8 {
1352
1390
  gap: 2rem;
1353
1391
  }
@@ -1563,6 +1601,10 @@ video {
1563
1601
  border-bottom-width: 4px;
1564
1602
  }
1565
1603
 
1604
+ .border-l-0 {
1605
+ border-left-width: 0px;
1606
+ }
1607
+
1566
1608
  .border-l-\[3px\] {
1567
1609
  border-left-width: 3px;
1568
1610
  }
@@ -1636,6 +1678,11 @@ video {
1636
1678
  border-color: rgb(241 245 249 / var(--tw-border-opacity));
1637
1679
  }
1638
1680
 
1681
+ .border-slate-300 {
1682
+ --tw-border-opacity: 1;
1683
+ border-color: rgb(203 213 225 / var(--tw-border-opacity));
1684
+ }
1685
+
1639
1686
  .border-transparent {
1640
1687
  border-color: transparent;
1641
1688
  }
@@ -1645,6 +1692,11 @@ video {
1645
1692
  border-color: rgb(63 83 255 / var(--tw-border-opacity));
1646
1693
  }
1647
1694
 
1695
+ .border-ultramarine-600 {
1696
+ --tw-border-opacity: 1;
1697
+ border-color: rgb(24 35 255 / var(--tw-border-opacity));
1698
+ }
1699
+
1648
1700
  .border-yellow-200 {
1649
1701
  --tw-border-opacity: 1;
1650
1702
  border-color: rgb(254 240 138 / var(--tw-border-opacity));
@@ -1774,6 +1826,11 @@ video {
1774
1826
  background-color: rgb(220 252 231 / var(--tw-bg-opacity));
1775
1827
  }
1776
1828
 
1829
+ .bg-green-200 {
1830
+ --tw-bg-opacity: 1;
1831
+ background-color: rgb(187 247 208 / var(--tw-bg-opacity));
1832
+ }
1833
+
1777
1834
  .bg-green-50 {
1778
1835
  --tw-bg-opacity: 1;
1779
1836
  background-color: rgb(240 253 244 / var(--tw-bg-opacity));
@@ -1794,6 +1851,11 @@ video {
1794
1851
  background-color: rgb(254 226 226 / var(--tw-bg-opacity));
1795
1852
  }
1796
1853
 
1854
+ .bg-red-200 {
1855
+ --tw-bg-opacity: 1;
1856
+ background-color: rgb(254 202 202 / var(--tw-bg-opacity));
1857
+ }
1858
+
1797
1859
  .bg-red-400 {
1798
1860
  --tw-bg-opacity: 1;
1799
1861
  background-color: rgb(248 113 113 / var(--tw-bg-opacity));
@@ -1819,6 +1881,11 @@ video {
1819
1881
  background-color: rgb(241 245 249 / var(--tw-bg-opacity));
1820
1882
  }
1821
1883
 
1884
+ .bg-slate-200 {
1885
+ --tw-bg-opacity: 1;
1886
+ background-color: rgb(226 232 240 / var(--tw-bg-opacity));
1887
+ }
1888
+
1822
1889
  .bg-slate-300 {
1823
1890
  --tw-bg-opacity: 1;
1824
1891
  background-color: rgb(203 213 225 / var(--tw-bg-opacity));
@@ -1839,6 +1906,11 @@ video {
1839
1906
  background-color: rgb(71 85 105 / var(--tw-bg-opacity));
1840
1907
  }
1841
1908
 
1909
+ .bg-slate-900 {
1910
+ --tw-bg-opacity: 1;
1911
+ background-color: rgb(15 23 42 / var(--tw-bg-opacity));
1912
+ }
1913
+
1842
1914
  .bg-teal-600 {
1843
1915
  --tw-bg-opacity: 1;
1844
1916
  background-color: rgb(0 168 165 / var(--tw-bg-opacity));
@@ -1883,6 +1955,11 @@ video {
1883
1955
  background-color: rgb(254 249 195 / var(--tw-bg-opacity));
1884
1956
  }
1885
1957
 
1958
+ .bg-yellow-200 {
1959
+ --tw-bg-opacity: 1;
1960
+ background-color: rgb(254 240 138 / var(--tw-bg-opacity));
1961
+ }
1962
+
1886
1963
  .bg-yellow-400 {
1887
1964
  --tw-bg-opacity: 1;
1888
1965
  background-color: rgb(250 204 21 / var(--tw-bg-opacity));
@@ -2062,6 +2139,11 @@ video {
2062
2139
  padding-bottom: 2rem;
2063
2140
  }
2064
2141
 
2142
+ .py-\[2px\] {
2143
+ padding-top: 2px;
2144
+ padding-bottom: 2px;
2145
+ }
2146
+
2065
2147
  .pb-16 {
2066
2148
  padding-bottom: 4rem;
2067
2149
  }
@@ -2198,10 +2280,6 @@ video {
2198
2280
  font-weight: 500;
2199
2281
  }
2200
2282
 
2201
- .font-normal {
2202
- font-weight: 400;
2203
- }
2204
-
2205
2283
  .font-semibold {
2206
2284
  font-weight: 600;
2207
2285
  }
@@ -2319,6 +2397,11 @@ video {
2319
2397
  color: rgb(22 101 52 / var(--tw-text-opacity));
2320
2398
  }
2321
2399
 
2400
+ .text-green-900 {
2401
+ --tw-text-opacity: 1;
2402
+ color: rgb(20 83 45 / var(--tw-text-opacity));
2403
+ }
2404
+
2322
2405
  .text-purple-600 {
2323
2406
  --tw-text-opacity: 1;
2324
2407
  color: rgb(147 51 234 / var(--tw-text-opacity));
@@ -2344,6 +2427,11 @@ video {
2344
2427
  color: rgb(153 27 27 / var(--tw-text-opacity));
2345
2428
  }
2346
2429
 
2430
+ .text-red-900 {
2431
+ --tw-text-opacity: 1;
2432
+ color: rgb(127 29 29 / var(--tw-text-opacity));
2433
+ }
2434
+
2347
2435
  .text-sky-600 {
2348
2436
  --tw-text-opacity: 1;
2349
2437
  color: rgb(2 132 199 / var(--tw-text-opacity));
@@ -2359,11 +2447,26 @@ video {
2359
2447
  color: rgb(7 89 133 / var(--tw-text-opacity));
2360
2448
  }
2361
2449
 
2450
+ .text-slate-200 {
2451
+ --tw-text-opacity: 1;
2452
+ color: rgb(226 232 240 / var(--tw-text-opacity));
2453
+ }
2454
+
2455
+ .text-slate-300 {
2456
+ --tw-text-opacity: 1;
2457
+ color: rgb(203 213 225 / var(--tw-text-opacity));
2458
+ }
2459
+
2362
2460
  .text-slate-400 {
2363
2461
  --tw-text-opacity: 1;
2364
2462
  color: rgb(148 163 184 / var(--tw-text-opacity));
2365
2463
  }
2366
2464
 
2465
+ .text-slate-50 {
2466
+ --tw-text-opacity: 1;
2467
+ color: rgb(248 250 252 / var(--tw-text-opacity));
2468
+ }
2469
+
2367
2470
  .text-slate-500 {
2368
2471
  --tw-text-opacity: 1;
2369
2472
  color: rgb(100 116 139 / var(--tw-text-opacity));
@@ -2384,6 +2487,11 @@ video {
2384
2487
  color: rgb(30 41 59 / var(--tw-text-opacity));
2385
2488
  }
2386
2489
 
2490
+ .text-slate-900 {
2491
+ --tw-text-opacity: 1;
2492
+ color: rgb(15 23 42 / var(--tw-text-opacity));
2493
+ }
2494
+
2387
2495
  .text-ultramarine-600 {
2388
2496
  --tw-text-opacity: 1;
2389
2497
  color: rgb(24 35 255 / var(--tw-text-opacity));
@@ -2424,6 +2532,11 @@ video {
2424
2532
  color: rgb(133 77 14 / var(--tw-text-opacity));
2425
2533
  }
2426
2534
 
2535
+ .text-yellow-900 {
2536
+ --tw-text-opacity: 1;
2537
+ color: rgb(113 63 18 / var(--tw-text-opacity));
2538
+ }
2539
+
2427
2540
  .underline {
2428
2541
  text-decoration-line: underline;
2429
2542
  }
@@ -2556,6 +2669,11 @@ video {
2556
2669
  --tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
2557
2670
  }
2558
2671
 
2672
+ .ring-gray-500 {
2673
+ --tw-ring-opacity: 1;
2674
+ --tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
2675
+ }
2676
+
2559
2677
  .ring-gray-600\/20 {
2560
2678
  --tw-ring-color: rgb(75 85 99 / 0.2);
2561
2679
  }
@@ -2569,6 +2687,11 @@ video {
2569
2687
  --tw-ring-color: rgb(74 222 128 / var(--tw-ring-opacity));
2570
2688
  }
2571
2689
 
2690
+ .ring-green-500 {
2691
+ --tw-ring-opacity: 1;
2692
+ --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity));
2693
+ }
2694
+
2572
2695
  .ring-green-600\/20 {
2573
2696
  --tw-ring-color: rgb(22 163 74 / 0.2);
2574
2697
  }
@@ -2578,11 +2701,26 @@ video {
2578
2701
  --tw-ring-color: rgb(248 113 113 / var(--tw-ring-opacity));
2579
2702
  }
2580
2703
 
2704
+ .ring-red-500 {
2705
+ --tw-ring-opacity: 1;
2706
+ --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity));
2707
+ }
2708
+
2709
+ .ring-slate-500 {
2710
+ --tw-ring-opacity: 1;
2711
+ --tw-ring-color: rgb(100 116 139 / var(--tw-ring-opacity));
2712
+ }
2713
+
2581
2714
  .ring-yellow-400 {
2582
2715
  --tw-ring-opacity: 1;
2583
2716
  --tw-ring-color: rgb(250 204 21 / var(--tw-ring-opacity));
2584
2717
  }
2585
2718
 
2719
+ .ring-yellow-500 {
2720
+ --tw-ring-opacity: 1;
2721
+ --tw-ring-color: rgb(234 179 8 / var(--tw-ring-opacity));
2722
+ }
2723
+
2586
2724
  .ring-opacity-5 {
2587
2725
  --tw-ring-opacity: 0.05;
2588
2726
  }
@@ -3258,6 +3396,28 @@ video {
3258
3396
  opacity: 0.5;
3259
3397
  }
3260
3398
 
3399
+ .group:focus-within .group-focus-within\:pointer-events-auto {
3400
+ pointer-events: auto;
3401
+ }
3402
+
3403
+ .group:focus-within .group-focus-within\:translate-y-0 {
3404
+ --tw-translate-y: 0px;
3405
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
3406
+ }
3407
+
3408
+ .group:focus-within .group-focus-within\:opacity-100 {
3409
+ opacity: 1;
3410
+ }
3411
+
3412
+ .group:hover .group-hover\:pointer-events-auto {
3413
+ pointer-events: auto;
3414
+ }
3415
+
3416
+ .group:hover .group-hover\:translate-y-0 {
3417
+ --tw-translate-y: 0px;
3418
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
3419
+ }
3420
+
3261
3421
  .group:hover .group-hover\:text-gray-700 {
3262
3422
  --tw-text-opacity: 1;
3263
3423
  color: rgb(55 65 81 / var(--tw-text-opacity));
@@ -3394,6 +3554,10 @@ video {
3394
3554
  gap: 1.5rem;
3395
3555
  }
3396
3556
 
3557
+ .md\:p-8 {
3558
+ padding: 2rem;
3559
+ }
3560
+
3397
3561
  .md\:px-0 {
3398
3562
  padding-left: 0px;
3399
3563
  padding-right: 0px;
@@ -3410,12 +3574,12 @@ video {
3410
3574
  margin-right: -2rem;
3411
3575
  }
3412
3576
 
3413
- .lg\:w-48 {
3414
- width: 12rem;
3577
+ .lg\:hidden {
3578
+ display: none;
3415
3579
  }
3416
3580
 
3417
- .lg\:w-64 {
3418
- width: 16rem;
3581
+ .lg\:w-48 {
3582
+ width: 12rem;
3419
3583
  }
3420
3584
 
3421
3585
  .lg\:max-w-\[calc\(100vw-15rem\)\] {
@@ -3426,6 +3590,11 @@ video {
3426
3590
  max-width: calc(100vw - 20rem);
3427
3591
  }
3428
3592
 
3593
+ .lg\:translate-x-0 {
3594
+ --tw-translate-x: 0px;
3595
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
3596
+ }
3597
+
3429
3598
  .lg\:px-8 {
3430
3599
  padding-left: 2rem;
3431
3600
  padding-right: 2rem;
@@ -135,6 +135,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
135
135
  yield { document: doc };
136
136
  }
137
137
  },
138
+ getEstimatedDocumentCounts: function getEstimatedDocumentCounts() {
139
+ return client.post('', { action: 'Model.getEstimatedDocumentCounts' }).then(res => res.data);
140
+ },
138
141
  streamDocumentChanges: async function* streamDocumentChanges(params, options = {}) {
139
142
  const pollIntervalMs = 5000;
140
143
  while (!options.signal?.aborted) {
@@ -385,6 +388,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
385
388
  }
386
389
  }
387
390
  },
391
+ getEstimatedDocumentCounts: function getEstimatedDocumentCounts() {
392
+ return client.post('/Model/getEstimatedDocumentCounts', {}).then(res => res.data);
393
+ },
388
394
  streamDocumentChanges: async function* streamDocumentChanges(params, options = {}) {
389
395
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
390
396
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/streamDocumentChanges?' + new URLSearchParams(params).toString();
@@ -1,10 +1,13 @@
1
1
  <div class="flex" style="height: calc(100vh - 55px); height: calc(100dvh - 55px)">
2
- <div
3
- class="fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10"
2
+ <button
3
+ type="button"
4
+ class="fixed top-[65px] left-0 cursor-pointer bg-gray-100 rounded-r-md z-10 p-1 lg:hidden"
4
5
  @click="hideSidebar = false"
5
- v-show="hideSidebar">
6
- <svg xmlns="http://www.w3.org/2000/svg" style="h-5 w-5" viewBox="0 -960 960 960" class="w-5" fill="#5f6368"><path d="M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z"/></svg>
7
- </div>
6
+ v-show="hideSidebar !== false"
7
+ aria-label="Open chat history"
8
+ title="Open chat history">
9
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" class="w-5 h-5" fill="#5f6368"><path d="M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z"/></svg>
10
+ </button>
8
11
  <button
9
12
  class="fixed top-[65px] right-4 z-10 p-2 rounded-md shadow bg-white"
10
13
  :class="hasWorkspace ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 cursor-not-allowed bg-gray-50'"
@@ -17,12 +20,15 @@
17
20
  <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
18
21
  </button>
19
22
  <!-- Sidebar: Chat Threads -->
20
- <aside class="border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative" :class="hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''">
23
+ <aside
24
+ class="bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-transform duration-300 ease-in-out z-20 w-64 fixed lg:relative"
25
+ :class="hideSidebar === false ? 'translate-x-0' : hideSidebar === true ? '-translate-x-full lg:hidden' : '-translate-x-full lg:translate-x-0'"
26
+ style="z-index: 10000">
21
27
  <div class="flex items-center border-b border-gray-100 w-64 overflow-x-hidden">
22
28
  <div class="p-1 ml-2 font-bold">Chat Threads</div>
23
29
  <button
24
30
  @click="hideSidebar = true"
25
- class="ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none"
31
+ class="ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none lg:hidden"
26
32
  aria-label="Close sidebar"
27
33
  >
28
34
  <svg xmlns="http://www.w3.org/2000/svg" style="h-5 w-5" viewBox="0 -960 960 960" class="w-5" fill="currentColor"><path d="M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z"/></svg>
@@ -289,5 +289,31 @@
289
289
  @close="closeScriptDrawer"
290
290
  @refresh="handleScriptRefresh"
291
291
  ></execute-script>
292
+ <div
293
+ v-if="keyboardShortcuts.length > 0"
294
+ class="fixed bottom-4 left-4 z-40 group"
295
+ >
296
+ <button
297
+ type="button"
298
+ aria-label="Keyboard shortcuts"
299
+ title="Keyboard shortcuts"
300
+ class="rounded-full border border-slate-300 bg-white p-2 text-slate-700 shadow-sm transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-ultramarine-500"
301
+ >
302
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
303
+ <rect x="2.5" y="6.5" width="19" height="11" rx="2" stroke-width="1.5"></rect>
304
+ <path d="M6 10.5h1.5M9 10.5h1.5M12 10.5h1.5M15 10.5h1.5M18 10.5h.01M6 13.5h1.5M9 13.5h6M16.5 13.5h1.5" stroke-width="1.5" stroke-linecap="round"></path>
305
+ </svg>
306
+ </button>
307
+ <div class="pointer-events-none absolute left-0 bottom-full mb-2 min-w-[220px] translate-y-1 rounded-lg bg-slate-900 px-3 py-2.5 text-slate-200 opacity-0 shadow-xl transition-all duration-150 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-y-0 group-focus-within:opacity-100">
308
+ <div
309
+ v-for="shortcut in keyboardShortcuts"
310
+ :key="shortcut.command + shortcut.description"
311
+ class="flex items-center justify-between gap-3 text-xs"
312
+ >
313
+ <span class="font-mono font-semibold text-slate-50">{{shortcut.command}}</span>
314
+ <span class="text-slate-300">{{shortcut.description}}</span>
315
+ </div>
316
+ </div>
317
+ </div>
292
318
  </div>
293
319
  </div>
@@ -40,11 +40,17 @@ module.exports = app => app.component('document', {
40
40
  }),
41
41
  async mounted() {
42
42
  window.pageState = this;
43
+ if (typeof window !== 'undefined' && window.addEventListener) {
44
+ window.addEventListener('keydown', this.handleSaveShortcut);
45
+ }
43
46
  // Store query parameters from the route (preserved from models page)
44
47
  this.previousQuery = Object.assign({}, this.$route.query);
45
48
  await this.refreshDocument({ force: true, source: 'initial' });
46
49
  },
47
- beforeDestroy() {
50
+ beforeUnmount() {
51
+ if (typeof window !== 'undefined' && window.removeEventListener) {
52
+ window.removeEventListener('keydown', this.handleSaveShortcut);
53
+ }
48
54
  this.stopAutoRefresh();
49
55
  },
50
56
  computed: {
@@ -74,6 +80,13 @@ module.exports = app => app.component('document', {
74
80
  canEdit() {
75
81
  return this.canManipulate && this.viewMode === 'fields';
76
82
  },
83
+ keyboardShortcuts() {
84
+ const shortcuts = [];
85
+ if (this.editting && this.canManipulate) {
86
+ shortcuts.push({ command: 'Ctrl + S', description: 'Save document' });
87
+ }
88
+ return shortcuts;
89
+ },
77
90
  isLambda() {
78
91
  return !!window?.MONGOOSE_STUDIO_CONFIG?.isLambda;
79
92
  }
@@ -86,6 +99,19 @@ module.exports = app => app.component('document', {
86
99
  }
87
100
  },
88
101
  methods: {
102
+ handleSaveShortcut(event) {
103
+ const key = typeof event?.key === 'string' ? event.key.toLowerCase() : '';
104
+ const isSaveShortcut = (event.ctrlKey || event.metaKey) && key === 's';
105
+ if (!isSaveShortcut) {
106
+ return;
107
+ }
108
+ if (!this.editting || !this.canManipulate) {
109
+ return;
110
+ }
111
+
112
+ event.preventDefault();
113
+ this.shouldShowConfirmModal = true;
114
+ },
89
115
  cancelEdit() {
90
116
  this.changes = {};
91
117
  this.editting = false;
@@ -20,9 +20,15 @@
20
20
  <li v-for="model in models">
21
21
  <router-link
22
22
  :to="'/model/' + model"
23
- class="block truncate rounded-md py-2 pr-2 pl-2 text-sm text-gray-700"
23
+ class="flex items-center gap-2 rounded-md py-2 pr-2 pl-2 text-sm text-gray-700"
24
24
  :class="model === currentModel ? 'bg-ultramarine-100 font-bold text-gray-900' : 'hover:bg-ultramarine-50'">
25
- {{model}}
25
+ <span class="truncate">{{model}}</span>
26
+ <span
27
+ v-if="modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel"
28
+ class="ml-auto text-xs text-gray-500 bg-gray-100 rounded-md px-1 py-[2px]"
29
+ >
30
+ {{formatCompactCount(modelDocumentCounts[model])}}
31
+ </span>
26
32
  </router-link>
27
33
  </li>
28
34
  </ul>
@@ -20,6 +20,7 @@ module.exports = app => app.component('models', {
20
20
  data: () => ({
21
21
  models: [],
22
22
  currentModel: null,
23
+ modelDocumentCounts: {},
23
24
  documents: [],
24
25
  schemaPaths: [],
25
26
  filteredPaths: [],
@@ -84,6 +85,7 @@ module.exports = app => app.component('models', {
84
85
  document.addEventListener('click', this.onOutsideActionsMenuClick, true);
85
86
  const { models, readyState } = await api.Model.listModels();
86
87
  this.models = models;
88
+ await this.loadModelCounts();
87
89
  if (this.currentModel == null && this.models.length > 0) {
88
90
  this.currentModel = this.models[0];
89
91
  }
@@ -589,6 +591,25 @@ module.exports = app => app.component('models', {
589
591
 
590
592
  return value.toLocaleString();
591
593
  },
594
+ formatCompactCount(value) {
595
+ if (typeof value !== 'number') {
596
+ return '—';
597
+ }
598
+ if (value < 1000) {
599
+ return `${value}`;
600
+ }
601
+ const formatValue = (number, suffix) => {
602
+ const rounded = (Math.round(number * 10) / 10).toFixed(1).replace(/\.0$/, '');
603
+ return `${rounded}${suffix}`;
604
+ };
605
+ if (value < 1000000) {
606
+ return formatValue(value / 1000, 'k');
607
+ }
608
+ if (value < 1000000000) {
609
+ return formatValue(value / 1000000, 'M');
610
+ }
611
+ return formatValue(value / 1000000000, 'B');
612
+ },
592
613
  checkIndexLocation(indexName) {
593
614
  if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
594
615
  return 'text-gray-500';
@@ -842,6 +863,19 @@ module.exports = app => app.component('models', {
842
863
  } else {
843
864
  this.selectMultiple = true;
844
865
  }
866
+ },
867
+ async loadModelCounts() {
868
+ if (!Array.isArray(this.models) || this.models.length === 0) {
869
+ return;
870
+ }
871
+ try {
872
+ const { counts } = await api.Model.getEstimatedDocumentCounts();
873
+ if (counts && typeof counts === 'object') {
874
+ this.modelDocumentCounts = counts;
875
+ }
876
+ } catch (err) {
877
+ console.error('Failed to load model document counts', err);
878
+ }
845
879
  }
846
880
  }
847
881
  });
@@ -42,7 +42,7 @@ module.exports = app => app.component('navbar', {
42
42
  return ['chat index', 'chat'].includes(this.$route.name);
43
43
  },
44
44
  taskView() {
45
- return ['tasks'].includes(this.$route.name);
45
+ return ['tasks', 'taskByName', 'taskSingle'].includes(this.$route.name);
46
46
  },
47
47
  routeName() {
48
48
  return this.$route.name;
@@ -2,14 +2,14 @@
2
2
 
3
3
  // Role-based access control configuration
4
4
  const roleAccess = {
5
- owner: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat'],
6
- admin: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat'],
7
- member: ['root', 'model', 'document', 'dashboards', 'dashboard', 'chat'],
5
+ owner: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat', 'tasks', 'taskByName', 'taskSingle'],
6
+ admin: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat', 'tasks', 'taskByName', 'taskSingle'],
7
+ member: ['root', 'model', 'document', 'dashboards', 'dashboard', 'chat', 'tasks', 'taskByName', 'taskSingle'],
8
8
  readonly: ['root', 'model', 'document', 'chat'],
9
9
  dashboards: ['dashboards', 'dashboard']
10
10
  };
11
11
 
12
- const allowedRoutesForLocalDev = ['document', 'root', 'chat', 'model'];
12
+ const allowedRoutesForLocalDev = ['document', 'root', 'chat', 'model', 'tasks', 'taskByName', 'taskSingle'];
13
13
 
14
14
  // Helper function to check if a role has access to a route
15
15
  function hasAccess(roles, routeName) {
@@ -76,6 +76,22 @@ module.exports = {
76
76
  authorized: true
77
77
  }
78
78
  },
79
+ {
80
+ path: '/tasks/:name',
81
+ name: 'taskByName',
82
+ component: 'task-by-name',
83
+ meta: {
84
+ authorized: true
85
+ }
86
+ },
87
+ {
88
+ path: '/tasks/:name/:id',
89
+ name: 'taskSingle',
90
+ component: 'task-single',
91
+ meta: {
92
+ authorized: true
93
+ }
94
+ },
79
95
  {
80
96
  path: '/chat',
81
97
  name: 'chat index',