latticesql 1.16.2 → 1.16.3
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 +30 -28
- package/dist/cli.js +354 -242
- package/dist/index.cjs +69 -1
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +69 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6643,6 +6643,10 @@ var css = `
|
|
|
6643
6643
|
.dm-graph .gnode-label { fill: var(--text); font-size: 12px; font-weight: 500; }
|
|
6644
6644
|
.dm-graph .gnode-icon { dominant-baseline: middle; }
|
|
6645
6645
|
.dm-graph .gnode:hover .gnode-dot { stroke: var(--text-muted); }
|
|
6646
|
+
/* Share-status stroke (cloud workspaces only): yellow = shared, red = private. */
|
|
6647
|
+
.dm-graph .gnode-shared .gnode-dot { stroke: #eab308; stroke-width: 2; }
|
|
6648
|
+
.dm-graph .gnode-private .gnode-dot { stroke: #ef4444; stroke-width: 2; }
|
|
6649
|
+
/* Selected (green) wins over share status \u2014 higher specificity (.gnode.active). */
|
|
6646
6650
|
.dm-graph .gnode.active .gnode-dot { stroke: var(--accent); stroke-width: 2; }
|
|
6647
6651
|
.dm-graph .gnode.active .gnode-glow { opacity: 0.18; }
|
|
6648
6652
|
.dm-graph .gnode.active .gnode-label { fill: var(--accent); }
|
|
@@ -6656,6 +6660,11 @@ var css = `
|
|
|
6656
6660
|
.dm-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
6657
6661
|
.dm-legend i { width: 16px; height: 0; border-top: 2px solid currentColor; display: inline-block; }
|
|
6658
6662
|
.dm-legend i.dash { border-top-style: dashed; }
|
|
6663
|
+
/* Share-status swatches: filled dots rather than the relationship line. */
|
|
6664
|
+
.dm-legend i.sw { width: 10px; height: 10px; border-top: 0; border-radius: 50%; }
|
|
6665
|
+
.dm-legend i.sw-shared { background: #eab308; }
|
|
6666
|
+
.dm-legend i.sw-private { background: #ef4444; }
|
|
6667
|
+
.dm-legend i.sw-selected { background: var(--accent); }
|
|
6659
6668
|
#dm-panel {
|
|
6660
6669
|
background: var(--surface); border: 1px solid var(--border);
|
|
6661
6670
|
border-radius: 10px; padding: 20px;
|
|
@@ -6954,6 +6963,17 @@ var css = `
|
|
|
6954
6963
|
}
|
|
6955
6964
|
.shared-row:hover, .member-row:hover { background: var(--row-hover); }
|
|
6956
6965
|
.shared-row .table-name { font-family: ui-monospace, monospace; }
|
|
6966
|
+
/* Role/status pills inside the settings-drawer member list, which is not
|
|
6967
|
+
under .team-card \u2014 so the .team-card-scoped .role-tag rules don't reach
|
|
6968
|
+
it. Covers creator / member / and the pending-invitee invited/expired. */
|
|
6969
|
+
.members-list .role-tag {
|
|
6970
|
+
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
|
6971
|
+
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
6972
|
+
background: var(--accent-soft); color: var(--accent);
|
|
6973
|
+
}
|
|
6974
|
+
.members-list .role-tag.role-member { background: #eef0f3; color: var(--text-muted); }
|
|
6975
|
+
.members-list .role-tag.role-expired { background: #fde2e1; color: #b91c1c; }
|
|
6976
|
+
.member-row-pending { opacity: 0.85; }
|
|
6957
6977
|
.teams-empty {
|
|
6958
6978
|
padding: 32px; text-align: center; color: var(--text-muted);
|
|
6959
6979
|
border: 1px dashed var(--border-strong); border-radius: 8px;
|
|
@@ -7089,6 +7109,8 @@ var css = `
|
|
|
7089
7109
|
.fs-context-doc .md-body a { color: var(--accent); }
|
|
7090
7110
|
.fs-field { padding: 12px 0; border-bottom: 1px solid var(--border); }
|
|
7091
7111
|
.fs-field:last-child { border-bottom: none; }
|
|
7112
|
+
/* Inline create-view action row (Save / Cancel). */
|
|
7113
|
+
.fs-create-actions { display: flex; gap: 8px; justify-content: flex-end; max-width: 900px; margin-top: 16px; }
|
|
7092
7114
|
.fs-field-label {
|
|
7093
7115
|
font-size: 11px; color: var(--text-muted); text-transform: uppercase;
|
|
7094
7116
|
letter-spacing: 0.04em; margin-bottom: 4px;
|
|
@@ -8127,11 +8149,11 @@ var appJs = `
|
|
|
8127
8149
|
'</button>';
|
|
8128
8150
|
}).join('');
|
|
8129
8151
|
menu.innerHTML =
|
|
8130
|
-
'<div class="db-section">
|
|
8152
|
+
'<div class="db-section">Workspaces</div>' +
|
|
8131
8153
|
items +
|
|
8132
|
-
'<div class="db-section">New
|
|
8154
|
+
'<div class="db-section">New workspace</div>' +
|
|
8133
8155
|
'<div class="db-create">' +
|
|
8134
|
-
'<button class="btn primary" id="db-create-btn" style="width:100%;">+ New
|
|
8156
|
+
'<button class="btn primary" id="db-create-btn" style="width:100%;">+ New workspace\u2026</button>' +
|
|
8135
8157
|
'</div>';
|
|
8136
8158
|
menu.querySelectorAll('button.db-item').forEach(function (b) {
|
|
8137
8159
|
b.addEventListener('click', function () {
|
|
@@ -8239,7 +8261,10 @@ var appJs = `
|
|
|
8239
8261
|
// Even segment count \u2192 item view; odd \u2192 folder/collection view.
|
|
8240
8262
|
var fsegs = fsParse(hash);
|
|
8241
8263
|
if (fsegs) {
|
|
8242
|
-
|
|
8264
|
+
// #/fs/<table>/new \u2192 inline create view (must precede the even/odd
|
|
8265
|
+
// item-vs-collection heuristic, since [table,'new'] is even-length).
|
|
8266
|
+
if (fsegs[fsegs.length - 1] === 'new') renderFsCreate(content, fsegs);
|
|
8267
|
+
else if (fsegs.length % 2 === 1) renderFsCollection(content, fsegs);
|
|
8243
8268
|
else renderFsItem(content, fsegs);
|
|
8244
8269
|
return;
|
|
8245
8270
|
}
|
|
@@ -8300,10 +8325,14 @@ var appJs = `
|
|
|
8300
8325
|
return dashboardPreferenceRank(a.name) - dashboardPreferenceRank(b.name);
|
|
8301
8326
|
});
|
|
8302
8327
|
if (ents.length === 0) {
|
|
8328
|
+
// Generic, role-agnostic empty state \u2014 the old copy told everyone to
|
|
8329
|
+
// "edit lattice.config.yml / db.define()", which a joined cloud member
|
|
8330
|
+
// cannot act on (they just have nothing shared with them yet).
|
|
8303
8331
|
content.innerHTML =
|
|
8304
8332
|
'<div class="placeholder">' +
|
|
8305
|
-
'<h2>
|
|
8306
|
-
'<p>
|
|
8333
|
+
'<h2>This workspace is empty</h2>' +
|
|
8334
|
+
'<p>There are no tables to show yet. Create one in the Data Model editor, ' +
|
|
8335
|
+
'or \u2014 on a cloud workspace \u2014 ask the owner to share a table with you.</p>' +
|
|
8307
8336
|
'</div>';
|
|
8308
8337
|
return;
|
|
8309
8338
|
}
|
|
@@ -8479,8 +8508,13 @@ var appJs = `
|
|
|
8479
8508
|
return '<input type="password" name="' + escapeHtml(col) + '" value="' +
|
|
8480
8509
|
escapeHtml(value || '') + '" autocomplete="off" />';
|
|
8481
8510
|
}
|
|
8482
|
-
// Multiline for
|
|
8483
|
-
|
|
8511
|
+
// Multiline for ALL long-form fields (matches FS_LONGFORM, the same set
|
|
8512
|
+
// fsValInner renders as markdown) AND any value that already contains a
|
|
8513
|
+
// newline. A single-line <input> normalizes/strips newlines, so a
|
|
8514
|
+
// multi-line markdown value put in one would be silently corrupted on the
|
|
8515
|
+
// next blur (a spurious PATCH) and then re-rendered as mangled markdown
|
|
8516
|
+
// ("huge text"). A <textarea> round-trips the exact text.
|
|
8517
|
+
if (FS_LONGFORM.indexOf(col) >= 0 || (value != null && String(value).indexOf('\\n') >= 0)) {
|
|
8484
8518
|
return '<textarea name="' + escapeHtml(col) + '">' + escapeHtml(value || '') + '</textarea>';
|
|
8485
8519
|
}
|
|
8486
8520
|
return '<input type="text" name="' + escapeHtml(col) + '" value="' + escapeHtml(value || '') + '" />';
|
|
@@ -9214,7 +9248,7 @@ var appJs = `
|
|
|
9214
9248
|
// opens a create form. Related-row folders aren't a place to mint a
|
|
9215
9249
|
// brand-new object, so the tile is top-level only.
|
|
9216
9250
|
var createTile = topLevel
|
|
9217
|
-
? '<a class="fs-tile fs-tile-create" href="
|
|
9251
|
+
? '<a class="fs-tile fs-tile-create" href="' + fsHref([table, 'new']) + '" title="Create a new ' + escapeHtml(d.label) + '">' +
|
|
9218
9252
|
'<div class="fs-tile-icon">\u2795</div>' +
|
|
9219
9253
|
'<div class="fs-tile-label">New ' + escapeHtml(d.label) + '</div>' +
|
|
9220
9254
|
'</a>'
|
|
@@ -9236,11 +9270,6 @@ var appJs = `
|
|
|
9236
9270
|
'<span class="count">' + rows.length + ' item' + (rows.length === 1 ? '' : 's') + '</span>' +
|
|
9237
9271
|
'</div>' +
|
|
9238
9272
|
'<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
|
-
});
|
|
9244
9273
|
});
|
|
9245
9274
|
}).catch(function (err) {
|
|
9246
9275
|
content.innerHTML = '<div class="placeholder"><h2>Failed</h2>' + escapeHtml(err.message) + '</div>';
|
|
@@ -9251,11 +9280,17 @@ var appJs = `
|
|
|
9251
9280
|
// page with blank fields + a Save button, plus a select-menu + "+" for each
|
|
9252
9281
|
// many-to-many link. Reuses fieldFor() (intrinsic + belongsTo) and the
|
|
9253
9282
|
// existing row-create + junction-row endpoints (no new backend).
|
|
9254
|
-
|
|
9283
|
+
// Inline create view (#/fs/<table>/new) \u2014 mirrors renderFsItem's formatted
|
|
9284
|
+
// layout (.fs-doc/.fs-field) with blank fields + Save/Cancel, instead of a
|
|
9285
|
+
// modal. Reuses fieldFor() + the row-create + junction /link endpoints.
|
|
9286
|
+
function renderFsCreate(content, segs) {
|
|
9287
|
+
var table = segs[0];
|
|
9255
9288
|
var t = tableByName(table);
|
|
9256
|
-
if (!t) return;
|
|
9289
|
+
if (!t) { content.innerHTML = '<div class="placeholder">Unknown entity: ' + escapeHtml(table) + '</div>'; return; }
|
|
9290
|
+
var d = displayFor(table);
|
|
9257
9291
|
var bt = belongsToColumns(t);
|
|
9258
9292
|
var juncs = junctionsFor(table);
|
|
9293
|
+
var collectionHref = fsHref([table]);
|
|
9259
9294
|
// Preload FK + junction-remote target rows so the <select> menus populate.
|
|
9260
9295
|
var needed = bt.map(function (b) { return b.rel.table; })
|
|
9261
9296
|
.concat(juncs.map(function (j) { return j.remoteRel.table; }));
|
|
@@ -9282,62 +9317,73 @@ var appJs = `
|
|
|
9282
9317
|
'<button type="button" class="btn fs-link-add">+ Add another</button>' +
|
|
9283
9318
|
'</div></div>';
|
|
9284
9319
|
});
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
var
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9320
|
+
content.innerHTML =
|
|
9321
|
+
'<nav class="fs-crumbs"><a href="#/">Home</a><span class="fs-sep">\u25B8</span>' +
|
|
9322
|
+
'<a href="' + collectionHref + '">' + escapeHtml(d.label) + '</a><span class="fs-sep">\u25B8</span>' +
|
|
9323
|
+
'<span>New</span></nav>' +
|
|
9324
|
+
'<div class="view-header">' +
|
|
9325
|
+
'<span class="entity-icon">' + d.icon + '</span>' +
|
|
9326
|
+
'<h1>New ' + escapeHtml(d.label) + '</h1>' +
|
|
9327
|
+
'</div>' +
|
|
9328
|
+
'<div class="fs-doc fs-create-form">' + fieldsHtml + '</div>' +
|
|
9329
|
+
'<div class="fs-create-actions">' +
|
|
9330
|
+
'<button class="btn" id="fs-create-cancel">Cancel</button>' +
|
|
9331
|
+
'<button class="btn primary" id="fs-create-save">Save</button>' +
|
|
9332
|
+
'</div>';
|
|
9333
|
+
content.querySelectorAll('.fs-link-add').forEach(function (addBtn) {
|
|
9334
|
+
addBtn.addEventListener('click', function () {
|
|
9335
|
+
var stage = addBtn.previousElementSibling; // the .fs-link-stage
|
|
9336
|
+
var firstSel = stage && stage.querySelector('.fs-link-select');
|
|
9337
|
+
if (!firstSel) return;
|
|
9338
|
+
var clone = firstSel.cloneNode(true);
|
|
9339
|
+
clone.value = '';
|
|
9340
|
+
stage.appendChild(clone);
|
|
9341
|
+
});
|
|
9342
|
+
});
|
|
9343
|
+
content.querySelector('#fs-create-cancel').addEventListener('click', function () {
|
|
9344
|
+
location.hash = collectionHref;
|
|
9345
|
+
});
|
|
9346
|
+
var saveBtn = content.querySelector('#fs-create-save');
|
|
9347
|
+
saveBtn.addEventListener('click', function () {
|
|
9348
|
+
var values = {};
|
|
9349
|
+
content.querySelectorAll('.fs-create-form [name]').forEach(function (el) {
|
|
9350
|
+
var v = el.value;
|
|
9351
|
+
if (v !== '' && v != null) values[el.getAttribute('name')] = v;
|
|
9352
|
+
});
|
|
9353
|
+
var links = [];
|
|
9354
|
+
content.querySelectorAll('.fs-link-stage').forEach(function (stage) {
|
|
9355
|
+
var junction = stage.getAttribute('data-junction');
|
|
9356
|
+
var localFk = stage.getAttribute('data-local-fk');
|
|
9357
|
+
var remoteFk = stage.getAttribute('data-remote-fk');
|
|
9358
|
+
stage.querySelectorAll('.fs-link-select').forEach(function (sel) {
|
|
9359
|
+
if (sel.value) links.push({ junction: junction, localFk: localFk, remoteFk: remoteFk, remoteId: sel.value });
|
|
9315
9360
|
});
|
|
9361
|
+
});
|
|
9362
|
+
withBusy(saveBtn, function () {
|
|
9316
9363
|
return rowWrite('POST', '/api/tables/' + encodeURIComponent(table) + '/rows', values).then(function (res) {
|
|
9317
9364
|
var newId = res && (res.id || (res.row && res.row.id));
|
|
9318
9365
|
var chain = Promise.resolve();
|
|
9319
9366
|
links.forEach(function (lk) {
|
|
9320
9367
|
chain = chain.then(function () {
|
|
9321
|
-
//
|
|
9322
|
-
//
|
|
9323
|
-
// idempotent, unlike a raw row insert.
|
|
9368
|
+
// Junction /link endpoint (INSERT OR IGNORE on the two FKs) \u2014
|
|
9369
|
+
// works for pk-less junctions + is idempotent.
|
|
9324
9370
|
var jrow = {};
|
|
9325
9371
|
jrow[lk.localFk] = newId;
|
|
9326
9372
|
jrow[lk.remoteFk] = lk.remoteId;
|
|
9327
9373
|
return rowWrite('POST', '/api/tables/' + encodeURIComponent(lk.junction) + '/link', jrow);
|
|
9328
9374
|
});
|
|
9329
9375
|
});
|
|
9330
|
-
return chain;
|
|
9331
|
-
}).then(function () {
|
|
9376
|
+
return chain.then(function () { return newId; });
|
|
9377
|
+
}).then(function (newId) {
|
|
9332
9378
|
invalidate(table);
|
|
9333
|
-
return refreshEntities()
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
});
|
|
9338
|
-
}
|
|
9379
|
+
return refreshEntities().then(function () {
|
|
9380
|
+
showToast('Created', {});
|
|
9381
|
+
location.hash = newId ? fsHref([table, String(newId)]) : collectionHref;
|
|
9382
|
+
});
|
|
9383
|
+
}).catch(function (err) { showToast('Create failed: ' + err.message, {}); });
|
|
9384
|
+
});
|
|
9339
9385
|
});
|
|
9340
|
-
}).catch(function (err) {
|
|
9386
|
+
}).catch(function (err) { content.innerHTML = '<div class="placeholder"><h2>Failed</h2>' + escapeHtml(err.message) + '</div>'; });
|
|
9341
9387
|
}
|
|
9342
9388
|
|
|
9343
9389
|
// Item view \u2014 one row as a document (click-to-edit) + its relationship folders.
|
|
@@ -9871,6 +9917,11 @@ var appJs = `
|
|
|
9871
9917
|
rowCount: rc,
|
|
9872
9918
|
cols: (meta.columns || []).length,
|
|
9873
9919
|
r: Math.max(11, Math.min(26, 11 + Math.sqrt(rc))),
|
|
9920
|
+
// Share status (cloud workspaces only). ownedByMe is set by the
|
|
9921
|
+
// server solely on cloud workspaces, so its presence flags a cloud
|
|
9922
|
+
// DB; on local DBs share status is N/A (no coloring).
|
|
9923
|
+
shared: meta.shared === true,
|
|
9924
|
+
cloudWorkspace: meta.ownedByMe !== undefined,
|
|
9874
9925
|
x: 0, y: 0, vx: 0, vy: 0,
|
|
9875
9926
|
});
|
|
9876
9927
|
});
|
|
@@ -9961,7 +10012,11 @@ var appJs = `
|
|
|
9961
10012
|
dash + markStart + markEnd + ' opacity="0.7"><title>' + escapeHtml(title) + '</title></line>';
|
|
9962
10013
|
}).join('');
|
|
9963
10014
|
var nodeSvg = nodes.map(function (nd) {
|
|
9964
|
-
|
|
10015
|
+
// Share-status coloring applies only on cloud workspaces (G). On a
|
|
10016
|
+
// local DB share status is N/A, so no extra class \u2192 neutral stroke.
|
|
10017
|
+
var shareCls = nd.cloudWorkspace ? (nd.shared ? ' gnode-shared' : ' gnode-private') : '';
|
|
10018
|
+
var shareTitle = nd.cloudWorkspace ? ' \xB7 ' + (nd.shared ? 'shared' : 'private') : '';
|
|
10019
|
+
return '<g class="gnode' + shareCls + '" data-table="' + escapeHtml(nd.name) + '" transform="translate(' +
|
|
9965
10020
|
nd.x.toFixed(1) + ',' + nd.y.toFixed(1) + ')">' +
|
|
9966
10021
|
'<circle class="gnode-glow" r="' + (nd.r + 8).toFixed(1) + '"/>' +
|
|
9967
10022
|
'<circle class="gnode-dot" r="' + nd.r.toFixed(1) + '"/>' +
|
|
@@ -9969,13 +10024,22 @@ var appJs = `
|
|
|
9969
10024
|
(nd.r * 0.95).toFixed(1) + '">' + nd.icon + '</text>' +
|
|
9970
10025
|
'<text class="gnode-label" y="' + (nd.r + 15).toFixed(1) + '" text-anchor="middle">' +
|
|
9971
10026
|
escapeHtml(nd.label) + '</text>' +
|
|
9972
|
-
'<title>' + escapeHtml(nd.label + ' \xB7 ' + nd.rowCount + ' rows \xB7 ' + nd.cols + ' columns') + '</title>' +
|
|
10027
|
+
'<title>' + escapeHtml(nd.label + ' \xB7 ' + nd.rowCount + ' rows \xB7 ' + nd.cols + ' columns' + shareTitle) + '</title>' +
|
|
9973
10028
|
'</g>';
|
|
9974
10029
|
}).join('');
|
|
10030
|
+
// Share legend entries only make sense on a cloud workspace (where nodes
|
|
10031
|
+
// carry share status). Local DBs show just the relationship key.
|
|
10032
|
+
var anyCloud = nodes.some(function (nd) { return nd.cloudWorkspace; });
|
|
10033
|
+
var shareLegend = anyCloud
|
|
10034
|
+
? '<span><i class="sw sw-shared"></i><span style="color:var(--text-muted)">shared</span></span>' +
|
|
10035
|
+
'<span><i class="sw sw-private"></i><span style="color:var(--text-muted)">private</span></span>' +
|
|
10036
|
+
'<span><i class="sw sw-selected"></i><span style="color:var(--text-muted)">selected</span></span>'
|
|
10037
|
+
: '';
|
|
9975
10038
|
var legend =
|
|
9976
10039
|
'<div class="dm-legend">' +
|
|
9977
10040
|
'<span style="color:' + DM_FK_COLOR + '"><i></i><span style="color:var(--text-muted)">foreign key</span></span>' +
|
|
9978
10041
|
'<span style="color:' + DM_M2M_COLOR + '"><i class="dash"></i><span style="color:var(--text-muted)">many-to-many</span></span>' +
|
|
10042
|
+
shareLegend +
|
|
9979
10043
|
'</div>';
|
|
9980
10044
|
return '<svg class="dm-graph" viewBox="' + vb.join(' ') + '" preserveAspectRatio="xMidYMid meet">' +
|
|
9981
10045
|
defs + '<g class="dm-stage">' + edgeSvg + nodeSvg + '</g></svg>' + legend;
|
|
@@ -10124,7 +10188,7 @@ var appJs = `
|
|
|
10124
10188
|
'</div>' +
|
|
10125
10189
|
'<div class="muted" style="margin-top:14px;font-size:12px;">' +
|
|
10126
10190
|
'New entities get id (uuid PK), name, and deleted_at columns. ' +
|
|
10127
|
-
'Add more columns once the entity exists. On a
|
|
10191
|
+
'Add more columns once the entity exists. On a cloud workspace the ' +
|
|
10128
10192
|
'entity is private to you until you share it.' +
|
|
10129
10193
|
'</div>';
|
|
10130
10194
|
wireEmojiPicker(panel, 'dm-create-icon');
|
|
@@ -10269,20 +10333,20 @@ var appJs = `
|
|
|
10269
10333
|
'</div>'
|
|
10270
10334
|
: '<span class="muted" style="font-size:12px">No other entities to link to.</span>';
|
|
10271
10335
|
|
|
10272
|
-
//
|
|
10273
|
-
//
|
|
10274
|
-
//
|
|
10336
|
+
// Cloud sharing row \u2014 only the owner of a table may toggle its
|
|
10337
|
+
// visibility (t.ownedByMe is set by the server only for cloud
|
|
10338
|
+
// workspaces). Tables shared to me by others, and all local-DB
|
|
10275
10339
|
// tables, show no sharing control.
|
|
10276
10340
|
var canShare = !!(t && t.ownedByMe === true);
|
|
10277
10341
|
var isShared = !!(t && t.shared);
|
|
10278
10342
|
var shareRow = canShare
|
|
10279
|
-
? '<label>
|
|
10343
|
+
? '<label>Cloud sharing</label>' +
|
|
10280
10344
|
'<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
|
|
10281
10345
|
'<button class="btn' + (isShared ? '' : ' primary') + '" id="dm-share-btn">' +
|
|
10282
|
-
(isShared ? '
|
|
10346
|
+
(isShared ? 'Make private' : 'Share with workspace') +
|
|
10283
10347
|
'</button>' +
|
|
10284
10348
|
'<span style="font-size:12px;color:var(--text-muted)">' +
|
|
10285
|
-
(isShared ? 'Visible to
|
|
10349
|
+
(isShared ? 'Visible to everyone on this cloud workspace.' : 'Private to you. Share to make it visible to everyone on this cloud workspace.') +
|
|
10286
10350
|
'</span>' +
|
|
10287
10351
|
'</div>'
|
|
10288
10352
|
: '';
|
|
@@ -10347,7 +10411,7 @@ var appJs = `
|
|
|
10347
10411
|
// so a light in-place refresh reflects it without a full reload.
|
|
10348
10412
|
return dmRefreshPanel(tableName, false);
|
|
10349
10413
|
}).then(function () {
|
|
10350
|
-
showToast(isShared ? 'Unshared "' + tableName + '" from
|
|
10414
|
+
showToast(isShared ? 'Unshared "' + tableName + '" from workspace' : 'Shared "' + tableName + '" with workspace', {});
|
|
10351
10415
|
}).catch(function (e) { showToast('Share update failed: ' + e.message, {}); });
|
|
10352
10416
|
});
|
|
10353
10417
|
});
|
|
@@ -10739,7 +10803,7 @@ var appJs = `
|
|
|
10739
10803
|
backdrop.className = 'modal-backdrop';
|
|
10740
10804
|
backdrop.innerHTML =
|
|
10741
10805
|
'<div class="modal" style="min-width:560px;max-width:640px">' +
|
|
10742
|
-
'<div class="modal-head" id="wiz-head">New
|
|
10806
|
+
'<div class="modal-head" id="wiz-head">New workspace \u2014 step 1 of 3</div>' +
|
|
10743
10807
|
'<div class="modal-body" id="wiz-body"></div>' +
|
|
10744
10808
|
'<div class="modal-foot">' +
|
|
10745
10809
|
'<button class="btn" data-act="cancel">Cancel</button>' +
|
|
@@ -10760,7 +10824,7 @@ var appJs = `
|
|
|
10760
10824
|
var body = backdrop.querySelector('#wiz-body');
|
|
10761
10825
|
var nextBtn = backdrop.querySelector('[data-act="next"]');
|
|
10762
10826
|
var backBtn = backdrop.querySelector('[data-act="back"]');
|
|
10763
|
-
head.textContent = 'New
|
|
10827
|
+
head.textContent = 'New workspace \u2014 step ' + wizState.step + ' of 3';
|
|
10764
10828
|
backBtn.style.display = wizState.step === 1 ? 'none' : '';
|
|
10765
10829
|
nextBtn.textContent = wizState.step === 3 ? 'Create' : 'Next';
|
|
10766
10830
|
if (wizState.step === 1) body.innerHTML = renderStep1();
|
|
@@ -10810,7 +10874,7 @@ var appJs = `
|
|
|
10810
10874
|
'</label>' +
|
|
10811
10875
|
'</div>' +
|
|
10812
10876
|
'<p style="font-size:11px;color:var(--text-muted);margin:6px 0 0">' +
|
|
10813
|
-
'Local databases are single-user SQLite files on your machine. Cloud databases are Postgres, can be shared with invited
|
|
10877
|
+
'Local databases are single-user SQLite files on your machine. Cloud databases are Postgres, can be shared with invited members, and stream realtime updates. Joining connects to a cloud DB you were invited to.' +
|
|
10814
10878
|
'</p>' +
|
|
10815
10879
|
'</div>' +
|
|
10816
10880
|
cloudBlock;
|
|
@@ -10838,7 +10902,7 @@ var appJs = `
|
|
|
10838
10902
|
'<button class="btn" id="wiz-add-entity" style="margin-top:10px">+ Add entity</button>' +
|
|
10839
10903
|
(wizState.kind === 'cloud'
|
|
10840
10904
|
? '<p style="font-size:11px;color:var(--text-muted);margin:10px 0 0">' +
|
|
10841
|
-
'Entities with \u201CShare with cloud\u201D checked are visible to
|
|
10905
|
+
'Entities with \u201CShare with cloud\u201D checked are visible to everyone on the cloud workspace. Unchecked entities live on the cloud DB but stay scoped to your own row links.' +
|
|
10842
10906
|
'</p>'
|
|
10843
10907
|
: '');
|
|
10844
10908
|
}
|
|
@@ -11073,7 +11137,7 @@ var appJs = `
|
|
|
11073
11137
|
'<p style="font-size:12px;color:var(--text-muted);margin:0">' +
|
|
11074
11138
|
'Use the same Postgres URL the inviter used (postgres://\u2026). Your email + display name come from User Settings \u2014 change them there. The email must match the address the invitation was addressed to.' +
|
|
11075
11139
|
'</p>';
|
|
11076
|
-
showModal('Join
|
|
11140
|
+
showModal('Join workspace', bodyHtml, {
|
|
11077
11141
|
primaryLabel: 'Join',
|
|
11078
11142
|
onSubmit: function (scope) {
|
|
11079
11143
|
var data = collectFormValues(scope);
|
|
@@ -11093,7 +11157,7 @@ var appJs = `
|
|
|
11093
11157
|
body: JSON.stringify({ path: path }),
|
|
11094
11158
|
})
|
|
11095
11159
|
.then(function () { return reloadEverything(); })
|
|
11096
|
-
.then(function () { showToast('Joined "' + (res.team && res.team.name || '
|
|
11160
|
+
.then(function () { showToast('Joined "' + (res.team && res.team.name || 'workspace') + '" \u2014 switched to it', {}); });
|
|
11097
11161
|
});
|
|
11098
11162
|
},
|
|
11099
11163
|
});
|
|
@@ -11156,7 +11220,7 @@ var appJs = `
|
|
|
11156
11220
|
host.innerHTML =
|
|
11157
11221
|
'<div class="dbconfig-panel" style="margin-bottom:18px;padding:14px;border:1px solid var(--border);border-radius:8px;background:var(--surface)">' +
|
|
11158
11222
|
'<h3 style="margin:0 0 10px">Identity</h3>' +
|
|
11159
|
-
'<p class="lead" style="margin:0 0 10px">Display name + email used when creating or joining
|
|
11223
|
+
'<p class="lead" style="margin:0 0 10px">Display name + email used when creating or joining cloud workspaces. Saved to ~/.lattice/identity.json and mirrored into the active Lattice.</p>' +
|
|
11160
11224
|
'<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">' +
|
|
11161
11225
|
'<div><label class="field-label">Display name</label><input id="id-display-name" type="text" value="' + escapeHtml(id.display_name || '') + '" style="width:100%"></div>' +
|
|
11162
11226
|
'<div><label class="field-label">Email</label><input id="id-email" type="email" value="' + escapeHtml(id.email || '') + '" style="width:100%"></div>' +
|
|
@@ -11258,8 +11322,8 @@ var appJs = `
|
|
|
11258
11322
|
// Database panel below.
|
|
11259
11323
|
content.innerHTML =
|
|
11260
11324
|
'<div class="teams-page">' +
|
|
11261
|
-
'<h2>
|
|
11262
|
-
'<div id="db-name-host"><div class="placeholder" style="padding:14px">Loading
|
|
11325
|
+
'<h2>Workspace Settings</h2>' +
|
|
11326
|
+
'<div id="db-name-host"><div class="placeholder" style="padding:14px">Loading workspace name\u2026</div></div>' +
|
|
11263
11327
|
'<div id="dbconfig-host"><div class="placeholder" style="padding:18px">Loading database configuration\u2026</div></div>' +
|
|
11264
11328
|
'<div id="data-model-host"><div class="placeholder" style="padding:18px">Loading data model\u2026</div></div>' +
|
|
11265
11329
|
'<div id="db-danger-host"></div>' +
|
|
@@ -11282,8 +11346,8 @@ var appJs = `
|
|
|
11282
11346
|
'<strong style="color:var(--danger)">This cannot be undone.</strong></p>' +
|
|
11283
11347
|
'<p style="margin:0 0 6px;font-size:12px;color:var(--text-muted)">Type <strong>' + escapeHtml(safeLabel) + '</strong> to confirm:</p>' +
|
|
11284
11348
|
'<input id="confirm-db-name" type="text" autocomplete="off" style="width:100%" />';
|
|
11285
|
-
showModal('Delete
|
|
11286
|
-
primaryLabel: 'Delete
|
|
11349
|
+
showModal('Delete workspace', body, {
|
|
11350
|
+
primaryLabel: 'Delete workspace',
|
|
11287
11351
|
primaryClass: 'destructive',
|
|
11288
11352
|
onBody: function (backdrop) {
|
|
11289
11353
|
var input = backdrop.querySelector('#confirm-db-name');
|
|
@@ -11344,7 +11408,7 @@ var appJs = `
|
|
|
11344
11408
|
'<div class="danger-zone">' +
|
|
11345
11409
|
'<h3>Danger zone</h3>' +
|
|
11346
11410
|
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11347
|
-
'Disconnect this database from the cloud. This
|
|
11411
|
+
'Disconnect this database from the cloud. This dissolves the cloud workspace and <strong>kicks all members</strong>. This cannot be undone.' +
|
|
11348
11412
|
'</p>' +
|
|
11349
11413
|
'<button class="btn destructive" id="db-disconnect-btn">Disconnect from cloud</button>' +
|
|
11350
11414
|
'</div>';
|
|
@@ -11365,16 +11429,16 @@ var appJs = `
|
|
|
11365
11429
|
'<div class="danger-zone">' +
|
|
11366
11430
|
'<h3>Danger zone</h3>' +
|
|
11367
11431
|
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11368
|
-
'Leave this
|
|
11432
|
+
'Leave this cloud workspace. It keeps running for everyone else; you simply stop being a member.' +
|
|
11369
11433
|
'</p>' +
|
|
11370
|
-
'<button class="btn destructive" id="db-leave-btn">Leave
|
|
11434
|
+
'<button class="btn destructive" id="db-leave-btn">Leave workspace</button>' +
|
|
11371
11435
|
'</div>';
|
|
11372
11436
|
host.querySelector('#db-leave-btn').addEventListener('click', function () {
|
|
11373
11437
|
if (!confirm('Leave "' + (cfg.teamName || label || 'this team') + '"?')) return;
|
|
11374
11438
|
var lbtn = host.querySelector('#db-leave-btn');
|
|
11375
11439
|
withBusy(lbtn, function () {
|
|
11376
11440
|
return fetchJson('/api/teams-gui/teams/' + cfg.teamId + '/members/' + encodeURIComponent(cfg.myUserId), { method: 'DELETE' })
|
|
11377
|
-
.then(function () { showToast('Left the
|
|
11441
|
+
.then(function () { showToast('Left the workspace', {}); return switchAway(); })
|
|
11378
11442
|
.catch(function (e) { alert('Leave failed: ' + e.message); });
|
|
11379
11443
|
});
|
|
11380
11444
|
});
|
|
@@ -11385,15 +11449,21 @@ var appJs = `
|
|
|
11385
11449
|
'<div class="danger-zone">' +
|
|
11386
11450
|
'<h3>Danger zone</h3>' +
|
|
11387
11451
|
'<p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">' +
|
|
11388
|
-
'Permanently delete this
|
|
11452
|
+
'Permanently delete this workspace. The configuration is removed and, for a local database, the underlying SQLite file is deleted. This cannot be undone.' +
|
|
11389
11453
|
'</p>' +
|
|
11390
|
-
'<button class="btn destructive" id="db-delete-btn">Delete
|
|
11454
|
+
'<button class="btn destructive" id="db-delete-btn">Delete workspace</button>' +
|
|
11391
11455
|
'</div>';
|
|
11392
11456
|
host.querySelector('#db-delete-btn').addEventListener('click', function () {
|
|
11393
11457
|
confirmDeleteDatabase(path, label, function () {
|
|
11394
|
-
// We just deleted the active
|
|
11458
|
+
// We just deleted the active workspace; the server switched to a
|
|
11459
|
+
// fallback. Re-render the drawer's Workspace-settings tab so it
|
|
11460
|
+
// reflects the NEW active workspace \u2014 previously this rendered into
|
|
11461
|
+
// #content behind the open drawer, leaving the user stuck on the
|
|
11462
|
+
// deleted workspace's settings.
|
|
11395
11463
|
return reloadEverything().then(function () {
|
|
11396
|
-
|
|
11464
|
+
var drawer = document.getElementById('settings-drawer');
|
|
11465
|
+
if (drawer && !drawer.hidden) selectDrawerTab('database');
|
|
11466
|
+
else closeSettingsDrawer();
|
|
11397
11467
|
});
|
|
11398
11468
|
});
|
|
11399
11469
|
});
|
|
@@ -11430,9 +11500,9 @@ var appJs = `
|
|
|
11430
11500
|
(canRename
|
|
11431
11501
|
? ('Friendly database name shown in the topbar and the dropdown. ' +
|
|
11432
11502
|
(isCloud
|
|
11433
|
-
? 'For cloud databases, the rename is broadcast to every
|
|
11503
|
+
? 'For cloud databases, the rename is broadcast to every member in realtime.'
|
|
11434
11504
|
: 'Saved to the YAML config\\'s name: key.'))
|
|
11435
|
-
: 'Only the
|
|
11505
|
+
: 'Only the workspace owner can rename this cloud database.') +
|
|
11436
11506
|
'</p>' +
|
|
11437
11507
|
'<div id="db-name-msg" style="margin-top:6px;font-size:12px;color:var(--text-muted)"></div>' +
|
|
11438
11508
|
'</div>';
|
|
@@ -11480,12 +11550,12 @@ var appJs = `
|
|
|
11480
11550
|
? (current.kind === 'cloud' ? 'Cloud (Postgres)' : 'Local (SQLite)')
|
|
11481
11551
|
: '\u2014';
|
|
11482
11552
|
var rowLabel = c.label || c.name;
|
|
11483
|
-
|
|
11553
|
+
// No per-row Delete / Action column (1.16.3) \u2014 rows are click-to-switch;
|
|
11554
|
+
// deletion lives in Workspace Settings \u2192 Danger Zone.
|
|
11484
11555
|
return '<tr' + (c.active ? '' : ' class="ws-row" data-switch-path="' + escapeHtml(c.path) + '"') + '>' +
|
|
11485
11556
|
'<td>' + escapeHtml(rowLabel) + (c.active ? ' <span class="role-tag">active</span>' : '') + '</td>' +
|
|
11486
11557
|
'<td>' + kind + '</td>' +
|
|
11487
11558
|
'<td><code>' + escapeHtml(c.dbFile || '') + '</code></td>' +
|
|
11488
|
-
'<td>' + del + '</td>' +
|
|
11489
11559
|
'</tr>';
|
|
11490
11560
|
}).join('');
|
|
11491
11561
|
host.innerHTML =
|
|
@@ -11495,8 +11565,8 @@ var appJs = `
|
|
|
11495
11565
|
'<button class="btn primary" id="action-add-db">+ Add new workspace</button>' +
|
|
11496
11566
|
'</div>' +
|
|
11497
11567
|
'<table style="width:100%;border-collapse:collapse">' +
|
|
11498
|
-
'<thead><tr style="text-align:left"><th>Name</th><th>Kind</th><th>File / source</th
|
|
11499
|
-
'<tbody>' + (rows || '<tr><td colspan="
|
|
11568
|
+
'<thead><tr style="text-align:left"><th>Name</th><th>Kind</th><th>File / source</th></tr></thead>' +
|
|
11569
|
+
'<tbody>' + (rows || '<tr><td colspan="3" style="padding:8px;color:var(--text-muted)">No workspaces configured.</td></tr>') + '</tbody>' +
|
|
11500
11570
|
'</table>' +
|
|
11501
11571
|
'</div>';
|
|
11502
11572
|
host.querySelectorAll('tr.ws-row[data-switch-path]').forEach(function (row) {
|
|
@@ -11508,22 +11578,6 @@ var appJs = `
|
|
|
11508
11578
|
.then(function () { renderLatticeSettings(document.getElementById('content')); });
|
|
11509
11579
|
});
|
|
11510
11580
|
});
|
|
11511
|
-
host.querySelectorAll('[data-delete-path]').forEach(function (btn) {
|
|
11512
|
-
btn.addEventListener('click', function (e) {
|
|
11513
|
-
e.stopPropagation(); // don't trigger the row's switch handler
|
|
11514
|
-
confirmDeleteDatabase(
|
|
11515
|
-
btn.getAttribute('data-delete-path'),
|
|
11516
|
-
btn.getAttribute('data-delete-label'),
|
|
11517
|
-
function () {
|
|
11518
|
-
// Deleting any row may have switched the active DB (if it was
|
|
11519
|
-
// the active one); refetch everything, then re-render the list.
|
|
11520
|
-
return reloadEverything().then(function () {
|
|
11521
|
-
renderLatticeSettings(document.getElementById('content'));
|
|
11522
|
-
});
|
|
11523
|
-
},
|
|
11524
|
-
);
|
|
11525
|
-
});
|
|
11526
|
-
});
|
|
11527
11581
|
host.querySelector('#action-add-db').addEventListener('click', showCreateDatabaseWizard);
|
|
11528
11582
|
}).catch(function (err) {
|
|
11529
11583
|
host.innerHTML = '<div class="placeholder">Failed to load databases: ' + escapeHtml(err.message) + '</div>';
|
|
@@ -11562,20 +11616,16 @@ var appJs = `
|
|
|
11562
11616
|
label = 'LOCAL';
|
|
11563
11617
|
color = 'var(--text-muted)';
|
|
11564
11618
|
break;
|
|
11565
|
-
case 'cloud-connected':
|
|
11566
|
-
label = 'CLOUD \xB7 CONNECTED';
|
|
11567
|
-
color = 'var(--accent)';
|
|
11568
|
-
break;
|
|
11569
11619
|
case 'team-cloud-creator':
|
|
11570
|
-
label = '\u{1F451}
|
|
11620
|
+
label = '\u{1F451} CLOUD \xB7 OWNER';
|
|
11571
11621
|
color = 'var(--accent)';
|
|
11572
11622
|
break;
|
|
11573
11623
|
case 'team-cloud-member':
|
|
11574
|
-
label = '
|
|
11624
|
+
label = 'CLOUD \xB7 MEMBER';
|
|
11575
11625
|
color = 'var(--accent)';
|
|
11576
11626
|
break;
|
|
11577
11627
|
case 'team-cloud-needs-invite':
|
|
11578
|
-
label = '
|
|
11628
|
+
label = 'CLOUD \xB7 NEEDS INVITE';
|
|
11579
11629
|
color = 'var(--warn)';
|
|
11580
11630
|
break;
|
|
11581
11631
|
default:
|
|
@@ -11597,24 +11647,16 @@ var appJs = `
|
|
|
11597
11647
|
'</div>'
|
|
11598
11648
|
);
|
|
11599
11649
|
}
|
|
11600
|
-
if (info.state === 'cloud-connected') {
|
|
11601
|
-
return (
|
|
11602
|
-
renderConnectionSummary(info) +
|
|
11603
|
-
'<div class="team-actions" style="margin-top:10px">' +
|
|
11604
|
-
'<button class="btn primary" data-act="open-upgrade">Upgrade to team cloud \u2192</button>' +
|
|
11605
|
-
'</div>'
|
|
11606
|
-
);
|
|
11607
|
-
}
|
|
11608
11650
|
if (info.state === 'team-cloud-creator' || info.state === 'team-cloud-member') {
|
|
11609
|
-
var
|
|
11651
|
+
var isOwner = info.state === 'team-cloud-creator';
|
|
11610
11652
|
return (
|
|
11611
11653
|
renderConnectionSummary(info) +
|
|
11612
11654
|
'<div style="margin-top:10px;font-size:13px">' +
|
|
11613
|
-
'<strong>
|
|
11614
|
-
(
|
|
11655
|
+
'<strong>Cloud workspace:</strong> ' + escapeHtml(info.teamName || '(unnamed)') +
|
|
11656
|
+
(isOwner ? ' \xB7 <span style="color:var(--accent)">you are the owner</span>' : ' \xB7 <span style="color:var(--text-muted)">member</span>') +
|
|
11615
11657
|
'</div>' +
|
|
11616
11658
|
'<div class="team-actions" style="margin-top:10px">' +
|
|
11617
|
-
(
|
|
11659
|
+
(isOwner ? '<button class="btn primary" data-act="open-invite">Invite member</button>' : '') +
|
|
11618
11660
|
'</div>' +
|
|
11619
11661
|
// Exit actions (Disconnect for the owner / Leave for a member) live
|
|
11620
11662
|
// in the Danger Zone below \u2014 not on a member row.
|
|
@@ -11625,14 +11667,14 @@ var appJs = `
|
|
|
11625
11667
|
return (
|
|
11626
11668
|
renderConnectionSummary(info) +
|
|
11627
11669
|
'<p style="margin-top:10px;color:var(--warn);font-size:13px">' +
|
|
11628
|
-
'
|
|
11670
|
+
'Not a member of this cloud workspace yet \u2014 paste your invite token to join.' +
|
|
11629
11671
|
'</p>' +
|
|
11630
11672
|
'<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:6px">' +
|
|
11631
11673
|
'<div><label class="field-label">Invite token</label>' +
|
|
11632
11674
|
'<textarea id="db-rejoin-token" placeholder="latinv_..." style="width:100%;height:54px;font-family:JetBrains Mono,monospace"></textarea></div>' +
|
|
11633
11675
|
'</div>' +
|
|
11634
11676
|
'<div class="team-actions" style="margin-top:10px">' +
|
|
11635
|
-
'<button class="btn primary" data-act="rejoin-with-token">Join
|
|
11677
|
+
'<button class="btn primary" data-act="rejoin-with-token">Join workspace \u2192</button>' +
|
|
11636
11678
|
'</div>'
|
|
11637
11679
|
);
|
|
11638
11680
|
}
|
|
@@ -11662,11 +11704,6 @@ var appJs = `
|
|
|
11662
11704
|
showMigrateToCloudModal(rerender);
|
|
11663
11705
|
});
|
|
11664
11706
|
|
|
11665
|
-
var upgradeBtn = host.querySelector('[data-act="open-upgrade"]');
|
|
11666
|
-
if (upgradeBtn) upgradeBtn.addEventListener('click', function () {
|
|
11667
|
-
showUpgradeToTeamModal(rerender);
|
|
11668
|
-
});
|
|
11669
|
-
|
|
11670
11707
|
// team_id / my_user_id / isCreator come from /api/dbconfig (info),
|
|
11671
11708
|
// resolved against the ACTIVE cloud DB \u2014 not a local connection row
|
|
11672
11709
|
// (which doesn't exist when the team cloud itself is active). This
|
|
@@ -11687,15 +11724,21 @@ var appJs = `
|
|
|
11687
11724
|
// rows carry Kick, shown only to the creator.
|
|
11688
11725
|
var membersHost = host.querySelector('#db-members-host');
|
|
11689
11726
|
if (membersHost && teamId && (info.state === 'team-cloud-creator' || info.state === 'team-cloud-member')) {
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
11727
|
+
Promise.all([
|
|
11728
|
+
fetchJson('/api/teams-gui/teams/' + teamId + '/members'),
|
|
11729
|
+
// Pending invitees (I). Resilient: an older cloud without the GET
|
|
11730
|
+
// invitations route shouldn't blank the whole member list.
|
|
11731
|
+
fetchJson('/api/teams-gui/teams/' + teamId + '/invitations').catch(function () { return { invitations: [] }; }),
|
|
11732
|
+
]).then(function (results) {
|
|
11733
|
+
var members = (results[0] && results[0].members) || [];
|
|
11734
|
+
var invitations = (results[1] && results[1].invitations) || [];
|
|
11735
|
+
membersHost.innerHTML = renderMembersList(members, myUserId, isCreator, invitations);
|
|
11693
11736
|
// Kick another member (creator only).
|
|
11694
11737
|
membersHost.querySelectorAll('[data-act="kick"]').forEach(function (btn) {
|
|
11695
11738
|
var row = btn.closest('[data-user-id]');
|
|
11696
11739
|
var userId = row && row.getAttribute('data-user-id');
|
|
11697
11740
|
btn.addEventListener('click', function () {
|
|
11698
|
-
if (!confirm('Remove this member from the
|
|
11741
|
+
if (!confirm('Remove this member from the workspace?')) return;
|
|
11699
11742
|
withBusy(btn, function () {
|
|
11700
11743
|
return fetchJson('/api/teams-gui/teams/' + teamId + '/members/' + encodeURIComponent(userId), { method: 'DELETE' })
|
|
11701
11744
|
.then(function () { rerender(); })
|
|
@@ -11714,7 +11757,7 @@ var appJs = `
|
|
|
11714
11757
|
// call the connect-existing endpoint with just the invite
|
|
11715
11758
|
// token. The handler reads credentials from db-credentials.enc
|
|
11716
11759
|
// via the active configPath's label.
|
|
11717
|
-
setMsg('Joining
|
|
11760
|
+
setMsg('Joining workspace\u2026');
|
|
11718
11761
|
fetch('/api/dbconfig/connect-existing', {
|
|
11719
11762
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
11720
11763
|
body: JSON.stringify({
|
|
@@ -11877,7 +11920,7 @@ var appJs = `
|
|
|
11877
11920
|
'<div style="margin-top:14px;padding:10px;border:1px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02)">' +
|
|
11878
11921
|
'<div style="font-size:12px;color:var(--text);text-transform:uppercase;letter-spacing:0.04em;font-weight:500;margin-bottom:6px">Share with cloud</div>' +
|
|
11879
11922
|
'<p style="margin:0 0 8px;font-size:12px;color:var(--text-muted)">' +
|
|
11880
|
-
'Checked tables become visible to every
|
|
11923
|
+
'Checked tables become visible to every member you invite. Uncheck any you want to keep ' +
|
|
11881
11924
|
'cloud-stored but unshared. You can change this later from Data Model.' +
|
|
11882
11925
|
'</p>' +
|
|
11883
11926
|
shareRows +
|
|
@@ -11900,7 +11943,7 @@ var appJs = `
|
|
|
11900
11943
|
return probeBeforeCredentialSave(body, msg).then(function (probe) {
|
|
11901
11944
|
if (probe.teamEnabled) {
|
|
11902
11945
|
throw new Error(
|
|
11903
|
-
'Target is already a cloud
|
|
11946
|
+
'Target is already a cloud workspace' +
|
|
11904
11947
|
(probe.teamName ? ' (' + probe.teamName + ')' : '') +
|
|
11905
11948
|
'. Migrate-to-cloud only works against fresh empty targets.'
|
|
11906
11949
|
);
|
|
@@ -11960,7 +12003,7 @@ var appJs = `
|
|
|
11960
12003
|
'<code>lattice.config.yml</code>\\'s <code>db:</code> line or via the ' +
|
|
11961
12004
|
'Databases catalog under User Config. If you want to <em>push</em> ' +
|
|
11962
12005
|
'your local rows into the target instead, use Migrate to cloud. If ' +
|
|
11963
|
-
'the target is a
|
|
12006
|
+
'the target is a shared cloud workspace you\\'ll be asked for an invite token ' +
|
|
11964
12007
|
'after the probe.' +
|
|
11965
12008
|
'</p>' +
|
|
11966
12009
|
postgresFormHtml({}) +
|
|
@@ -11982,7 +12025,7 @@ var appJs = `
|
|
|
11982
12025
|
var zone = document.getElementById('w-team-zone');
|
|
11983
12026
|
zone.innerHTML =
|
|
11984
12027
|
'<div style="padding:10px;background:rgba(251,146,60,0.08);border:1px solid var(--warn);border-radius:6px">' +
|
|
11985
|
-
'<p style="margin:0 0 8px;font-size:13px;color:var(--warn)">Target is a
|
|
12028
|
+
'<p style="margin:0 0 8px;font-size:13px;color:var(--warn)">Target is a shared cloud workspace' +
|
|
11986
12029
|
(probe.teamName ? ' (<strong>' + escapeHtml(probe.teamName) + '</strong>)' : '') +
|
|
11987
12030
|
'. Paste your invite token to join:</p>' +
|
|
11988
12031
|
'<textarea id="w-invite-token" placeholder="latinv_..." style="width:100%;height:54px;font-family:JetBrains Mono,monospace"></textarea>' +
|
|
@@ -12016,35 +12059,11 @@ var appJs = `
|
|
|
12016
12059
|
});
|
|
12017
12060
|
}
|
|
12018
12061
|
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
'Upgrade this cloud DB to a team DB by registering as the founding member. ' +
|
|
12023
|
-
'Your display name + email from <strong>User Config \u2192 Identity</strong> are used.' +
|
|
12024
|
-
'</p>' +
|
|
12025
|
-
'<div><label class="field-label">Team name</label>' +
|
|
12026
|
-
'<input type="text" id="w-team-name" placeholder="Atlas" style="width:100%"></div>' +
|
|
12027
|
-
'<div id="w-msg" style="margin-top:10px;font-size:12px;color:var(--text-muted)"></div>';
|
|
12028
|
-
showModal('Upgrade to team cloud', bodyHtml, {
|
|
12029
|
-
primaryLabel: 'Upgrade \u2192',
|
|
12030
|
-
onSubmit: function () {
|
|
12031
|
-
var teamName = (document.getElementById('w-team-name').value || '').trim();
|
|
12032
|
-
if (!teamName) throw new Error('Team name is required.');
|
|
12033
|
-
var msg = document.getElementById('w-msg');
|
|
12034
|
-
msg.textContent = 'Upgrading\u2026';
|
|
12035
|
-
return fetch('/api/dbconfig/upgrade-to-team', {
|
|
12036
|
-
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ team_name: teamName }),
|
|
12037
|
-
})
|
|
12038
|
-
.then(function (r) { return r.json().then(function (d) { return { status: r.status, body: d }; }); })
|
|
12039
|
-
.then(function (r) {
|
|
12040
|
-
if (!r.body.ok) throw new Error(r.body.error || ('HTTP ' + r.status));
|
|
12041
|
-
if (onClose) onClose();
|
|
12042
|
-
});
|
|
12043
|
-
},
|
|
12044
|
-
});
|
|
12045
|
-
}
|
|
12062
|
+
// (Removed in 1.16.3) The standalone upgrade-to-cloud-sharing modal is
|
|
12063
|
+
// gone; cloud workspaces initialize their member/share machinery
|
|
12064
|
+
// automatically (see TeamsClient.ensureCloudWorkspaceIdentity).
|
|
12046
12065
|
|
|
12047
|
-
function renderMembersList(members, myUserId, isCreator) {
|
|
12066
|
+
function renderMembersList(members, myUserId, isCreator, invitations) {
|
|
12048
12067
|
var rows = members.map(function (m) {
|
|
12049
12068
|
var label = m.name || m.email || '(unknown)';
|
|
12050
12069
|
var isSelf = m.user_id === myUserId;
|
|
@@ -12064,7 +12083,22 @@ var appJs = `
|
|
|
12064
12083
|
btn +
|
|
12065
12084
|
'</div>';
|
|
12066
12085
|
}).join('');
|
|
12067
|
-
|
|
12086
|
+
// Pending (unredeemed) invitations \u2014 shown below active members so the
|
|
12087
|
+
// owner can see who's been invited but hasn't joined yet (I).
|
|
12088
|
+
var pending = (invitations || []).filter(function (iv) { return iv && iv.invitee_email; });
|
|
12089
|
+
var pendingHtml = pending.length
|
|
12090
|
+
? '<h4 style="margin-top:14px">Pending invitations</h4>' +
|
|
12091
|
+
pending.map(function (iv) {
|
|
12092
|
+
return '<div class="member-row member-row-pending">' +
|
|
12093
|
+
'<span style="color:var(--text-muted)">' + escapeHtml(iv.invitee_email) +
|
|
12094
|
+
' <span class="role-tag' + (iv.expired ? ' role-expired' : ' role-member') + '">' +
|
|
12095
|
+
(iv.expired ? 'expired' : 'invited') +
|
|
12096
|
+
'</span>' +
|
|
12097
|
+
'</span>' +
|
|
12098
|
+
'</div>';
|
|
12099
|
+
}).join('')
|
|
12100
|
+
: '';
|
|
12101
|
+
return '<div class="members-list"><h4>Members</h4>' + rows + pendingHtml + '</div>';
|
|
12068
12102
|
}
|
|
12069
12103
|
|
|
12070
12104
|
function showInviteByEmailModal(teamId, info) {
|
|
@@ -12225,7 +12259,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
12225
12259
|
<button class="drawer-close" id="drawer-close" title="Close" aria-label="Close settings">\u2715</button>
|
|
12226
12260
|
</div>
|
|
12227
12261
|
<div class="drawer-tabs" id="drawer-tabs">
|
|
12228
|
-
<button class="drawer-tab" data-tab="database">
|
|
12262
|
+
<button class="drawer-tab" data-tab="database">Workspace</button>
|
|
12229
12263
|
<button class="drawer-tab" data-tab="lattice">Lattice</button>
|
|
12230
12264
|
<button class="drawer-tab" data-tab="user">User</button>
|
|
12231
12265
|
</div>
|
|
@@ -12864,6 +12898,22 @@ async function listTeamMembers(db, teamId) {
|
|
|
12864
12898
|
}
|
|
12865
12899
|
return out;
|
|
12866
12900
|
}
|
|
12901
|
+
async function listPendingInvitations(db, teamId) {
|
|
12902
|
+
const rows = await db.query("__lattice_invitations", {
|
|
12903
|
+
filters: [
|
|
12904
|
+
{ col: "team_id", op: "eq", val: teamId },
|
|
12905
|
+
{ col: "redeemed_at", op: "isNull" }
|
|
12906
|
+
]
|
|
12907
|
+
});
|
|
12908
|
+
const nowMs = Date.now();
|
|
12909
|
+
return rows.map((r) => ({
|
|
12910
|
+
id: r.id,
|
|
12911
|
+
invitee_email: r.invitee_email,
|
|
12912
|
+
invited_at: r.created_at,
|
|
12913
|
+
expires_at: r.expires_at ?? null,
|
|
12914
|
+
expired: r.expires_at != null && new Date(r.expires_at).getTime() < nowMs
|
|
12915
|
+
})).sort((a, b) => a.invited_at < b.invited_at ? 1 : a.invited_at > b.invited_at ? -1 : 0);
|
|
12916
|
+
}
|
|
12867
12917
|
async function appendChangeEnvelope(db, entry) {
|
|
12868
12918
|
const rows = await db.query("__lattice_change_log", {
|
|
12869
12919
|
filters: [{ col: "team_id", op: "eq", val: entry.team_id }],
|
|
@@ -12984,6 +13034,9 @@ async function unshareObject(db, teamId, table) {
|
|
|
12984
13034
|
async function listMembersDirect(db, teamId) {
|
|
12985
13035
|
return listTeamMembers(db, teamId);
|
|
12986
13036
|
}
|
|
13037
|
+
async function listPendingInvitationsDirect(db, teamId) {
|
|
13038
|
+
return listPendingInvitations(db, teamId);
|
|
13039
|
+
}
|
|
12987
13040
|
async function inviteDirect(db, teamId, inviterUserId, inviteeEmail, expiresInHours = 7 * 24) {
|
|
12988
13041
|
const team = await db.get("__lattice_team", teamId);
|
|
12989
13042
|
if (!team || team.deleted_at) {
|
|
@@ -13984,6 +14037,10 @@ async function dispatchTeamRoute(req, res, ctx) {
|
|
|
13984
14037
|
await handleCreateInvitation(req, res, ctx, invitationsMatch[1] ?? "");
|
|
13985
14038
|
return true;
|
|
13986
14039
|
}
|
|
14040
|
+
if (invitationsMatch && method === "GET") {
|
|
14041
|
+
await handleListInvitations(res, ctx, invitationsMatch[1] ?? "");
|
|
14042
|
+
return true;
|
|
14043
|
+
}
|
|
13987
14044
|
const objectsListMatch = /^\/api\/teams\/([^/]+)\/objects$/.exec(pathname);
|
|
13988
14045
|
if (objectsListMatch) {
|
|
13989
14046
|
if (method === "POST") {
|
|
@@ -14260,6 +14317,18 @@ async function handleListMembers(res, ctx, teamId) {
|
|
|
14260
14317
|
}
|
|
14261
14318
|
sendJson2(res, { members: await listTeamMembers(ctx.db, teamId) });
|
|
14262
14319
|
}
|
|
14320
|
+
async function handleListInvitations(res, ctx, teamId) {
|
|
14321
|
+
if (!ctx.authContext) {
|
|
14322
|
+
sendJson2(res, { error: "Unauthorized" }, 401);
|
|
14323
|
+
return;
|
|
14324
|
+
}
|
|
14325
|
+
const role = await getMembershipRole(ctx.db, teamId, ctx.authContext.user.id);
|
|
14326
|
+
if (!role) {
|
|
14327
|
+
sendJson2(res, { error: "Not a member of this team" }, 403);
|
|
14328
|
+
return;
|
|
14329
|
+
}
|
|
14330
|
+
sendJson2(res, { invitations: await listPendingInvitations(ctx.db, teamId) });
|
|
14331
|
+
}
|
|
14263
14332
|
async function handleKickMember(res, ctx, teamId, userId) {
|
|
14264
14333
|
if (!ctx.authContext) {
|
|
14265
14334
|
sendJson2(res, { error: "Unauthorized" }, 401);
|
|
@@ -14970,6 +15039,42 @@ var TeamsClient = class {
|
|
|
14970
15039
|
});
|
|
14971
15040
|
return reg;
|
|
14972
15041
|
}
|
|
15042
|
+
/**
|
|
15043
|
+
* Idempotently initialize a cloud Postgres DB as a collaborative cloud
|
|
15044
|
+
* workspace (members + sharing). 1.16.3 deprecated the user-facing "team"
|
|
15045
|
+
* concept and the explicit "upgrade to team" step — every cloud workspace
|
|
15046
|
+
* gets this machinery automatically at migrate / connect / open time, so the
|
|
15047
|
+
* members + per-table sharing surface is always available on a cloud DB.
|
|
15048
|
+
*
|
|
15049
|
+
* No-op (returns created:false) when the cloud already carries an identity.
|
|
15050
|
+
* On a fresh cloud the caller becomes the owner. Race-safe: a concurrent
|
|
15051
|
+
* initializer that wins the singleton insert is treated as success.
|
|
15052
|
+
*/
|
|
15053
|
+
async ensureCloudWorkspaceIdentity(opts) {
|
|
15054
|
+
const probe = await probeCloud(opts.cloudUrl);
|
|
15055
|
+
if (!probe.reachable) {
|
|
15056
|
+
throw new Error(`Cloud DB unreachable: ${probe.error ?? "unknown error"}`);
|
|
15057
|
+
}
|
|
15058
|
+
if (probe.teamEnabled) return { created: false };
|
|
15059
|
+
if (!opts.email) {
|
|
15060
|
+
throw new Error("Set your email in User settings to set up this cloud workspace.");
|
|
15061
|
+
}
|
|
15062
|
+
try {
|
|
15063
|
+
const displayName = opts.displayName?.trim() ? opts.displayName : opts.email;
|
|
15064
|
+
await this.upgradeToTeamCloud({
|
|
15065
|
+
label: opts.label,
|
|
15066
|
+
cloudUrl: opts.cloudUrl,
|
|
15067
|
+
teamName: opts.workspaceName,
|
|
15068
|
+
email: opts.email,
|
|
15069
|
+
displayName
|
|
15070
|
+
});
|
|
15071
|
+
return { created: true };
|
|
15072
|
+
} catch (e) {
|
|
15073
|
+
const msg = e.message || "";
|
|
15074
|
+
if (/already has (a team|users)/i.test(msg)) return { created: false };
|
|
15075
|
+
throw e;
|
|
15076
|
+
}
|
|
15077
|
+
}
|
|
14973
15078
|
// ── Cloud team operations (dispatch on URL scheme) ──────────────────────
|
|
14974
15079
|
// For HTTP cloud URLs (`http://lattice-server:port`), every operation
|
|
14975
15080
|
// round-trips through the team server's authenticated REST API. For
|
|
@@ -15003,6 +15108,18 @@ var TeamsClient = class {
|
|
|
15003
15108
|
);
|
|
15004
15109
|
return r.members;
|
|
15005
15110
|
}
|
|
15111
|
+
async listPendingInvitations(cloudUrl, token, teamId) {
|
|
15112
|
+
if (isPostgresUrl(cloudUrl)) {
|
|
15113
|
+
return listPendingInvitationsDirect(this.local, teamId);
|
|
15114
|
+
}
|
|
15115
|
+
const r = await this.fetchAuthed(
|
|
15116
|
+
cloudUrl,
|
|
15117
|
+
token,
|
|
15118
|
+
"GET",
|
|
15119
|
+
`/api/teams/${teamId}/invitations`
|
|
15120
|
+
);
|
|
15121
|
+
return r.invitations;
|
|
15122
|
+
}
|
|
15006
15123
|
async invite(cloudUrl, token, teamId, inviteeEmail, expiresInHours, inviterUserId) {
|
|
15007
15124
|
if (isPostgresUrl(cloudUrl)) {
|
|
15008
15125
|
if (!inviterUserId) {
|
|
@@ -15907,6 +16024,15 @@ async function dispatchTeamSubroute(req, res, ctx, teamId, subpath) {
|
|
|
15907
16024
|
sendJson(res, { members });
|
|
15908
16025
|
return;
|
|
15909
16026
|
}
|
|
16027
|
+
if (subpath === "invitations" && method === "GET") {
|
|
16028
|
+
const invitations = await ctx.client.listPendingInvitations(
|
|
16029
|
+
conn.cloud_url,
|
|
16030
|
+
conn.api_token,
|
|
16031
|
+
teamId
|
|
16032
|
+
);
|
|
16033
|
+
sendJson(res, { invitations });
|
|
16034
|
+
return;
|
|
16035
|
+
}
|
|
15910
16036
|
if (subpath === "invitations" && method === "POST") {
|
|
15911
16037
|
const body = await readJson(req);
|
|
15912
16038
|
const inviteeEmail = requireString2(body, "invitee_email");
|
|
@@ -16268,7 +16394,8 @@ var NATIVE_ENTITY_DEFS = {
|
|
|
16268
16394
|
notes: {
|
|
16269
16395
|
// A generic knowledge object: a free-form note with a title and body.
|
|
16270
16396
|
// Ordinary, user-editable rows; `source_file_id` optionally points back at
|
|
16271
|
-
// an originating `files` row.
|
|
16397
|
+
// an originating `files` row. Retained as native (1.16.3) because the
|
|
16398
|
+
// reference/source-organizer store uses it as the fallback organizer target.
|
|
16272
16399
|
columns: {
|
|
16273
16400
|
id: "TEXT PRIMARY KEY",
|
|
16274
16401
|
title: "TEXT",
|
|
@@ -16464,7 +16591,7 @@ async function getCreatorEmail(db) {
|
|
|
16464
16591
|
}
|
|
16465
16592
|
function computeState(type, teamEnabled, label, creatorEmail) {
|
|
16466
16593
|
if (type === "sqlite") return "local";
|
|
16467
|
-
if (!teamEnabled) return "cloud-
|
|
16594
|
+
if (!teamEnabled) return "team-cloud-needs-invite";
|
|
16468
16595
|
if (!label) {
|
|
16469
16596
|
return "team-cloud-needs-invite";
|
|
16470
16597
|
}
|
|
@@ -16744,6 +16871,19 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
16744
16871
|
const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
|
|
16745
16872
|
const backupPath = archiveLocalSqlite(sourceDbPath);
|
|
16746
16873
|
saveDbCredential(parsed.label, url);
|
|
16874
|
+
try {
|
|
16875
|
+
const identity = readIdentity();
|
|
16876
|
+
if (identity.email) {
|
|
16877
|
+
await new TeamsClient(ctx.db).ensureCloudWorkspaceIdentity({
|
|
16878
|
+
label: parsed.label,
|
|
16879
|
+
cloudUrl: url,
|
|
16880
|
+
workspaceName: parsed.label,
|
|
16881
|
+
email: identity.email,
|
|
16882
|
+
displayName: identity.display_name
|
|
16883
|
+
});
|
|
16884
|
+
}
|
|
16885
|
+
} catch {
|
|
16886
|
+
}
|
|
16747
16887
|
rewriteDbLine(ctx.configPath, "${LATTICE_DB:" + parsed.label + "}");
|
|
16748
16888
|
await ctx.swap();
|
|
16749
16889
|
sendJson(res, {
|
|
@@ -16789,6 +16929,18 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
16789
16929
|
...identity.email ? { email: identity.email } : {},
|
|
16790
16930
|
...identity.display_name ? { name: identity.display_name } : {}
|
|
16791
16931
|
});
|
|
16932
|
+
if (!result.probe.teamEnabled && identity.email) {
|
|
16933
|
+
try {
|
|
16934
|
+
await client.ensureCloudWorkspaceIdentity({
|
|
16935
|
+
label: parsed.label,
|
|
16936
|
+
cloudUrl: url,
|
|
16937
|
+
workspaceName: parsed.label,
|
|
16938
|
+
email: identity.email,
|
|
16939
|
+
displayName: identity.display_name
|
|
16940
|
+
});
|
|
16941
|
+
} catch {
|
|
16942
|
+
}
|
|
16943
|
+
}
|
|
16792
16944
|
rewriteDbLine(ctx.configPath, "${LATTICE_DB:" + parsed.label + "}");
|
|
16793
16945
|
await ctx.swap();
|
|
16794
16946
|
sendJson(res, {
|
|
@@ -16853,63 +17005,6 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
16853
17005
|
});
|
|
16854
17006
|
return true;
|
|
16855
17007
|
}
|
|
16856
|
-
if (pathname === "/api/dbconfig/upgrade-to-team" && method === "POST") {
|
|
16857
|
-
await tryHandler(res, async () => {
|
|
16858
|
-
const body = await readJson(req);
|
|
16859
|
-
const teamName = typeof body.team_name === "string" && body.team_name.trim() ? body.team_name.trim() : "";
|
|
16860
|
-
if (!teamName) {
|
|
16861
|
-
sendJson(res, { error: "team_name is required" }, 400);
|
|
16862
|
-
return;
|
|
16863
|
-
}
|
|
16864
|
-
const info = await describeCurrent(ctx.configPath, ctx.db);
|
|
16865
|
-
if (info.type !== "postgres" || !info.label) {
|
|
16866
|
-
sendJson(
|
|
16867
|
-
res,
|
|
16868
|
-
{
|
|
16869
|
-
error: "upgrade-to-team requires the active project to be on a labeled cloud DB. Migrate to cloud first."
|
|
16870
|
-
},
|
|
16871
|
-
400
|
|
16872
|
-
);
|
|
16873
|
-
return;
|
|
16874
|
-
}
|
|
16875
|
-
if (info.teamEnabled) {
|
|
16876
|
-
sendJson(res, { error: "Cloud DB is already a team DB" }, 409);
|
|
16877
|
-
return;
|
|
16878
|
-
}
|
|
16879
|
-
const cloudUrl = getDbCredential(info.label);
|
|
16880
|
-
if (!cloudUrl) {
|
|
16881
|
-
sendJson(res, { error: "No saved credential for " + info.label }, 500);
|
|
16882
|
-
return;
|
|
16883
|
-
}
|
|
16884
|
-
const identity = readIdentity();
|
|
16885
|
-
if (!identity.email || !identity.display_name) {
|
|
16886
|
-
sendJson(
|
|
16887
|
-
res,
|
|
16888
|
-
{
|
|
16889
|
-
error: "Set your display name + email in User Config \u2192 Identity before creating a team"
|
|
16890
|
-
},
|
|
16891
|
-
400
|
|
16892
|
-
);
|
|
16893
|
-
return;
|
|
16894
|
-
}
|
|
16895
|
-
const client = new TeamsClient(ctx.db);
|
|
16896
|
-
try {
|
|
16897
|
-
const reg = await client.upgradeToTeamCloud({
|
|
16898
|
-
label: info.label,
|
|
16899
|
-
cloudUrl,
|
|
16900
|
-
teamName,
|
|
16901
|
-
email: identity.email,
|
|
16902
|
-
displayName: identity.display_name
|
|
16903
|
-
});
|
|
16904
|
-
await ctx.swap();
|
|
16905
|
-
sendJson(res, { ok: true, team: reg.team, user: reg.user });
|
|
16906
|
-
} catch (e) {
|
|
16907
|
-
const status = e.status ?? 500;
|
|
16908
|
-
sendJson(res, { ok: false, error: e.message }, status);
|
|
16909
|
-
}
|
|
16910
|
-
});
|
|
16911
|
-
return true;
|
|
16912
|
-
}
|
|
16913
17008
|
return false;
|
|
16914
17009
|
}
|
|
16915
17010
|
|
|
@@ -17370,6 +17465,30 @@ async function openConfig(configPath, outputDir, autoRender = false) {
|
|
|
17370
17465
|
} catch {
|
|
17371
17466
|
teamEnabled = false;
|
|
17372
17467
|
}
|
|
17468
|
+
if (!teamEnabled) {
|
|
17469
|
+
try {
|
|
17470
|
+
const rawDb = parseDocument2(readFileSync13(configPath, "utf8")).get("db");
|
|
17471
|
+
const dbLine = typeof rawDb === "string" ? rawDb.trim() : "";
|
|
17472
|
+
const labelMatch = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(dbLine);
|
|
17473
|
+
const label = labelMatch?.[1];
|
|
17474
|
+
const identity = readIdentity();
|
|
17475
|
+
if (label && identity.email) {
|
|
17476
|
+
await teamsClient.ensureCloudWorkspaceIdentity({
|
|
17477
|
+
label,
|
|
17478
|
+
cloudUrl: parsed.dbPath,
|
|
17479
|
+
workspaceName: label,
|
|
17480
|
+
email: identity.email,
|
|
17481
|
+
displayName: identity.display_name
|
|
17482
|
+
});
|
|
17483
|
+
teamEnabled = await db.get("__lattice_team_identity", "singleton") != null;
|
|
17484
|
+
}
|
|
17485
|
+
} catch (e) {
|
|
17486
|
+
console.warn(
|
|
17487
|
+
"[openConfig] could not auto-initialize cloud workspace:",
|
|
17488
|
+
e.message
|
|
17489
|
+
);
|
|
17490
|
+
}
|
|
17491
|
+
}
|
|
17373
17492
|
if (teamEnabled) {
|
|
17374
17493
|
await registerTeamCloudTables(db);
|
|
17375
17494
|
try {
|
|
@@ -17483,19 +17602,12 @@ function saveConfigDoc(configPath, doc) {
|
|
|
17483
17602
|
function createBlankConfig(activeConfigPath, dbName) {
|
|
17484
17603
|
const dir = dirname8(activeConfigPath);
|
|
17485
17604
|
const slug = dbName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
17486
|
-
if (!slug) throw new Error("
|
|
17605
|
+
if (!slug) throw new Error("Workspace name must contain at least one alphanumeric character");
|
|
17487
17606
|
const configPath = join16(dir, `${slug}.config.yml`);
|
|
17488
17607
|
if (existsSync18(configPath)) throw new Error(`Config already exists: ${slug}.config.yml`);
|
|
17489
17608
|
const yaml = `db: ./data/${slug}.db
|
|
17490
17609
|
|
|
17491
|
-
entities:
|
|
17492
|
-
items:
|
|
17493
|
-
fields:
|
|
17494
|
-
id: { type: uuid, primaryKey: true }
|
|
17495
|
-
name: { type: text, required: true }
|
|
17496
|
-
notes: { type: text }
|
|
17497
|
-
deleted_at: { type: text }
|
|
17498
|
-
outputFile: ITEMS.md
|
|
17610
|
+
entities: {}
|
|
17499
17611
|
`;
|
|
17500
17612
|
writeFileSync8(configPath, yaml, "utf8");
|
|
17501
17613
|
mkdirSync8(join16(dir, "data"), { recursive: true });
|