latticesql 1.16.0 → 1.16.2
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 +9 -1
- package/dist/cli.js +391 -303
- package/dist/index.cjs +10 -0
- package/dist/index.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2300,7 +2300,15 @@ lattice teams join \
|
|
|
2300
2300
|
|
|
2301
2301
|
The cloud rejects redemption if the caller's claimed email doesn't match the invitation's `invitee_email` (case-insensitive). Sharing an invite token in a public channel is therefore safe — only the addressee can redeem it.
|
|
2302
2302
|
|
|
2303
|
-
**Other subcommands** (`lattice teams help` for the full list): `list`, `members`, `leave`, `destroy`, `share`, `unshare`, `shared`, `sync`, `link`, `unlink`, `pull`, `push`, `status`.
|
|
2303
|
+
**Other subcommands** (`lattice teams help` for the full list): `list`, `members`, `leave`, `destroy`, `share`, `unshare`, `shared`, `sync`, `link`, `unlink`, `pull`, `push`, `status`, `dlq`.
|
|
2304
|
+
|
|
2305
|
+
**Dead-letter queue (v1.15+).** A pulled change envelope that fails to apply (e.g. it arrived before the row/table it depends on), and any non-owner-overwrite divergence notice, lands in `__lattice_team_dlq`. Inspect and recover it instead of losing it behind the pull cursor:
|
|
2306
|
+
|
|
2307
|
+
```bash
|
|
2308
|
+
lattice teams dlq list --team <name> # show entries (op, target, error)
|
|
2309
|
+
lattice teams dlq retry --team <name> [--id <id>] # replay; a late dependency now applies cleanly
|
|
2310
|
+
lattice teams dlq purge --team <name> [--id <id>] # discard without applying
|
|
2311
|
+
```
|
|
2304
2312
|
|
|
2305
2313
|
**Per-table ownership + opt-in sharing (v1.14+).** Team members share one physical Postgres, so visibility is enforced at the app layer via a `__lattice_object_owners` table: each table records its creator, and a user sees only the tables they own plus tables explicitly shared to the team. The native `files`/`secrets` objects are owned by the database creator and private by default. Sharing is an explicit, owner-only action (not a side effect of creating a table). The filter gates API access, not just the display.
|
|
2306
2314
|
|
package/dist/cli.js
CHANGED
|
@@ -435,6 +435,15 @@ function resolveDbPath(raw, configDir2) {
|
|
|
435
435
|
}
|
|
436
436
|
return resolve(configDir2, raw);
|
|
437
437
|
}
|
|
438
|
+
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
439
|
+
function warnDeprecatedRef(entity, field, target) {
|
|
440
|
+
const key = `${entity}.${field}`;
|
|
441
|
+
if (warnedDeprecatedRefs.has(key)) return;
|
|
442
|
+
warnedDeprecatedRefs.add(key);
|
|
443
|
+
console.warn(
|
|
444
|
+
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
438
447
|
function entityToTableDef(entityName, entity) {
|
|
439
448
|
const rawFields = entity.fields;
|
|
440
449
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -457,6 +466,7 @@ function entityToTableDef(entityName, entity) {
|
|
|
457
466
|
table: field.ref,
|
|
458
467
|
foreignKey: fieldName
|
|
459
468
|
};
|
|
469
|
+
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
460
470
|
}
|
|
461
471
|
}
|
|
462
472
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
@@ -6238,7 +6248,6 @@ var css = `
|
|
|
6238
6248
|
--danger: #ef4444;
|
|
6239
6249
|
--danger-deep: #dc2626;
|
|
6240
6250
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
6241
|
-
--sidebar-width: 380px;
|
|
6242
6251
|
--nav-width: 220px;
|
|
6243
6252
|
}
|
|
6244
6253
|
* { box-sizing: border-box; }
|
|
@@ -6459,30 +6468,11 @@ var css = `
|
|
|
6459
6468
|
minimum keeps the track at content-width and the whole page scrolls
|
|
6460
6469
|
horizontally. */
|
|
6461
6470
|
.layout {
|
|
6462
|
-
display: grid; grid-template-columns: var(--nav-width) minmax(0, 1fr)
|
|
6471
|
+
display: grid; grid-template-columns: var(--nav-width) minmax(0, 1fr);
|
|
6463
6472
|
height: calc(100vh - 56px);
|
|
6464
6473
|
}
|
|
6465
|
-
.rail-handle { display: none; }
|
|
6466
6474
|
@media (max-width: 720px) {
|
|
6467
|
-
|
|
6468
|
-
the feed to ~62svh. */
|
|
6469
|
-
.layout { grid-template-columns: var(--nav-width) minmax(0, 1fr); }
|
|
6470
|
-
.assistant-rail {
|
|
6471
|
-
position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
|
|
6472
|
-
border-left: none; border-top: 1px solid var(--border);
|
|
6473
|
-
max-height: 62svh; box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
|
|
6474
|
-
}
|
|
6475
|
-
.rail-resize { display: none; }
|
|
6476
|
-
.rail-handle {
|
|
6477
|
-
display: block; flex: 0 0 auto; height: 22px; cursor: pointer; position: relative;
|
|
6478
|
-
}
|
|
6479
|
-
.rail-handle::after {
|
|
6480
|
-
content: ''; position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
|
|
6481
|
-
width: 40px; height: 4px; border-radius: 2px; background: var(--border-strong);
|
|
6482
|
-
}
|
|
6483
|
-
.assistant-rail:not(.expanded) { max-height: none; }
|
|
6484
|
-
.assistant-rail:not(.expanded) .rail-feed { display: none; }
|
|
6485
|
-
main#content { padding-bottom: 96px; }
|
|
6475
|
+
main#content { padding-bottom: 24px; }
|
|
6486
6476
|
}
|
|
6487
6477
|
nav.sidebar {
|
|
6488
6478
|
background: var(--surface); border-right: 1px solid var(--border);
|
|
@@ -6506,51 +6496,6 @@ var css = `
|
|
|
6506
6496
|
|
|
6507
6497
|
main#content { padding: 24px; overflow: auto; }
|
|
6508
6498
|
|
|
6509
|
-
/* \u2500\u2500 Assistant rail (activity feed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6510
|
-
.assistant-rail {
|
|
6511
|
-
position: relative;
|
|
6512
|
-
background: var(--surface);
|
|
6513
|
-
border-left: 1px solid var(--border);
|
|
6514
|
-
display: flex; flex-direction: column;
|
|
6515
|
-
min-width: 0; overflow: hidden;
|
|
6516
|
-
}
|
|
6517
|
-
.rail-resize {
|
|
6518
|
-
position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
|
|
6519
|
-
cursor: col-resize; background: transparent; z-index: 5;
|
|
6520
|
-
transition: background-color 120ms;
|
|
6521
|
-
}
|
|
6522
|
-
.rail-resize:hover, .rail-resize.dragging { background: var(--accent-soft); }
|
|
6523
|
-
.rail-header {
|
|
6524
|
-
flex: 0 0 auto; padding: 12px 14px; border-bottom: 1px solid var(--border);
|
|
6525
|
-
display: flex; align-items: center; gap: 8px;
|
|
6526
|
-
}
|
|
6527
|
-
.rail-title {
|
|
6528
|
-
font-size: 11px; font-weight: 600; color: var(--text-muted);
|
|
6529
|
-
text-transform: uppercase; letter-spacing: 0.06em; flex: 0 0 auto;
|
|
6530
|
-
}
|
|
6531
|
-
.rail-feed {
|
|
6532
|
-
flex: 1 1 auto; overflow-y: auto; padding: 10px 12px;
|
|
6533
|
-
display: flex; flex-direction: column; gap: 8px;
|
|
6534
|
-
}
|
|
6535
|
-
.rail-empty { color: var(--text-muted); font-size: 12.5px; text-align: center; padding: 18px 8px; }
|
|
6536
|
-
.feed-item {
|
|
6537
|
-
display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; gap: 8px;
|
|
6538
|
-
align-items: baseline; padding: 7px 9px; border-radius: 8px;
|
|
6539
|
-
background: var(--surface-2); border: 1px solid var(--border);
|
|
6540
|
-
animation: feedIn 0.18s ease-out;
|
|
6541
|
-
}
|
|
6542
|
-
@keyframes feedIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
6543
|
-
.feed-icon { text-align: center; font-size: 13px; }
|
|
6544
|
-
.feed-body { min-width: 0; }
|
|
6545
|
-
.feed-summary { font-size: 13px; color: var(--text); word-break: break-word; }
|
|
6546
|
-
.feed-meta { margin-top: 2px; display: flex; align-items: center; gap: 6px; }
|
|
6547
|
-
.feed-source {
|
|
6548
|
-
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
|
|
6549
|
-
padding: 1px 6px; border-radius: 999px;
|
|
6550
|
-
background: var(--accent-soft); color: var(--accent);
|
|
6551
|
-
}
|
|
6552
|
-
.feed-time { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
6553
|
-
|
|
6554
6499
|
/* \u2500\u2500 File preview (files detail page) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6555
6500
|
.file-preview { margin: 4px 0 16px; }
|
|
6556
6501
|
.file-preview .file-desc {
|
|
@@ -6599,13 +6544,6 @@ var css = `
|
|
|
6599
6544
|
.stat-n { font-size: 24px; font-weight: 700; color: var(--text); }
|
|
6600
6545
|
.stat-tile.warn .stat-n { color: var(--warn); }
|
|
6601
6546
|
.stat-l { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
6602
|
-
.dash-recent { margin-top: 26px; max-width: 1100px; }
|
|
6603
|
-
.dash-recent-head { font-size: 13px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 10px; }
|
|
6604
|
-
.dash-recent ul { list-style: none; margin: 0; padding: 0; }
|
|
6605
|
-
.dash-act { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
|
|
6606
|
-
.dash-act-ic { width: 18px; text-align: center; }
|
|
6607
|
-
.dash-act-txt { flex: 1; color: var(--text); }
|
|
6608
|
-
.dash-act-time { font-size: 12px; color: var(--text-muted); }
|
|
6609
6547
|
|
|
6610
6548
|
/* \u2500\u2500 Table view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6611
6549
|
.view-header {
|
|
@@ -7117,6 +7055,8 @@ var css = `
|
|
|
7117
7055
|
transition: transform 0.05s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
7118
7056
|
}
|
|
7119
7057
|
.fs-tile:hover { border-color: var(--accent); transform: translateY(-1px); }
|
|
7058
|
+
.fs-tile-create { border-style: dashed; background: transparent; }
|
|
7059
|
+
.fs-tile-create .fs-tile-icon { color: var(--accent); }
|
|
7120
7060
|
.fs-tile-icon { font-size: 40px; line-height: 1; }
|
|
7121
7061
|
.fs-tile-label {
|
|
7122
7062
|
font-size: 13px; font-weight: 500; color: var(--text);
|
|
@@ -7412,9 +7352,6 @@ var appJs = `
|
|
|
7412
7352
|
refreshHistoryState();
|
|
7413
7353
|
renderRoute();
|
|
7414
7354
|
startRealtime();
|
|
7415
|
-
initRailResize();
|
|
7416
|
-
initRailDrawer();
|
|
7417
|
-
startFeed();
|
|
7418
7355
|
initSearch();
|
|
7419
7356
|
initLastEdited();
|
|
7420
7357
|
initOffline();
|
|
@@ -7770,11 +7707,12 @@ var appJs = `
|
|
|
7770
7707
|
}
|
|
7771
7708
|
|
|
7772
7709
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7773
|
-
//
|
|
7774
|
-
//
|
|
7775
|
-
//
|
|
7710
|
+
// Shared activity helpers \u2014 the operation-icon map and relative-time
|
|
7711
|
+
// formatter, used by Version History and the dashboard activity list. The
|
|
7712
|
+
// standalone Activity rail was removed in 1.16.1 (redundant with Version
|
|
7713
|
+
// History); multiplayer realtime convergence runs on the separate realtime
|
|
7714
|
+
// channel (startRealtime), not on this.
|
|
7776
7715
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7777
|
-
var feedSource = null;
|
|
7778
7716
|
var FEED_ICONS = {
|
|
7779
7717
|
insert: '\u2795', update: '\u270F\uFE0F', delete: '\u{1F5D1}',
|
|
7780
7718
|
link: '\u{1F517}', unlink: '\u26D3', undo: '\u21B6', redo: '\u21B7', schema: '\u{1F6E0}',
|
|
@@ -7790,51 +7728,6 @@ var appJs = `
|
|
|
7790
7728
|
return new Date(iso).toLocaleDateString();
|
|
7791
7729
|
} catch (_) { return ''; }
|
|
7792
7730
|
}
|
|
7793
|
-
function renderFeedItem(ev) {
|
|
7794
|
-
var feedEl = document.getElementById('rail-feed');
|
|
7795
|
-
if (!feedEl) return;
|
|
7796
|
-
var empty = document.getElementById('rail-empty');
|
|
7797
|
-
if (empty) empty.remove();
|
|
7798
|
-
var item = document.createElement('div');
|
|
7799
|
-
item.className = 'feed-item';
|
|
7800
|
-
var icon = document.createElement('div');
|
|
7801
|
-
icon.className = 'feed-icon';
|
|
7802
|
-
icon.textContent = FEED_ICONS[ev.op] || '\u2022';
|
|
7803
|
-
var body = document.createElement('div');
|
|
7804
|
-
body.className = 'feed-body';
|
|
7805
|
-
var summary = document.createElement('div');
|
|
7806
|
-
summary.className = 'feed-summary';
|
|
7807
|
-
summary.textContent = ev.summary || (String(ev.op || '') + ' ' + String(ev.table || ''));
|
|
7808
|
-
var meta = document.createElement('div');
|
|
7809
|
-
meta.className = 'feed-meta';
|
|
7810
|
-
var src = document.createElement('span');
|
|
7811
|
-
src.className = 'feed-source';
|
|
7812
|
-
src.textContent = ev.source === 'gui' ? 'you' : String(ev.source || '');
|
|
7813
|
-
meta.appendChild(src);
|
|
7814
|
-
body.appendChild(summary);
|
|
7815
|
-
body.appendChild(meta);
|
|
7816
|
-
var time = document.createElement('div');
|
|
7817
|
-
time.className = 'feed-time';
|
|
7818
|
-
time.textContent = relTime(ev.ts);
|
|
7819
|
-
item.appendChild(icon);
|
|
7820
|
-
item.appendChild(body);
|
|
7821
|
-
item.appendChild(time);
|
|
7822
|
-
// Most-recent on top: prepend new items and keep the view scrolled up.
|
|
7823
|
-
feedEl.insertBefore(item, feedEl.firstChild);
|
|
7824
|
-
feedEl.scrollTop = 0;
|
|
7825
|
-
}
|
|
7826
|
-
function startFeed() {
|
|
7827
|
-
if (feedSource) {
|
|
7828
|
-
try { feedSource.close(); } catch (_) { /* ignore */ }
|
|
7829
|
-
feedSource = null;
|
|
7830
|
-
}
|
|
7831
|
-
if (typeof EventSource === 'undefined') return;
|
|
7832
|
-
feedSource = new EventSource('/api/feed/stream');
|
|
7833
|
-
feedSource.addEventListener('feed', function (ev) {
|
|
7834
|
-
try { renderFeedItem(JSON.parse(ev.data)); } catch (_) { /* ignore malformed */ }
|
|
7835
|
-
});
|
|
7836
|
-
// EventSource auto-reconnects on error; no extra handling needed.
|
|
7837
|
-
}
|
|
7838
7731
|
|
|
7839
7732
|
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7840
7733
|
// Full-text search \u2014 GET /api/search, grouped dropdown, click to open.
|
|
@@ -7916,52 +7809,6 @@ var appJs = `
|
|
|
7916
7809
|
});
|
|
7917
7810
|
}
|
|
7918
7811
|
|
|
7919
|
-
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7920
|
-
// Activity rail resize \u2014 drag the left edge, clamp, persist.
|
|
7921
|
-
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7922
|
-
var RAIL_MIN = 320, RAIL_MAX = 640, RAIL_KEY = 'lattice-rail-width';
|
|
7923
|
-
function applyRailWidth(px) {
|
|
7924
|
-
var w = Math.min(RAIL_MAX, Math.max(RAIL_MIN, Math.round(px)));
|
|
7925
|
-
document.documentElement.style.setProperty('--sidebar-width', w + 'px');
|
|
7926
|
-
return w;
|
|
7927
|
-
}
|
|
7928
|
-
function initRailResize() {
|
|
7929
|
-
var saved = parseInt(window.localStorage.getItem(RAIL_KEY) || '', 10);
|
|
7930
|
-
if (!isNaN(saved)) applyRailWidth(saved);
|
|
7931
|
-
var handle = document.getElementById('rail-resize');
|
|
7932
|
-
if (!handle) return;
|
|
7933
|
-
handle.addEventListener('pointerdown', function (e) {
|
|
7934
|
-
e.preventDefault();
|
|
7935
|
-
var startX = e.clientX;
|
|
7936
|
-
var rail = document.getElementById('assistant-rail');
|
|
7937
|
-
var startW = rail ? rail.getBoundingClientRect().width : 380;
|
|
7938
|
-
handle.classList.add('dragging');
|
|
7939
|
-
function move(ev) {
|
|
7940
|
-
// Rail sits on the right; dragging left (smaller clientX) widens it.
|
|
7941
|
-
applyRailWidth(startW - (ev.clientX - startX));
|
|
7942
|
-
}
|
|
7943
|
-
function up() {
|
|
7944
|
-
handle.classList.remove('dragging');
|
|
7945
|
-
window.removeEventListener('pointermove', move);
|
|
7946
|
-
window.removeEventListener('pointerup', up);
|
|
7947
|
-
var cur = parseInt(
|
|
7948
|
-
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width'),
|
|
7949
|
-
10,
|
|
7950
|
-
);
|
|
7951
|
-
if (!isNaN(cur)) window.localStorage.setItem(RAIL_KEY, String(cur));
|
|
7952
|
-
}
|
|
7953
|
-
window.addEventListener('pointermove', move);
|
|
7954
|
-
window.addEventListener('pointerup', up);
|
|
7955
|
-
});
|
|
7956
|
-
}
|
|
7957
|
-
|
|
7958
|
-
// Mobile: tapping the handle expands/collapses the bottom drawer.
|
|
7959
|
-
function initRailDrawer() {
|
|
7960
|
-
var handle = document.getElementById('rail-handle');
|
|
7961
|
-
var rail = document.getElementById('assistant-rail');
|
|
7962
|
-
if (handle && rail) handle.addEventListener('click', function () { rail.classList.toggle('expanded'); });
|
|
7963
|
-
}
|
|
7964
|
-
|
|
7965
7812
|
/** Reload column meta after a secret-flag change. */
|
|
7966
7813
|
function refreshColumnMeta() {
|
|
7967
7814
|
return fetchJson('/api/gui-meta/columns').then(function (d) {
|
|
@@ -8112,10 +7959,10 @@ var appJs = `
|
|
|
8112
7959
|
else renderRoute();
|
|
8113
7960
|
loadedTables = {};
|
|
8114
7961
|
startRealtime();
|
|
8115
|
-
startFeed();
|
|
8116
7962
|
});
|
|
8117
7963
|
}
|
|
8118
7964
|
|
|
7965
|
+
var wsOutsideClickBound = false;
|
|
8119
7966
|
function renderWsSwitcher(data) {
|
|
8120
7967
|
var wrap = document.getElementById('ws-switcher');
|
|
8121
7968
|
var btn = document.getElementById('ws-button');
|
|
@@ -8173,8 +8020,13 @@ var appJs = `
|
|
|
8173
8020
|
});
|
|
8174
8021
|
});
|
|
8175
8022
|
});
|
|
8176
|
-
document.getElementById('ws-create-btn').addEventListener('click', function () {
|
|
8177
|
-
showCreateWorkspaceInput
|
|
8023
|
+
document.getElementById('ws-create-btn').addEventListener('click', function (e) {
|
|
8024
|
+
// Stop propagation: showCreateWorkspaceInput replaces .db-create's
|
|
8025
|
+
// innerHTML, detaching THIS button. Without this, the click then
|
|
8026
|
+
// bubbles to the document outside-click closer, whose
|
|
8027
|
+
// menu.contains(e.target) is now false (target detached) \u2192 it would
|
|
8028
|
+
// close the menu, so the create input never appears.
|
|
8029
|
+
e.stopPropagation(); showCreateWorkspaceInput(menu);
|
|
8178
8030
|
});
|
|
8179
8031
|
}
|
|
8180
8032
|
|
|
@@ -8183,12 +8035,20 @@ var appJs = `
|
|
|
8183
8035
|
if (menu.hidden) buildMenu();
|
|
8184
8036
|
menu.hidden = !menu.hidden;
|
|
8185
8037
|
};
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8038
|
+
// Attach the outside-click closer ONCE \u2014 renderWsSwitcher runs on every
|
|
8039
|
+
// reload, so adding it each time leaked a listener per render. Re-fetch
|
|
8040
|
+
// the elements by id inside the handler so it never holds a stale closure.
|
|
8041
|
+
if (!wsOutsideClickBound) {
|
|
8042
|
+
wsOutsideClickBound = true;
|
|
8043
|
+
document.addEventListener('click', function (e) {
|
|
8044
|
+
var m = document.getElementById('ws-menu');
|
|
8045
|
+
var b = document.getElementById('ws-button');
|
|
8046
|
+
if (!m || m.hidden) return;
|
|
8047
|
+
if (!m.contains(e.target) && e.target !== b && (!b || !b.contains(e.target))) {
|
|
8048
|
+
m.hidden = true;
|
|
8049
|
+
}
|
|
8050
|
+
});
|
|
8051
|
+
}
|
|
8192
8052
|
}
|
|
8193
8053
|
|
|
8194
8054
|
// Inline "new workspace" name entry, shown inside the Workspaces menu.
|
|
@@ -8433,7 +8293,6 @@ var appJs = `
|
|
|
8433
8293
|
entities: tables.map(function (t) {
|
|
8434
8294
|
return { name: t.name, rowCount: t.rowCount, lastUpdatedAt: null, stale: false };
|
|
8435
8295
|
}),
|
|
8436
|
-
recent: [],
|
|
8437
8296
|
};
|
|
8438
8297
|
}
|
|
8439
8298
|
function drawDashboard(content, d) {
|
|
@@ -8471,21 +8330,7 @@ var appJs = `
|
|
|
8471
8330
|
fresh +
|
|
8472
8331
|
'</a>';
|
|
8473
8332
|
}).join('');
|
|
8474
|
-
|
|
8475
|
-
if (d.recent && d.recent.length) {
|
|
8476
|
-
var items = d.recent.map(function (r) {
|
|
8477
|
-
var ic = FEED_ICONS[r.op] || '\u2022';
|
|
8478
|
-
var label = displayFor(r.table).label;
|
|
8479
|
-
return '<li class="dash-act">' +
|
|
8480
|
-
'<span class="dash-act-ic">' + ic + '</span>' +
|
|
8481
|
-
'<span class="dash-act-txt">' + escapeHtml(String(r.op)) + ' \xB7 ' + escapeHtml(label) + '</span>' +
|
|
8482
|
-
'<span class="dash-act-time">' + relTime(r.ts) + '</span>' +
|
|
8483
|
-
'</li>';
|
|
8484
|
-
}).join('');
|
|
8485
|
-
recent = '<div class="dash-recent"><div class="dash-recent-head">Recent activity</div>' +
|
|
8486
|
-
'<ul>' + items + '</ul></div>';
|
|
8487
|
-
}
|
|
8488
|
-
content.innerHTML = stats + '<div class="dashboard">' + cards + '</div>' + recent;
|
|
8333
|
+
content.innerHTML = stats + '<div class="dashboard">' + cards + '</div>';
|
|
8489
8334
|
}
|
|
8490
8335
|
function renderDashboard(content) {
|
|
8491
8336
|
// Workspace overview: counts + freshness + recent activity from
|
|
@@ -9365,7 +9210,16 @@ var appJs = `
|
|
|
9365
9210
|
return rowsP.then(function (rows) {
|
|
9366
9211
|
var d = displayFor(table);
|
|
9367
9212
|
var base = fsHref(segs);
|
|
9368
|
-
|
|
9213
|
+
// "New" tile (top-level collections only) \u2014 a folder box with a + that
|
|
9214
|
+
// opens a create form. Related-row folders aren't a place to mint a
|
|
9215
|
+
// brand-new object, so the tile is top-level only.
|
|
9216
|
+
var createTile = topLevel
|
|
9217
|
+
? '<a class="fs-tile fs-tile-create" href="#" data-fs-create="1" title="Create a new ' + escapeHtml(d.label) + '">' +
|
|
9218
|
+
'<div class="fs-tile-icon">\u2795</div>' +
|
|
9219
|
+
'<div class="fs-tile-label">New ' + escapeHtml(d.label) + '</div>' +
|
|
9220
|
+
'</a>'
|
|
9221
|
+
: '';
|
|
9222
|
+
var rowTiles = rows.length
|
|
9369
9223
|
? rows.map(function (r) {
|
|
9370
9224
|
var icon = (table === 'files') ? fileEmoji(r) : '\u{1F4C1}';
|
|
9371
9225
|
return '<a class="fs-tile" href="' + base + '/' + encodeURIComponent(r.id) + '">' +
|
|
@@ -9373,7 +9227,7 @@ var appJs = `
|
|
|
9373
9227
|
'<div class="fs-tile-label">' + escapeHtml(fsDisplayName(r)) + '</div>' +
|
|
9374
9228
|
'</a>';
|
|
9375
9229
|
}).join('')
|
|
9376
|
-
: '<div class="fs-empty">Nothing here yet.</div>';
|
|
9230
|
+
: (topLevel ? '' : '<div class="fs-empty">Nothing here yet.</div>');
|
|
9377
9231
|
content.innerHTML =
|
|
9378
9232
|
fsBreadcrumb(segs, crumbs) +
|
|
9379
9233
|
'<div class="view-header">' +
|
|
@@ -9381,13 +9235,111 @@ var appJs = `
|
|
|
9381
9235
|
'<h1>' + escapeHtml(d.label) + '</h1>' +
|
|
9382
9236
|
'<span class="count">' + rows.length + ' item' + (rows.length === 1 ? '' : 's') + '</span>' +
|
|
9383
9237
|
'</div>' +
|
|
9384
|
-
'<div class="fs-grid">' +
|
|
9238
|
+
'<div class="fs-grid">' + createTile + rowTiles + '</div>';
|
|
9239
|
+
var ctile = content.querySelector('[data-fs-create]');
|
|
9240
|
+
if (ctile) ctile.addEventListener('click', function (e) {
|
|
9241
|
+
e.preventDefault();
|
|
9242
|
+
openFsCreateModal(content, table, segs);
|
|
9243
|
+
});
|
|
9385
9244
|
});
|
|
9386
9245
|
}).catch(function (err) {
|
|
9387
9246
|
content.innerHTML = '<div class="placeholder"><h2>Failed</h2>' + escapeHtml(err.message) + '</div>';
|
|
9388
9247
|
});
|
|
9389
9248
|
}
|
|
9390
9249
|
|
|
9250
|
+
// Create a new object from the simple view \u2014 a form styled like the item
|
|
9251
|
+
// page with blank fields + a Save button, plus a select-menu + "+" for each
|
|
9252
|
+
// many-to-many link. Reuses fieldFor() (intrinsic + belongsTo) and the
|
|
9253
|
+
// existing row-create + junction-row endpoints (no new backend).
|
|
9254
|
+
function openFsCreateModal(content, table, segs) {
|
|
9255
|
+
var t = tableByName(table);
|
|
9256
|
+
if (!t) return;
|
|
9257
|
+
var bt = belongsToColumns(t);
|
|
9258
|
+
var juncs = junctionsFor(table);
|
|
9259
|
+
// Preload FK + junction-remote target rows so the <select> menus populate.
|
|
9260
|
+
var needed = bt.map(function (b) { return b.rel.table; })
|
|
9261
|
+
.concat(juncs.map(function (j) { return j.remoteRel.table; }));
|
|
9262
|
+
Promise.all(needed.map(loadAllRows)).then(function () {
|
|
9263
|
+
var fieldsHtml = '';
|
|
9264
|
+
intrinsicColumns(t).forEach(function (c) {
|
|
9265
|
+
fieldsHtml += '<div class="fs-field"><div class="fs-field-label">' + escapeHtml(titleCase(c)) + '</div>' +
|
|
9266
|
+
'<div class="fs-field-val">' + fieldFor(c, '', t) + '</div></div>';
|
|
9267
|
+
});
|
|
9268
|
+
bt.forEach(function (b) {
|
|
9269
|
+
fieldsHtml += '<div class="fs-field"><div class="fs-field-label">' + escapeHtml(titleCase(b.relName)) + '</div>' +
|
|
9270
|
+
'<div class="fs-field-val">' + fieldFor(b.rel.foreignKey, '', t) + '</div></div>';
|
|
9271
|
+
});
|
|
9272
|
+
juncs.forEach(function (j) {
|
|
9273
|
+
var remoteRows = loadedTables[j.remoteRel.table] || [];
|
|
9274
|
+
var opts = '<option value="">(none)</option>' + remoteRows.map(function (r) {
|
|
9275
|
+
return '<option value="' + escapeHtml(r.id) + '">' + escapeHtml(displayNameFor(r)) + '</option>';
|
|
9276
|
+
}).join('');
|
|
9277
|
+
fieldsHtml += '<div class="fs-field"><div class="fs-field-label">' + escapeHtml(titleCase(j.remoteRel.table)) + ' (links)</div>' +
|
|
9278
|
+
'<div class="fs-field-val">' +
|
|
9279
|
+
'<div class="fs-link-stage" data-junction="' + escapeHtml(j.junction) + '" data-local-fk="' + escapeHtml(j.localFk) + '" data-remote-fk="' + escapeHtml(j.remoteRel.foreignKey) + '">' +
|
|
9280
|
+
'<select class="fs-link-select">' + opts + '</select>' +
|
|
9281
|
+
'</div>' +
|
|
9282
|
+
'<button type="button" class="btn fs-link-add">+ Add another</button>' +
|
|
9283
|
+
'</div></div>';
|
|
9284
|
+
});
|
|
9285
|
+
showModal('New ' + displayFor(table).label, '<div class="fs-doc fs-create-form">' + fieldsHtml + '</div>', {
|
|
9286
|
+
primaryLabel: 'Save',
|
|
9287
|
+
onBody: function (backdrop) {
|
|
9288
|
+
backdrop.querySelectorAll('.fs-link-add').forEach(function (addBtn) {
|
|
9289
|
+
addBtn.addEventListener('click', function () {
|
|
9290
|
+
var stage = addBtn.previousElementSibling; // the .fs-link-stage
|
|
9291
|
+
var firstSel = stage && stage.querySelector('.fs-link-select');
|
|
9292
|
+
if (!firstSel) return;
|
|
9293
|
+
var clone = firstSel.cloneNode(true);
|
|
9294
|
+
clone.value = '';
|
|
9295
|
+
stage.appendChild(clone);
|
|
9296
|
+
});
|
|
9297
|
+
});
|
|
9298
|
+
},
|
|
9299
|
+
onSubmit: function (scope) {
|
|
9300
|
+
// Intrinsic + belongsTo values (the [name] inputs/selects).
|
|
9301
|
+
var values = {};
|
|
9302
|
+
scope.querySelectorAll('.fs-create-form [name]').forEach(function (el) {
|
|
9303
|
+
var v = el.value;
|
|
9304
|
+
if (v !== '' && v != null) values[el.getAttribute('name')] = v;
|
|
9305
|
+
});
|
|
9306
|
+
// Staged many-to-many links \u2014 one junction row per chosen target.
|
|
9307
|
+
var links = [];
|
|
9308
|
+
scope.querySelectorAll('.fs-link-stage').forEach(function (stage) {
|
|
9309
|
+
var junction = stage.getAttribute('data-junction');
|
|
9310
|
+
var localFk = stage.getAttribute('data-local-fk');
|
|
9311
|
+
var remoteFk = stage.getAttribute('data-remote-fk');
|
|
9312
|
+
stage.querySelectorAll('.fs-link-select').forEach(function (sel) {
|
|
9313
|
+
if (sel.value) links.push({ junction: junction, localFk: localFk, remoteFk: remoteFk, remoteId: sel.value });
|
|
9314
|
+
});
|
|
9315
|
+
});
|
|
9316
|
+
return rowWrite('POST', '/api/tables/' + encodeURIComponent(table) + '/rows', values).then(function (res) {
|
|
9317
|
+
var newId = res && (res.id || (res.row && res.row.id));
|
|
9318
|
+
var chain = Promise.resolve();
|
|
9319
|
+
links.forEach(function (lk) {
|
|
9320
|
+
chain = chain.then(function () {
|
|
9321
|
+
// Use the junction's /link endpoint (INSERT OR IGNORE on the
|
|
9322
|
+
// two FK columns) \u2014 works for junctions with no own pk and is
|
|
9323
|
+
// idempotent, unlike a raw row insert.
|
|
9324
|
+
var jrow = {};
|
|
9325
|
+
jrow[lk.localFk] = newId;
|
|
9326
|
+
jrow[lk.remoteFk] = lk.remoteId;
|
|
9327
|
+
return rowWrite('POST', '/api/tables/' + encodeURIComponent(lk.junction) + '/link', jrow);
|
|
9328
|
+
});
|
|
9329
|
+
});
|
|
9330
|
+
return chain;
|
|
9331
|
+
}).then(function () {
|
|
9332
|
+
invalidate(table);
|
|
9333
|
+
return refreshEntities();
|
|
9334
|
+
}).then(function () {
|
|
9335
|
+
showToast('Created', {});
|
|
9336
|
+
renderFsCollection(content, segs);
|
|
9337
|
+
});
|
|
9338
|
+
},
|
|
9339
|
+
});
|
|
9340
|
+
}).catch(function (err) { showToast('Create failed: ' + err.message, {}); });
|
|
9341
|
+
}
|
|
9342
|
+
|
|
9391
9343
|
// Item view \u2014 one row as a document (click-to-edit) + its relationship folders.
|
|
9392
9344
|
function renderFsItem(content, segs) {
|
|
9393
9345
|
fsWalk(segs).then(function (crumbs) {
|
|
@@ -9732,8 +9684,11 @@ var appJs = `
|
|
|
9732
9684
|
if (!s) return '';
|
|
9733
9685
|
try {
|
|
9734
9686
|
var d = new Date(s);
|
|
9687
|
+
// Never render the literal "Invalid Date" \u2014 new Date() returns an
|
|
9688
|
+
// Invalid Date (not a throw) for an unparseable value.
|
|
9689
|
+
if (isNaN(d.getTime())) return '(no timestamp)';
|
|
9735
9690
|
return d.toLocaleString();
|
|
9736
|
-
} catch (_e) { return
|
|
9691
|
+
} catch (_e) { return '(no timestamp)'; }
|
|
9737
9692
|
}
|
|
9738
9693
|
|
|
9739
9694
|
/** Side-by-side-ish text diff. Shows changed columns only for updates. */
|
|
@@ -10288,7 +10243,9 @@ var appJs = `
|
|
|
10288
10243
|
var linkRows = dmLinks.map(function (lk, i) {
|
|
10289
10244
|
return '<div class="dm-link-row">' +
|
|
10290
10245
|
'<span class="dm-link-name">' + escapeHtml(displayFor(lk.other).label) + '</span>' +
|
|
10291
|
-
'<span class="dm-link-arrow"
|
|
10246
|
+
'<span class="dm-link-arrow' + (lk.kind === 'fk' ? ' legacy' : '') + '" ' +
|
|
10247
|
+
(lk.kind === 'fk' ? 'title="Legacy one-to-many link. New links are many-to-many; this is kept for back-compat and will be migrated in 2.0."' : '') +
|
|
10248
|
+
'>' + (lk.kind === 'fk' ? '\u2192 one-to-many (legacy)' : '\u2194 many-to-many') + '</span>' +
|
|
10292
10249
|
'<button class="btn danger dm-link-destroy" data-link="' + i +
|
|
10293
10250
|
'" title="Delete this link \u2014 removes it from both tables">Delete link</button>' +
|
|
10294
10251
|
'</div>';
|
|
@@ -11230,12 +11187,12 @@ var appJs = `
|
|
|
11230
11187
|
fetchJson('/api/userconfig/databases').then(function (cat) {
|
|
11231
11188
|
var localRows = (cat.local || []).map(function (d) {
|
|
11232
11189
|
var stateBadge = '<span style="font-family:JetBrains Mono,monospace;font-size:10px;color:var(--text-muted)">' + escapeHtml((d.state || 'local').toUpperCase()) + '</span>';
|
|
11233
|
-
return '<tr>' +
|
|
11190
|
+
return '<tr' + (d.active ? '' : ' class="db-row" data-switch-path="' + escapeHtml(d.configPath) + '"') + '>' +
|
|
11234
11191
|
'<td>' + escapeHtml(d.label) + (d.active ? ' <span class="role-tag">active</span>' : '') + '</td>' +
|
|
11235
11192
|
'<td>SQLite</td>' +
|
|
11236
11193
|
'<td>' + stateBadge + '</td>' +
|
|
11237
11194
|
'<td><code>' + escapeHtml(d.dbFile) + '</code></td>' +
|
|
11238
|
-
'<td
|
|
11195
|
+
'<td>\u2014</td>' +
|
|
11239
11196
|
'</tr>';
|
|
11240
11197
|
}).join('');
|
|
11241
11198
|
var cloudRows = (cat.cloud || []).map(function (d) {
|
|
@@ -11253,9 +11210,9 @@ var appJs = `
|
|
|
11253
11210
|
'<tbody>' + (localRows + cloudRows || '<tr><td colspan="5" style="padding:8px;color:var(--text-muted)">No databases configured.</td></tr>') + '</tbody>' +
|
|
11254
11211
|
'</table>' +
|
|
11255
11212
|
'</div>';
|
|
11256
|
-
host.querySelectorAll('[data-switch]').forEach(function (
|
|
11257
|
-
|
|
11258
|
-
var configPath =
|
|
11213
|
+
host.querySelectorAll('tr.db-row[data-switch-path]').forEach(function (row) {
|
|
11214
|
+
row.addEventListener('click', function () {
|
|
11215
|
+
var configPath = row.getAttribute('data-switch-path');
|
|
11259
11216
|
fetch('/api/databases/switch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: configPath }) })
|
|
11260
11217
|
.then(function (r) { return r.json(); })
|
|
11261
11218
|
.then(function () { renderUserConfig(document.getElementById('content')); });
|
|
@@ -11356,11 +11313,74 @@ var appJs = `
|
|
|
11356
11313
|
|
|
11357
11314
|
function renderDatabaseDangerZone(host) {
|
|
11358
11315
|
if (!host) return;
|
|
11359
|
-
|
|
11316
|
+
Promise.all([
|
|
11317
|
+
fetchJson('/api/databases'),
|
|
11318
|
+
fetchJson('/api/dbconfig').catch(function () { return {}; }),
|
|
11319
|
+
]).then(function (results) {
|
|
11320
|
+
var data = results[0];
|
|
11321
|
+
var cfg = results[1] || {};
|
|
11360
11322
|
var current = (data && data.current) || {};
|
|
11361
11323
|
var label = current.label || current.dbFile || '';
|
|
11362
11324
|
var path = current.path || '';
|
|
11363
11325
|
if (!path) { host.innerHTML = ''; return; }
|
|
11326
|
+
|
|
11327
|
+
// After tearing down / leaving the active DB, switch to another the
|
|
11328
|
+
// operator still has and navigate off the (now-gone) page.
|
|
11329
|
+
var switchAway = function () {
|
|
11330
|
+
var cur = (data && data.current && data.current.path) || null;
|
|
11331
|
+
var target = ((data && data.configs) || []).filter(function (c) { return c.path !== cur; })[0];
|
|
11332
|
+
var p = target
|
|
11333
|
+
? fetchJson('/api/databases/switch', {
|
|
11334
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
11335
|
+
body: JSON.stringify({ path: target.path }),
|
|
11336
|
+
}).then(function () { return reloadEverything(); })
|
|
11337
|
+
: reloadEverything();
|
|
11338
|
+
return p.then(function () { location.hash = '#/'; renderRoute(); });
|
|
11339
|
+
};
|
|
11340
|
+
|
|
11341
|
+
if (cfg.state === 'team-cloud-creator') {
|
|
11342
|
+
// Owner: disconnect the database from the cloud \u2014 kicks all members.
|
|
11343
|
+
host.innerHTML =
|
|
11344
|
+
'<div class="danger-zone">' +
|
|
11345
|
+
'<h3>Danger zone</h3>' +
|
|
11346
|
+
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11347
|
+
'Disconnect this database from the cloud. This removes the team and <strong>kicks all members</strong>. This cannot be undone.' +
|
|
11348
|
+
'</p>' +
|
|
11349
|
+
'<button class="btn destructive" id="db-disconnect-btn">Disconnect from cloud</button>' +
|
|
11350
|
+
'</div>';
|
|
11351
|
+
host.querySelector('#db-disconnect-btn').addEventListener('click', function () {
|
|
11352
|
+
if (!confirm('Disconnect "' + (cfg.teamName || label || 'this database') + '" from the cloud? This kicks all members and cannot be undone.')) return;
|
|
11353
|
+
var dbtn = host.querySelector('#db-disconnect-btn');
|
|
11354
|
+
withBusy(dbtn, function () {
|
|
11355
|
+
return fetchJson('/api/teams-gui/teams/' + cfg.teamId, { method: 'DELETE' })
|
|
11356
|
+
.then(function () { showToast('Disconnected from cloud', {}); return switchAway(); })
|
|
11357
|
+
.catch(function (e) { alert('Disconnect failed: ' + e.message); });
|
|
11358
|
+
});
|
|
11359
|
+
});
|
|
11360
|
+
return;
|
|
11361
|
+
}
|
|
11362
|
+
if (cfg.state === 'team-cloud-member') {
|
|
11363
|
+
// Member: leave the team. The cloud DB keeps running for others.
|
|
11364
|
+
host.innerHTML =
|
|
11365
|
+
'<div class="danger-zone">' +
|
|
11366
|
+
'<h3>Danger zone</h3>' +
|
|
11367
|
+
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11368
|
+
'Leave this team. The cloud database keeps running for everyone else; you simply stop being a member.' +
|
|
11369
|
+
'</p>' +
|
|
11370
|
+
'<button class="btn destructive" id="db-leave-btn">Leave team</button>' +
|
|
11371
|
+
'</div>';
|
|
11372
|
+
host.querySelector('#db-leave-btn').addEventListener('click', function () {
|
|
11373
|
+
if (!confirm('Leave "' + (cfg.teamName || label || 'this team') + '"?')) return;
|
|
11374
|
+
var lbtn = host.querySelector('#db-leave-btn');
|
|
11375
|
+
withBusy(lbtn, function () {
|
|
11376
|
+
return fetchJson('/api/teams-gui/teams/' + cfg.teamId + '/members/' + encodeURIComponent(cfg.myUserId), { method: 'DELETE' })
|
|
11377
|
+
.then(function () { showToast('Left the team', {}); return switchAway(); })
|
|
11378
|
+
.catch(function (e) { alert('Leave failed: ' + e.message); });
|
|
11379
|
+
});
|
|
11380
|
+
});
|
|
11381
|
+
return;
|
|
11382
|
+
}
|
|
11383
|
+
// Local / non-team cloud database: delete it.
|
|
11364
11384
|
host.innerHTML =
|
|
11365
11385
|
'<div class="danger-zone">' +
|
|
11366
11386
|
'<h3>Danger zone</h3>' +
|
|
@@ -11461,12 +11481,11 @@ var appJs = `
|
|
|
11461
11481
|
: '\u2014';
|
|
11462
11482
|
var rowLabel = c.label || c.name;
|
|
11463
11483
|
var del = '<button class="btn danger" data-delete-path="' + escapeHtml(c.path) + '" data-delete-label="' + escapeHtml(rowLabel) + '">Delete</button>';
|
|
11464
|
-
|
|
11465
|
-
return '<tr>' +
|
|
11484
|
+
return '<tr' + (c.active ? '' : ' class="ws-row" data-switch-path="' + escapeHtml(c.path) + '"') + '>' +
|
|
11466
11485
|
'<td>' + escapeHtml(rowLabel) + (c.active ? ' <span class="role-tag">active</span>' : '') + '</td>' +
|
|
11467
11486
|
'<td>' + kind + '</td>' +
|
|
11468
11487
|
'<td><code>' + escapeHtml(c.dbFile || '') + '</code></td>' +
|
|
11469
|
-
'<td>' +
|
|
11488
|
+
'<td>' + del + '</td>' +
|
|
11470
11489
|
'</tr>';
|
|
11471
11490
|
}).join('');
|
|
11472
11491
|
host.innerHTML =
|
|
@@ -11480,9 +11499,9 @@ var appJs = `
|
|
|
11480
11499
|
'<tbody>' + (rows || '<tr><td colspan="4" style="padding:8px;color:var(--text-muted)">No workspaces configured.</td></tr>') + '</tbody>' +
|
|
11481
11500
|
'</table>' +
|
|
11482
11501
|
'</div>';
|
|
11483
|
-
host.querySelectorAll('[data-switch]').forEach(function (
|
|
11484
|
-
|
|
11485
|
-
var configPath =
|
|
11502
|
+
host.querySelectorAll('tr.ws-row[data-switch-path]').forEach(function (row) {
|
|
11503
|
+
row.addEventListener('click', function () {
|
|
11504
|
+
var configPath = row.getAttribute('data-switch-path');
|
|
11486
11505
|
fetch('/api/databases/switch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: configPath }) })
|
|
11487
11506
|
.then(function (r) { return r.json(); })
|
|
11488
11507
|
.then(function () { return reloadEverything(); })
|
|
@@ -11490,7 +11509,8 @@ var appJs = `
|
|
|
11490
11509
|
});
|
|
11491
11510
|
});
|
|
11492
11511
|
host.querySelectorAll('[data-delete-path]').forEach(function (btn) {
|
|
11493
|
-
btn.addEventListener('click', function () {
|
|
11512
|
+
btn.addEventListener('click', function (e) {
|
|
11513
|
+
e.stopPropagation(); // don't trigger the row's switch handler
|
|
11494
11514
|
confirmDeleteDatabase(
|
|
11495
11515
|
btn.getAttribute('data-delete-path'),
|
|
11496
11516
|
btn.getAttribute('data-delete-label'),
|
|
@@ -11569,11 +11589,11 @@ var appJs = `
|
|
|
11569
11589
|
return (
|
|
11570
11590
|
'<p style="margin:0 0 12px;color:var(--text-muted);font-size:13px">' +
|
|
11571
11591
|
'SQLite DB: <code>' + escapeHtml(info.dbFile || '(unknown)') + '</code>. ' +
|
|
11572
|
-
'
|
|
11592
|
+
'Push this workspace to a cloud Postgres to collaborate. ' +
|
|
11593
|
+
'(To join an existing cloud, create a new workspace and choose \u201Cjoin via cloud invite\u201D.)' +
|
|
11573
11594
|
'</p>' +
|
|
11574
11595
|
'<div class="team-actions">' +
|
|
11575
11596
|
'<button class="btn primary" data-act="open-migrate">Migrate to cloud \u2192</button>' +
|
|
11576
|
-
'<button class="btn" data-act="open-connect-existing">Connect to existing cloud \u2192</button>' +
|
|
11577
11597
|
'</div>'
|
|
11578
11598
|
);
|
|
11579
11599
|
}
|
|
@@ -11594,10 +11614,10 @@ var appJs = `
|
|
|
11594
11614
|
(isCreator ? ' \xB7 <span style="color:var(--accent)">you are the creator</span>' : ' \xB7 <span style="color:var(--text-muted)">member</span>') +
|
|
11595
11615
|
'</div>' +
|
|
11596
11616
|
'<div class="team-actions" style="margin-top:10px">' +
|
|
11597
|
-
(isCreator ? '<button class="btn primary" data-act="open-invite">
|
|
11617
|
+
(isCreator ? '<button class="btn primary" data-act="open-invite">Invite member</button>' : '') +
|
|
11598
11618
|
'</div>' +
|
|
11599
|
-
//
|
|
11600
|
-
// the
|
|
11619
|
+
// Exit actions (Disconnect for the owner / Leave for a member) live
|
|
11620
|
+
// in the Danger Zone below \u2014 not on a member row.
|
|
11601
11621
|
'<div id="db-members-host" style="margin-top:12px"><div style="font-size:12px;color:var(--text-muted)">Loading members\u2026</div></div>'
|
|
11602
11622
|
);
|
|
11603
11623
|
}
|
|
@@ -11642,11 +11662,6 @@ var appJs = `
|
|
|
11642
11662
|
showMigrateToCloudModal(rerender);
|
|
11643
11663
|
});
|
|
11644
11664
|
|
|
11645
|
-
var connectExBtn = host.querySelector('[data-act="open-connect-existing"]');
|
|
11646
|
-
if (connectExBtn) connectExBtn.addEventListener('click', function () {
|
|
11647
|
-
showConnectExistingModal(rerender);
|
|
11648
|
-
});
|
|
11649
|
-
|
|
11650
11665
|
var upgradeBtn = host.querySelector('[data-act="open-upgrade"]');
|
|
11651
11666
|
if (upgradeBtn) upgradeBtn.addEventListener('click', function () {
|
|
11652
11667
|
showUpgradeToTeamModal(rerender);
|
|
@@ -11661,27 +11676,10 @@ var appJs = `
|
|
|
11661
11676
|
var myUserId = info.myUserId;
|
|
11662
11677
|
var isCreator = !!info.isCreator;
|
|
11663
11678
|
|
|
11664
|
-
// After leaving/destroying, the left DB is torn down on the backend
|
|
11665
|
-
// (config + credential removed). Switch to another database the
|
|
11666
|
-
// operator still has and navigate off the (now-gone) DB page.
|
|
11667
|
-
var switchAway = function () {
|
|
11668
|
-
return fetchJson('/api/databases').then(function (data) {
|
|
11669
|
-
var current = (data && data.current && data.current.path) || null;
|
|
11670
|
-
var configs = (data && data.configs) || [];
|
|
11671
|
-
var target = configs.filter(function (c) { return c.path !== current; })[0];
|
|
11672
|
-
if (!target) return reloadEverything();
|
|
11673
|
-
return fetchJson('/api/databases/switch', {
|
|
11674
|
-
method: 'POST',
|
|
11675
|
-
headers: { 'content-type': 'application/json' },
|
|
11676
|
-
body: JSON.stringify({ path: target.path }),
|
|
11677
|
-
}).then(function () { return reloadEverything(); });
|
|
11678
|
-
}).then(function () { location.hash = '#/'; renderRoute(); });
|
|
11679
|
-
};
|
|
11680
|
-
|
|
11681
11679
|
var inviteBtn = host.querySelector('[data-act="open-invite"]');
|
|
11682
11680
|
if (inviteBtn) inviteBtn.addEventListener('click', function () {
|
|
11683
11681
|
if (!teamId) { alert('No team is active.'); return; }
|
|
11684
|
-
showInviteByEmailModal(teamId);
|
|
11682
|
+
showInviteByEmailModal(teamId, info);
|
|
11685
11683
|
});
|
|
11686
11684
|
|
|
11687
11685
|
// Inline member list for the active team cloud. Marks "you"; your
|
|
@@ -11705,26 +11703,6 @@ var appJs = `
|
|
|
11705
11703
|
});
|
|
11706
11704
|
});
|
|
11707
11705
|
});
|
|
11708
|
-
// Leave (member) / Destroy team (creator) \u2014 your own row.
|
|
11709
|
-
var selfBtn = membersHost.querySelector('[data-act="leave-self"]');
|
|
11710
|
-
if (selfBtn) selfBtn.addEventListener('click', function () {
|
|
11711
|
-
|
|
11712
|
-
if (isCreator) {
|
|
11713
|
-
if (!confirm('Destroy team "' + (info.teamName || 'this team') + '"? This soft-deletes it on the cloud for everyone.')) return;
|
|
11714
|
-
withBusy(selfBtn, function () {
|
|
11715
|
-
return fetchJson('/api/teams-gui/teams/' + teamId, { method: 'DELETE' })
|
|
11716
|
-
.then(function () { showToast('Team destroyed', {}); return switchAway(); })
|
|
11717
|
-
.catch(function (e) { setMsg('Destroy failed: ' + e.message, false); });
|
|
11718
|
-
});
|
|
11719
|
-
} else {
|
|
11720
|
-
if (!confirm('Leave team "' + (info.teamName || 'this team') + '"?')) return;
|
|
11721
|
-
withBusy(selfBtn, function () {
|
|
11722
|
-
return fetchJson('/api/teams-gui/teams/' + teamId + '/members/' + encodeURIComponent(myUserId), { method: 'DELETE' })
|
|
11723
|
-
.then(function () { showToast('Left the team', {}); return switchAway(); })
|
|
11724
|
-
.catch(function (e) { setMsg('Leave failed: ' + e.message, false); });
|
|
11725
|
-
});
|
|
11726
|
-
}
|
|
11727
|
-
});
|
|
11728
11706
|
}).catch(function () { membersHost.innerHTML = '<div style="font-size:12px;color:var(--text-muted)">Members unavailable.</div>'; });
|
|
11729
11707
|
}
|
|
11730
11708
|
|
|
@@ -12070,13 +12048,11 @@ var appJs = `
|
|
|
12070
12048
|
var rows = members.map(function (m) {
|
|
12071
12049
|
var label = m.name || m.email || '(unknown)';
|
|
12072
12050
|
var isSelf = m.user_id === myUserId;
|
|
12073
|
-
//
|
|
12074
|
-
//
|
|
12051
|
+
// Other rows: Kick, but only the creator may remove other members.
|
|
12052
|
+
// Your own exit (Disconnect for the owner / Leave for a member) lives
|
|
12053
|
+
// in the Danger Zone, not on a member row.
|
|
12075
12054
|
var btn = '';
|
|
12076
|
-
if (isSelf) {
|
|
12077
|
-
btn = '<button class="btn danger-btn" data-act="leave-self">' +
|
|
12078
|
-
(isCreator ? 'Destroy team' : 'Leave') + '</button>';
|
|
12079
|
-
} else if (isCreator) {
|
|
12055
|
+
if (!isSelf && isCreator) {
|
|
12080
12056
|
btn = '<button class="btn danger-btn" data-act="kick">Kick</button>';
|
|
12081
12057
|
}
|
|
12082
12058
|
return '<div class="member-row" data-user-id="' + escapeHtml(m.user_id) + '">' +
|
|
@@ -12091,7 +12067,7 @@ var appJs = `
|
|
|
12091
12067
|
return '<div class="members-list"><h4>Members</h4>' + rows + '</div>';
|
|
12092
12068
|
}
|
|
12093
12069
|
|
|
12094
|
-
function showInviteByEmailModal(teamId) {
|
|
12070
|
+
function showInviteByEmailModal(teamId, info) {
|
|
12095
12071
|
var bodyHtml =
|
|
12096
12072
|
'<div class="field"><label>Invitee email</label>' +
|
|
12097
12073
|
'<input name="invitee_email" type="email" placeholder="bob@example.com" /></div>' +
|
|
@@ -12107,17 +12083,30 @@ var appJs = `
|
|
|
12107
12083
|
method: 'POST',
|
|
12108
12084
|
headers: { 'content-type': 'application/json' },
|
|
12109
12085
|
body: JSON.stringify({ invitee_email: data.invitee_email }),
|
|
12110
|
-
}).then(function (inv) { showInviteTokenModal(inv); });
|
|
12086
|
+
}).then(function (inv) { showInviteTokenModal(inv, info); });
|
|
12111
12087
|
},
|
|
12112
12088
|
});
|
|
12113
12089
|
}
|
|
12114
12090
|
|
|
12115
|
-
function showInviteTokenModal(inv) {
|
|
12091
|
+
function showInviteTokenModal(inv, info) {
|
|
12092
|
+
info = info || {};
|
|
12093
|
+
// The invitee needs the cloud connection string AND the token. Show the
|
|
12094
|
+
// URL with the password MASKED \u2014 redeem never needs the owner's password
|
|
12095
|
+
// (the invitee authenticates with their own credentials).
|
|
12096
|
+
var connStr = info.host
|
|
12097
|
+
? 'postgres://' + (info.user || 'user') + ':****@' + info.host + ':' + (info.port || 5432) + '/' + (info.dbname || '')
|
|
12098
|
+
: '';
|
|
12099
|
+
var connBlock = connStr
|
|
12100
|
+
? '<h4 style="margin:14px 0 4px">Cloud connection</h4>' +
|
|
12101
|
+
'<div class="copy-token" id="copy-conn">' + escapeHtml(connStr) + '</div>' +
|
|
12102
|
+
'<p style="font-size:12px;color:var(--text-muted);margin:4px 0 0">Share this URL with the invitee (password masked). Click to copy.</p>'
|
|
12103
|
+
: '';
|
|
12116
12104
|
var bodyHtml =
|
|
12117
12105
|
'<p style="margin-top:0">Share this token with the invitee (one-time use). It expires at <code>' +
|
|
12118
12106
|
escapeHtml(inv.expires_at || '(no expiry)') + '</code>.</p>' +
|
|
12119
12107
|
'<div class="copy-token" id="copy-token">' + escapeHtml(inv.raw_token) + '</div>' +
|
|
12120
|
-
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:0">Click the token to copy.</p>'
|
|
12108
|
+
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:0">Click the token to copy.</p>' +
|
|
12109
|
+
connBlock;
|
|
12121
12110
|
var handle = showModal('Invitation token', bodyHtml, { primaryLabel: 'Done', onSubmit: function () {} });
|
|
12122
12111
|
var tokenEl = document.getElementById('copy-token');
|
|
12123
12112
|
if (tokenEl) {
|
|
@@ -12128,6 +12117,15 @@ var appJs = `
|
|
|
12128
12117
|
});
|
|
12129
12118
|
});
|
|
12130
12119
|
}
|
|
12120
|
+
var connEl = document.getElementById('copy-conn');
|
|
12121
|
+
if (connEl) {
|
|
12122
|
+
connEl.addEventListener('click', function () {
|
|
12123
|
+
navigator.clipboard.writeText(connStr).then(function () {
|
|
12124
|
+
connEl.textContent = 'Copied!';
|
|
12125
|
+
setTimeout(function () { connEl.textContent = connStr; }, 1200);
|
|
12126
|
+
});
|
|
12127
|
+
});
|
|
12128
|
+
}
|
|
12131
12129
|
// Suppress unused-var on handle
|
|
12132
12130
|
void handle;
|
|
12133
12131
|
}
|
|
@@ -12218,16 +12216,6 @@ var guiAppHtml = `<!doctype html>
|
|
|
12218
12216
|
</div>
|
|
12219
12217
|
</nav>
|
|
12220
12218
|
<main id="content"></main>
|
|
12221
|
-
<aside class="assistant-rail" id="assistant-rail">
|
|
12222
|
-
<div class="rail-resize" id="rail-resize" role="separator" aria-orientation="vertical" title="Drag to resize"></div>
|
|
12223
|
-
<div class="rail-handle" id="rail-handle" title="Expand / collapse"></div>
|
|
12224
|
-
<div class="rail-header">
|
|
12225
|
-
<span class="rail-title">Activity</span>
|
|
12226
|
-
</div>
|
|
12227
|
-
<div class="rail-feed" id="rail-feed">
|
|
12228
|
-
<div class="rail-empty" id="rail-empty">No activity yet. Changes you make will appear here.</div>
|
|
12229
|
-
</div>
|
|
12230
|
-
</aside>
|
|
12231
12219
|
</div>
|
|
12232
12220
|
|
|
12233
12221
|
<div class="drawer-backdrop" id="drawer-backdrop" hidden></div>
|
|
@@ -13303,17 +13291,22 @@ async function resolveTeamContext(db, teamsClient, cloudUrl, candidateTables) {
|
|
|
13303
13291
|
const teamId = identity.team_id;
|
|
13304
13292
|
const creatorUserId = identity.creator_email ? await resolveUserIdByEmail(db, identity.creator_email) : null;
|
|
13305
13293
|
let myUserId = "";
|
|
13294
|
+
let myEmail = "";
|
|
13306
13295
|
try {
|
|
13307
13296
|
const me = await db.get("__lattice_user_identity", "singleton");
|
|
13308
|
-
if (me?.email)
|
|
13297
|
+
if (me?.email) {
|
|
13298
|
+
myEmail = me.email;
|
|
13299
|
+
myUserId = await resolveUserIdByEmail(db, me.email) ?? "";
|
|
13300
|
+
}
|
|
13309
13301
|
} catch {
|
|
13310
13302
|
myUserId = "";
|
|
13311
13303
|
}
|
|
13304
|
+
let savedConn = null;
|
|
13312
13305
|
if (!myUserId) {
|
|
13313
13306
|
try {
|
|
13314
13307
|
const conns = await teamsClient.listConnections();
|
|
13315
|
-
|
|
13316
|
-
myUserId =
|
|
13308
|
+
savedConn = conns.find((c) => c.cloud_url === cloudUrl) ?? conns.find((c) => c.team_id === teamId) ?? null;
|
|
13309
|
+
myUserId = savedConn?.my_user_id ?? "";
|
|
13317
13310
|
} catch {
|
|
13318
13311
|
}
|
|
13319
13312
|
}
|
|
@@ -13332,6 +13325,26 @@ async function resolveTeamContext(db, teamsClient, cloudUrl, candidateTables) {
|
|
|
13332
13325
|
isMember = false;
|
|
13333
13326
|
}
|
|
13334
13327
|
}
|
|
13328
|
+
if (!isMember && (myEmail || savedConn)) {
|
|
13329
|
+
try {
|
|
13330
|
+
const memberRows = await db.query("__lattice_team_members", {
|
|
13331
|
+
filters: [
|
|
13332
|
+
{ col: "team_id", op: "eq", val: teamId },
|
|
13333
|
+
{ col: "deleted_at", op: "isNull" }
|
|
13334
|
+
]
|
|
13335
|
+
});
|
|
13336
|
+
if (memberRows.length > 0) {
|
|
13337
|
+
const matchId = myEmail ? await resolveUserIdByEmail(db, myEmail) : null;
|
|
13338
|
+
if (matchId && memberRows.some((m) => m.user_id === matchId)) {
|
|
13339
|
+
myUserId = matchId;
|
|
13340
|
+
isMember = true;
|
|
13341
|
+
} else if (savedConn) {
|
|
13342
|
+
isMember = true;
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
} catch {
|
|
13346
|
+
}
|
|
13347
|
+
}
|
|
13335
13348
|
if (creatorUserId) {
|
|
13336
13349
|
await reconcileObjectOwners(db, teamId, creatorUserId, candidateTables);
|
|
13337
13350
|
}
|
|
@@ -13602,6 +13615,11 @@ async function appendAudit(db, feed, table, rowId, op, before, after, source = "
|
|
|
13602
13615
|
for (const r of undone) await db.delete("_lattice_gui_audit", r.id);
|
|
13603
13616
|
await db.insert("_lattice_gui_audit", {
|
|
13604
13617
|
id: crypto.randomUUID(),
|
|
13618
|
+
// Set ts explicitly (don't rely on the column DEFAULT — it uses the
|
|
13619
|
+
// SQLite-only `strftime(...)`, which doesn't yield a parseable ISO string
|
|
13620
|
+
// on Postgres, so cloud history rendered "Invalid Date"). Mirrors the
|
|
13621
|
+
// explicit `client_ts` below; adapter-agnostic.
|
|
13622
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13605
13623
|
table_name: table,
|
|
13606
13624
|
row_id: rowId,
|
|
13607
13625
|
operation: op,
|
|
@@ -13623,6 +13641,9 @@ async function recordSchemaAudit(db, feed, table, operation, before, after, summ
|
|
|
13623
13641
|
for (const r of undone) await db.delete("_lattice_gui_audit", r.id);
|
|
13624
13642
|
await db.insert("_lattice_gui_audit", {
|
|
13625
13643
|
id: crypto.randomUUID(),
|
|
13644
|
+
// Explicit ISO ts — see appendAudit (the SQLite-only strftime DEFAULT
|
|
13645
|
+
// rendered "Invalid Date" on the Postgres/cloud path).
|
|
13646
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13626
13647
|
table_name: table,
|
|
13627
13648
|
row_id: null,
|
|
13628
13649
|
operation,
|
|
@@ -13653,10 +13674,32 @@ async function createRow(ctx, table, values) {
|
|
|
13653
13674
|
await emitTeamEnvelope(ctx, table, id, "upsert", row);
|
|
13654
13675
|
return { id, row };
|
|
13655
13676
|
}
|
|
13677
|
+
function storedValueMatches(stored, requested) {
|
|
13678
|
+
if (stored === requested) return true;
|
|
13679
|
+
const storedEmpty = stored === null || stored === void 0 || stored === "";
|
|
13680
|
+
const reqEmpty = requested === null || requested === void 0 || requested === "";
|
|
13681
|
+
if (storedEmpty && reqEmpty) return true;
|
|
13682
|
+
if (typeof requested === "boolean") return Number(stored) === Number(requested);
|
|
13683
|
+
if (typeof requested === "number") return Number(stored) === requested;
|
|
13684
|
+
return String(stored) === String(requested);
|
|
13685
|
+
}
|
|
13686
|
+
function rowsEqual(a, b) {
|
|
13687
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
13688
|
+
for (const k of keys) if (a[k] !== b[k]) return false;
|
|
13689
|
+
return true;
|
|
13690
|
+
}
|
|
13656
13691
|
async function updateRow(ctx, table, id, values) {
|
|
13657
13692
|
const before = await ctx.db.get(table, id);
|
|
13658
13693
|
await ctx.db.update(table, id, values);
|
|
13659
13694
|
const after = await ctx.db.get(table, id);
|
|
13695
|
+
if (before != null && after != null) {
|
|
13696
|
+
const wantedChange = Object.keys(values).some(
|
|
13697
|
+
(k) => !storedValueMatches(before[k], values[k])
|
|
13698
|
+
);
|
|
13699
|
+
if (wantedChange && rowsEqual(before, after)) {
|
|
13700
|
+
throw new Error("Row update did not persist \u2014 the data source may be read-only");
|
|
13701
|
+
}
|
|
13702
|
+
}
|
|
13660
13703
|
await appendAudit(
|
|
13661
13704
|
ctx.db,
|
|
13662
13705
|
ctx.feed,
|
|
@@ -17132,18 +17175,11 @@ async function dashboardPayload(db, configPath, outputDir, teamContext) {
|
|
|
17132
17175
|
if (stale) staleCount += 1;
|
|
17133
17176
|
return { ...t, lastUpdatedAt, stale };
|
|
17134
17177
|
});
|
|
17135
|
-
let recent = [];
|
|
17136
|
-
try {
|
|
17137
|
-
const raw = await db.query("_lattice_gui_audit", { limit: 15 });
|
|
17138
|
-
recent = raw.map(parseAudit).sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, 15).map((e) => ({ table: e.table_name, op: e.operation, rowId: e.row_id, ts: e.ts }));
|
|
17139
|
-
} catch {
|
|
17140
|
-
}
|
|
17141
17178
|
return {
|
|
17142
17179
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17143
17180
|
staleDays: DASHBOARD_STALE_DAYS,
|
|
17144
17181
|
totals: { entities: entities.length, rows: totalRows, stale: staleCount },
|
|
17145
|
-
entities
|
|
17146
|
-
recent
|
|
17182
|
+
entities
|
|
17147
17183
|
};
|
|
17148
17184
|
}
|
|
17149
17185
|
var ROWS_PATH = /^\/api\/tables\/([^/]+)\/rows(?:\/(.+))?$/;
|
|
@@ -18194,6 +18230,14 @@ data: ${JSON.stringify(data)}
|
|
|
18194
18230
|
sendJson(res, { error: `Unknown entity: ${oldName}` }, 400);
|
|
18195
18231
|
return;
|
|
18196
18232
|
}
|
|
18233
|
+
if (isNativeEntity(oldName)) {
|
|
18234
|
+
sendJson(
|
|
18235
|
+
res,
|
|
18236
|
+
{ error: `"${oldName}" is a built-in entity and cannot be modified` },
|
|
18237
|
+
400
|
|
18238
|
+
);
|
|
18239
|
+
return;
|
|
18240
|
+
}
|
|
18197
18241
|
if (!operatorOwnsTable(active.teamContext, oldName)) {
|
|
18198
18242
|
sendJson(res, { error: "Only the table owner can edit this entity" }, 403);
|
|
18199
18243
|
return;
|
|
@@ -18239,6 +18283,14 @@ data: ${JSON.stringify(data)}
|
|
|
18239
18283
|
sendJson(res, { error: `Unknown entity: ${entityName}` }, 400);
|
|
18240
18284
|
return;
|
|
18241
18285
|
}
|
|
18286
|
+
if (isNativeEntity(entityName)) {
|
|
18287
|
+
sendJson(
|
|
18288
|
+
res,
|
|
18289
|
+
{ error: `"${entityName}" is a built-in entity and cannot be modified` },
|
|
18290
|
+
400
|
|
18291
|
+
);
|
|
18292
|
+
return;
|
|
18293
|
+
}
|
|
18242
18294
|
if (!operatorOwnsTable(active.teamContext, entityName)) {
|
|
18243
18295
|
sendJson(res, { error: "Only the table owner can edit this entity" }, 403);
|
|
18244
18296
|
return;
|
|
@@ -18266,12 +18318,22 @@ data: ${JSON.stringify(data)}
|
|
|
18266
18318
|
sendJson(res, { error: "Use \u201CAdd link\u201D to create a relationship column" }, 400);
|
|
18267
18319
|
return;
|
|
18268
18320
|
}
|
|
18321
|
+
const doc = loadConfigDoc(active.configPath);
|
|
18322
|
+
const fieldsNode = doc.getIn(["entities", entityName, "fields"]);
|
|
18323
|
+
if (!fieldsNode || typeof fieldsNode !== "object" || typeof fieldsNode.toJSON !== "function") {
|
|
18324
|
+
sendJson(res, { error: `Cannot add columns to "${entityName}"` }, 400);
|
|
18325
|
+
return;
|
|
18326
|
+
}
|
|
18327
|
+
const existingFields = fieldsNode.toJSON();
|
|
18328
|
+
if (colName in existingFields) {
|
|
18329
|
+
sendJson(res, { error: `Column "${colName}" already exists on ${entityName}` }, 400);
|
|
18330
|
+
return;
|
|
18331
|
+
}
|
|
18269
18332
|
const sqliteType = fieldToSqliteBaseType(colType);
|
|
18270
18333
|
await execSql(
|
|
18271
18334
|
active.db,
|
|
18272
18335
|
`ALTER TABLE "${entityName}" ADD COLUMN "${colName}" ${sqliteType}`
|
|
18273
18336
|
);
|
|
18274
|
-
const doc = loadConfigDoc(active.configPath);
|
|
18275
18337
|
const fieldDef = { type: colType };
|
|
18276
18338
|
if (body.required === true) fieldDef.required = true;
|
|
18277
18339
|
doc.setIn(["entities", entityName, "fields", colName], fieldDef);
|
|
@@ -18298,6 +18360,14 @@ data: ${JSON.stringify(data)}
|
|
|
18298
18360
|
sendJson(res, { error: `Unknown entity: ${entityName}` }, 400);
|
|
18299
18361
|
return;
|
|
18300
18362
|
}
|
|
18363
|
+
if (isNativeEntity(entityName)) {
|
|
18364
|
+
sendJson(
|
|
18365
|
+
res,
|
|
18366
|
+
{ error: `"${entityName}" is a built-in entity and cannot be modified` },
|
|
18367
|
+
400
|
|
18368
|
+
);
|
|
18369
|
+
return;
|
|
18370
|
+
}
|
|
18301
18371
|
if (!operatorOwnsTable(active.teamContext, entityName)) {
|
|
18302
18372
|
sendJson(res, { error: "Only the table owner can edit this entity" }, 403);
|
|
18303
18373
|
return;
|
|
@@ -18320,14 +18390,30 @@ data: ${JSON.stringify(data)}
|
|
|
18320
18390
|
sendJson(res, { error: `"${newCol}" is a reserved system column` }, 400);
|
|
18321
18391
|
return;
|
|
18322
18392
|
}
|
|
18393
|
+
const doc = loadConfigDoc(active.configPath);
|
|
18394
|
+
const fieldsNode = doc.getIn(["entities", entityName, "fields"]);
|
|
18395
|
+
if (!fieldsNode || typeof fieldsNode !== "object" || typeof fieldsNode.toJSON !== "function") {
|
|
18396
|
+
sendJson(res, { error: `Cannot rename columns on "${entityName}"` }, 400);
|
|
18397
|
+
return;
|
|
18398
|
+
}
|
|
18399
|
+
const fieldsObj = fieldsNode.toJSON();
|
|
18400
|
+
if (!(colName in fieldsObj)) {
|
|
18401
|
+
sendJson(res, { error: `Unknown column "${colName}" on ${entityName}` }, 400);
|
|
18402
|
+
return;
|
|
18403
|
+
}
|
|
18404
|
+
if (newCol in fieldsObj) {
|
|
18405
|
+
sendJson(res, { error: `Column "${newCol}" already exists on ${entityName}` }, 400);
|
|
18406
|
+
return;
|
|
18407
|
+
}
|
|
18323
18408
|
await execSql(
|
|
18324
18409
|
active.db,
|
|
18325
18410
|
`ALTER TABLE "${entityName}" RENAME COLUMN "${colName}" TO "${newCol}"`
|
|
18326
18411
|
);
|
|
18327
|
-
const
|
|
18328
|
-
const
|
|
18329
|
-
|
|
18330
|
-
|
|
18412
|
+
const renamedFields = {};
|
|
18413
|
+
for (const k of Object.keys(fieldsObj)) {
|
|
18414
|
+
renamedFields[k === colName ? newCol : k] = fieldsObj[k];
|
|
18415
|
+
}
|
|
18416
|
+
doc.setIn(["entities", entityName, "fields"], renamedFields);
|
|
18331
18417
|
saveConfigDoc(active.configPath, doc);
|
|
18332
18418
|
await disposeActive(active);
|
|
18333
18419
|
active = await openConfig(active.configPath, active.outputDir, autoRender);
|
|
@@ -18569,10 +18655,12 @@ data: ${JSON.stringify(data)}
|
|
|
18569
18655
|
(e) => e.table_name === filterTable || junctionMatchesFilter.has(e.table_name)
|
|
18570
18656
|
);
|
|
18571
18657
|
}
|
|
18572
|
-
const
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18658
|
+
const sessionRows = await active.db.query("_lattice_gui_audit", {
|
|
18659
|
+
filters: [{ col: "session_id", op: "eq", val: sessionId }]
|
|
18660
|
+
});
|
|
18661
|
+
const sessionLive = sessionRows.filter((r) => Number(r.undone) === 0).length;
|
|
18662
|
+
const sessionUndone = sessionRows.length - sessionLive;
|
|
18663
|
+
sendJson(res, { entries, canUndo: sessionLive > 0, canRedo: sessionUndone > 0 });
|
|
18576
18664
|
return;
|
|
18577
18665
|
}
|
|
18578
18666
|
if (method === "POST" && pathname === "/api/history/undo") {
|
package/dist/index.cjs
CHANGED
|
@@ -3560,6 +3560,15 @@ function resolveDbPath(raw, configDir2) {
|
|
|
3560
3560
|
}
|
|
3561
3561
|
return (0, import_node_path11.resolve)(configDir2, raw);
|
|
3562
3562
|
}
|
|
3563
|
+
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
3564
|
+
function warnDeprecatedRef(entity, field, target) {
|
|
3565
|
+
const key = `${entity}.${field}`;
|
|
3566
|
+
if (warnedDeprecatedRefs.has(key)) return;
|
|
3567
|
+
warnedDeprecatedRefs.add(key);
|
|
3568
|
+
console.warn(
|
|
3569
|
+
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3563
3572
|
function entityToTableDef(entityName, entity) {
|
|
3564
3573
|
const rawFields = entity.fields;
|
|
3565
3574
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -3582,6 +3591,7 @@ function entityToTableDef(entityName, entity) {
|
|
|
3582
3591
|
table: field.ref,
|
|
3583
3592
|
foreignKey: fieldName
|
|
3584
3593
|
};
|
|
3594
|
+
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
3585
3595
|
}
|
|
3586
3596
|
}
|
|
3587
3597
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
package/dist/index.js
CHANGED
|
@@ -3436,6 +3436,15 @@ function resolveDbPath(raw, configDir2) {
|
|
|
3436
3436
|
}
|
|
3437
3437
|
return resolve2(configDir2, raw);
|
|
3438
3438
|
}
|
|
3439
|
+
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
3440
|
+
function warnDeprecatedRef(entity, field, target) {
|
|
3441
|
+
const key = `${entity}.${field}`;
|
|
3442
|
+
if (warnedDeprecatedRefs.has(key)) return;
|
|
3443
|
+
warnedDeprecatedRefs.add(key);
|
|
3444
|
+
console.warn(
|
|
3445
|
+
`Lattice: one-to-many \`ref:\` on "${entity}.${field}" \u2192 "${target}" is deprecated in favor of many-to-many junction tables and will be removed in 2.0.`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3439
3448
|
function entityToTableDef(entityName, entity) {
|
|
3440
3449
|
const rawFields = entity.fields;
|
|
3441
3450
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
@@ -3458,6 +3467,7 @@ function entityToTableDef(entityName, entity) {
|
|
|
3458
3467
|
table: field.ref,
|
|
3459
3468
|
foreignKey: fieldName
|
|
3460
3469
|
};
|
|
3470
|
+
warnDeprecatedRef(entityName, fieldName, field.ref);
|
|
3461
3471
|
}
|
|
3462
3472
|
}
|
|
3463
3473
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
package/package.json
CHANGED