0.10.0: Archify Sync, optional live collaboration on a model
Several people edit one model at once through a server you run yourself (archify-vscode-ext-sync); the files stay ordinary files on each disk. Sign-in through the browser, offline-first with later merge, teammates' cursors in the Source view and text editors, and a Shared section on the model page with people, versions, Compare and Restore. Without a server nothing changes: the sync code is a separate bundle that loads only when a model is shared, after a sign-in, or when a sync command or invite link is used.
This commit is contained in:
1 parent
a9021312b0
commit
c99f3e5fe8
27 files changed
+2899
-14
No files matched your search
@@ -618,6 +618,11 @@ function bridgeScript(options: PageOptions): string {
|
||||
if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(head, anchor); else document.body.appendChild(head);
|
||||
|
||||
var body = el('section', 'archify-model archify-model-body');
|
||||
// Live: filled by the host when the model is shared (Archify Sync); hidden otherwise.
|
||||
var live = el('div', 'archify-live');
|
||||
live.id = 'archify-live';
|
||||
live.hidden = true;
|
||||
body.appendChild(live);
|
||||
var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0);
|
||||
var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, true);
|
||||
body.appendChild(diagramsPanel);
|
||||
@@ -635,6 +640,7 @@ function bridgeScript(options: PageOptions): string {
|
||||
var button = el('button', 'archify-model-open');
|
||||
button.type = 'button';
|
||||
button.title = diagram.path;
|
||||
button.setAttribute('data-path', diagram.path);
|
||||
button.appendChild(el('span', 'archify-model-item-title', diagram.title || diagram.path.split('/').pop()));
|
||||
button.appendChild(el('span', 'archify-model-item-meta', diagram.parseError ? 'does not parse: ' + diagram.parseError : (diagram.kind || 'diagram') + ' · ' + diagram.path));
|
||||
button.addEventListener('click', function () { post({ type: 'openDiagram', path: diagram.path }); });
|
||||
@@ -677,6 +683,99 @@ function bridgeScript(options: PageOptions): string {
|
||||
decisionsPanel.appendChild(decisionList);
|
||||
}
|
||||
if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(body, anchor.nextSibling); else document.body.appendChild(body);
|
||||
post({ type: 'liveReady' });
|
||||
}
|
||||
|
||||
var LIVE_STATE = {
|
||||
online: 'Live',
|
||||
connecting: 'Connecting\u2026',
|
||||
offline: 'Offline \u00b7 edits are kept here and sent when the server is back',
|
||||
'signed-out': 'Signed out \u00b7 edits are kept here until you sign in',
|
||||
forbidden: 'No access \u00b7 you are no longer a member of this model',
|
||||
outdated: 'The server needs a newer extension',
|
||||
};
|
||||
var KIND_LABEL = { snapshot: 'snapshot', publish: 'published', restore: 'restored' };
|
||||
|
||||
function liveButton(label, title, action) {
|
||||
var button = el('button', 'archify-model-used-link archify-live-action', label);
|
||||
button.type = 'button';
|
||||
button.title = title;
|
||||
button.addEventListener('click', function (event) { event.preventDefault(); event.stopPropagation(); post({ type: 'liveAction', action: action }); });
|
||||
return button;
|
||||
}
|
||||
|
||||
function when(iso) {
|
||||
var date = new Date(iso);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
var seconds = (Date.now() - date.getTime()) / 1000;
|
||||
if (seconds < 60) return 'just now';
|
||||
if (seconds < 3600) return Math.floor(seconds / 60) + ' min ago';
|
||||
if (seconds < 86400) return Math.floor(seconds / 3600) + ' h ago';
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// The Live section of a shared model: who is here, the state, and versions.
|
||||
function renderLive(info) {
|
||||
var holder = document.getElementById('archify-live');
|
||||
if (!holder) return;
|
||||
holder.textContent = '';
|
||||
document.querySelectorAll('.archify-live-here').forEach(function (n) { n.remove(); });
|
||||
if (!info) { holder.hidden = true; return; }
|
||||
holder.hidden = false;
|
||||
var others = info.people.filter(function (p) { return !p.me; });
|
||||
var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, true);
|
||||
var status = el('div', 'archify-live-status');
|
||||
status.appendChild(el('span', 'archify-live-dot'));
|
||||
status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : '')));
|
||||
if (info.state === 'signed-out') status.appendChild(liveButton('Sign in', 'Sign in to ' + info.host, { kind: 'signIn' }));
|
||||
var actions = el('span', 'archify-live-actions');
|
||||
actions.appendChild(liveButton('Publish version', 'Name the current state as a version', { kind: 'publish' }));
|
||||
actions.appendChild(liveButton('Copy invite link', 'Copy a link that lets someone join this model', { kind: 'invite' }));
|
||||
status.appendChild(actions);
|
||||
livePanel.appendChild(status);
|
||||
|
||||
var people = el('ul', 'archify-live-people');
|
||||
info.people.forEach(function (person) {
|
||||
var item = el('li', 'archify-live-person' + (person.me ? ' is-me' : ''));
|
||||
item.appendChild(el('strong', '', person.me ? person.name + ' (you)' : person.name));
|
||||
if (person.file) item.appendChild(el('small', '', person.file));
|
||||
people.appendChild(item);
|
||||
});
|
||||
livePanel.appendChild(people);
|
||||
|
||||
// Who is in which diagram, on the diagram rows.
|
||||
others.forEach(function (person) {
|
||||
if (!person.file) return;
|
||||
document.querySelectorAll('.archify-model-open[data-path]').forEach(function (button) {
|
||||
if (button.getAttribute('data-path') !== person.file) return;
|
||||
var meta = button.querySelector('.archify-live-here');
|
||||
if (!meta) { meta = el('span', 'archify-live-here', ''); button.appendChild(meta); }
|
||||
meta.textContent = (meta.textContent ? meta.textContent.replace(/ here$/, '') + ', ' : '') + person.name + ' here';
|
||||
});
|
||||
});
|
||||
|
||||
var versions = info.versions || [];
|
||||
var head = el('div', 'archify-live-versions-head');
|
||||
head.appendChild(el('span', 'archify-model-summary-title', 'Versions'));
|
||||
if (versions.length > 8) head.appendChild(liveButton('All ' + versions.length + '\u2026', 'Pick from every version', { kind: 'versions' }));
|
||||
livePanel.appendChild(head);
|
||||
if (info.versionsProblem) livePanel.appendChild(el('p', 'archify-model-empty archify-live-note', info.versionsProblem));
|
||||
else if (!versions.length) livePanel.appendChild(el('p', 'archify-model-empty archify-live-note', 'No versions yet. One is taken on its own after a pause in editing.'));
|
||||
var list = el('ul', 'archify-live-versions');
|
||||
versions.slice(0, 8).forEach(function (v) {
|
||||
var item = el('li', 'archify-live-version is-' + v.kind);
|
||||
item.appendChild(el('strong', 'archify-live-version-label', v.label));
|
||||
item.appendChild(el('span', 'archify-decision-status archify-live-kind', KIND_LABEL[v.kind] + (v.restoredFrom ? ' ' + v.restoredFrom : '')));
|
||||
if (v.name) item.appendChild(el('span', 'archify-live-version-name', v.name));
|
||||
item.appendChild(el('small', 'archify-live-version-when', when(v.at)));
|
||||
var buttons = el('span', 'archify-live-actions');
|
||||
buttons.appendChild(liveButton('Compare', 'Compare ' + v.label + ' with the files here', { kind: 'compare', version: v.id }));
|
||||
buttons.appendChild(liveButton('Restore', 'Bring back ' + v.label + ' for everyone, as a new version', { kind: 'restore', version: v.id }));
|
||||
item.appendChild(buttons);
|
||||
list.appendChild(item);
|
||||
});
|
||||
livePanel.appendChild(list);
|
||||
holder.appendChild(livePanel);
|
||||
}
|
||||
|
||||
// What the page shows: Diagram | Graph | Source (Model | Graph | Source on
|
||||
@@ -775,6 +874,7 @@ function bridgeScript(options: PageOptions): string {
|
||||
if (message.type === 'notice') showNotice(message.notice);
|
||||
if (message.type === 'sourceDiagnostics') showProblemCount(message.count);
|
||||
if (message.type === 'setView') setSource(message.view === 'source');
|
||||
if (message.type === 'live') renderLive(message.live);
|
||||
});
|
||||
function onReady(fn) {
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
@@ -854,6 +954,26 @@ const MODEL_STYLE = `
|
||||
.archify-modal-defined { margin: 0.75rem 0 0; font-size: 0.8125rem; color: var(--m-muted); display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
|
||||
.archify-modal .archify-model-title { margin: 1.25rem 0 0.6rem; }
|
||||
.archify-modal .archify-model-list { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
|
||||
/* Live: a shared model's state, people and versions. */
|
||||
.archify-live-status { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0 0.9rem; font-size: 0.8125rem; }
|
||||
.archify-live-dot { width: 0.55rem; height: 0.55rem; border-radius: 50%; background: var(--m-muted); flex: none; }
|
||||
.archify-model-live.is-online .archify-live-dot { background: var(--vscode-testing-iconPassed, #3fb950); }
|
||||
.archify-model-live.is-signed-out .archify-live-dot, .archify-model-live.is-forbidden .archify-live-dot, .archify-model-live.is-outdated .archify-live-dot { background: var(--vscode-editorWarning-foreground, #d29922); }
|
||||
.archify-live-actions { display: inline-flex; flex-wrap: wrap; gap: 0.35rem; margin-left: auto; }
|
||||
.archify-live .archify-live-action { font-family: var(--vscode-font-family, system-ui, sans-serif); }
|
||||
.archify-live-people { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.6rem 0.9rem 0; padding: 0; }
|
||||
.archify-live-person { display: inline-flex; align-items: baseline; gap: 0.4rem; padding: 0.2rem 0.6rem; border: 1px solid var(--m-border); border-radius: 999px; font-size: 0.8125rem; }
|
||||
.archify-live-person small { color: var(--m-muted); font-family: ui-monospace, Menlo, monospace; font-size: 0.6875rem; }
|
||||
.archify-live-person.is-me { opacity: 0.75; }
|
||||
.archify-live-here { font-size: 0.6875rem; color: var(--m-accent); font-weight: 600; }
|
||||
.archify-live-versions-head { display: flex; align-items: center; gap: 0.5rem; margin: 0.9rem 0.9rem 0.35rem; font-size: 0.8125rem; font-weight: 600; }
|
||||
.archify-live-note { margin: 0 0.9rem; font-size: 0.8125rem; }
|
||||
.archify-live-versions { list-style: none; margin: 0 0.9rem; padding: 0; display: grid; gap: 0.3rem; }
|
||||
.archify-live-version { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding: 0.4rem 0.7rem; border: 1px solid var(--m-border); border-radius: 0.5rem; font-size: 0.8125rem; }
|
||||
.archify-live-version-label { font-family: ui-monospace, Menlo, monospace; }
|
||||
.archify-live-version.is-publish .archify-live-version-label { color: var(--m-accent); }
|
||||
.archify-live-version-name { font-weight: 600; }
|
||||
.archify-live-version-when { color: var(--m-muted); }
|
||||
/* Decisions: rows, chips and status badges. */
|
||||
.archify-decision-list { grid-template-columns: 1fr; gap: 0.35rem; }
|
||||
.archify-decision-open { padding: 0.55rem 0.85rem; }
|
||||
|
||||
Reference in new issue
Block a user