openvibers/openvibe-git Commits
Empty states: the shared kit, with the next action as a button (#42)
Changes
@@@ -4,6 +4,7 @@* The CI pages: the runs of one repository and one run's log. Plain and readable, no JavaScript needed. The* verdict is also a commit status ("forge/test") on the change page and in the API.*/++const kit = require('openvibe-shared/kit');const { esc } = require('../render/html');const { repoTabs } = require('./index');@@@ -39,7 +40,7 @@ function list({ row, enabled, runs, context }) {return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${b}">${esc(row.owner_username)}/${esc(row.name)}</a></p><h1>CI</h1><p>${enabled ? `The forge runner tests branch pushes by OpenVibers members and reports <code>${esc(context)}</code>.` : 'Forge CI is not enabled for this repository; GitHub Actions still runs.'}</p></header>${repoTabs(row, 'ci')}--${runs.length ? `<ul class="git-ci-list">${rows}</ul>` : '<p class="git-empty">No forge runs yet.</p>'}++${runs.length ? `<ul class="git-ci-list">${rows}</ul>` : kit.empty({ icon: 'gear', title: 'No forge runs yet', text: 'A run appears here when someone pushes a branch.' })}<p class="git-muted git-small">The runner clones the branch, installs dependencies through an allowlisted proxy, then runs the tests with no network at all. Logs stay on the run they belong to.</p></div>`;}@@@ -52,17 +52,17 @@ function home({ viewer, stats, trending = [], activity = [], official = [], base<section class="git-section" aria-labelledby="git-trending-h"><div class="git-section-head"><h2 id="git-trending-h">Trending this week</h2><a href="/explore?sort=trending">All repositories</a></div>-- ${trending.length ? `<ul class="git-cards">${trending.map(card).join('')}</ul>` : '<p class="git-empty">No repositories yet. <a href="/new">Make the first one</a>.</p>'}++ ${trending.length ? `<ul class="git-cards">${trending.map(card).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No repositories yet', text: signedIn ? 'Make the first one: an empty repository, a starter app or a mirror of a public GitHub URL.' : 'Sign in and make the first one: an empty repository, a starter app or a mirror of a public GitHub URL.', action: { href: signedIn ? '/new' : '/auth/login?next=%2Fnew', label: 'New repository', icon: 'plus' } })}</section><div class="git-split"><section class="git-section" aria-labelledby="git-activity-h"><div class="git-section-head"><h2 id="git-activity-h">Recent activity</h2></div>-- ${activity.length ? `<ul class="git-activity">${activity.map(activityRow).join('')}</ul>` : '<p class="git-empty">Nothing has happened yet.</p>'}++ ${activity.length ? `<ul class="git-activity">${activity.map(activityRow).join('')}</ul>` : kit.empty({ icon: 'clock', title: 'Nothing has happened yet', text: 'Pushes, changes and issues appear here as people work.', action: { href: '/explore', label: 'Explore repositories', icon: 'search' } })}</section><section class="git-section" aria-labelledby="git-official-h"><div class="git-section-head"><h2 id="git-official-h">Official OpenVibe repositories</h2></div>-- ${official.length ? `<ul class="git-official">${official.map((r) => `<li><a class="git-official-main" href="/${esc(r.full_name)}"><span class="git-avatar git-avatar--org">${icon('code')}</span><span><b>${esc(r.name)}</b><small>${esc(r.description || 'The OpenVibe projects.')}</small></span></a>${r.github_full_name ? `<a class="git-official-gh" href="https://github.com/${esc(r.github_full_name)}" rel="noopener">${icon('open')} GitHub</a>` : ''}</li>`).join('')}</ul>` : '<p class="git-empty">The official repositories are on their way.</p>'}++ ${official.length ? `<ul class="git-official">${official.map((r) => `<li><a class="git-official-main" href="/${esc(r.full_name)}"><span class="git-avatar git-avatar--org">${icon('code')}</span><span><b>${esc(r.name)}</b><small>${esc(r.description || 'The OpenVibe projects.')}</small></span></a>${r.github_full_name ? `<a class="git-official-gh" href="https://github.com/${esc(r.github_full_name)}" rel="noopener">${icon('open')} GitHub</a>` : ''}</li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'The official repositories are on their way', text: 'The OpenVibe projects appear here once their mirrors are up.' })}</section></div>@@@ -111,7 +111,7 @@ function explore({ viewer, repositories = [], query = '', filters = {}, next = n${chip('mine', 'Mine', keep({ mine: 1 }), filters.mine)}${chip('starred', 'Starred', keep({ starred: 1 }), filters.starred)}</nav>--${repositories.length ? `<ul class="git-cards git-cards--wide">${repositories.map(card).join('')}</ul>` : `<p class="git-empty">Nothing matches. ${query ? `No repository matches “${esc(query)}”.` : 'Try another filter.'} <a href="/new">Make one</a>.</p>`}++${repositories.length ? `<ul class="git-cards git-cards--wide">${repositories.map(card).join('')}</ul>` : kit.empty({ icon: 'search', title: 'Nothing matches', text: query ? `No repository matches “${query}”. Try another filter, or make one.` : 'Try another filter, or make one.', action: { href: '/new', label: 'Make a repository', icon: 'plus' } })}${next ? `<a class="ovk-btn ovk-btn--ghost git-more" href="/explore${qs({ ...base, ...(filters.official ? { official: 1 } : {}), ...(filters.mirror ? { mirror: 1 } : {}), ...(filters.mine ? { mine: 1 } : {}), ...(filters.starred ? { starred: 1 } : {}), cursor: next })}">More</a>` : ''}</div>`;}@@@ -348,7 +348,7 @@ ${commits.length ? `<ul class="git-commit-list">${commits.map((c) => `<li><a hre<span class="git-avatar git-avatar--commit">${icon('code')}</span><span class="git-commit-main"><b>${esc(c.subject || c.message || '(no message)')}</b><small>${esc(c.author.name)} · ${esc(ago(c.date))}</small></span><span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span>--</a></li>`).join('')}</ul>` : '<p class="git-empty">No commits here yet.</p>'}++</a></li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No commits here yet', text: 'The first commit starts the history.' })}${next ? `<a class="ovk-btn ovk-btn--ghost git-more" href="${base}/commits${qs({ ref, path: filePath, cursor: next })}">More commits</a>` : ''}</div>`;}@@@ -396,20 +396,20 @@ ${diff && commit.diff ? `<div class="git-diff" data-git-diff><div class="git-diffunction branchesPage({ row, branches }) {const base = `/${row.owner_username}/${row.name}`;return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Branches</h1></header>${repoTabs(row, 'branches')}--${branches.length ? `<ul class="git-refs">${branches.map((b) => `<li><a href="${base}/tree${qs({ ref: b.name })}"><span>${icon('code')} <b>${esc(b.name)}</b>${b.name === row.default_branch ? ' <span class="git-chip">default</span>' : ''}</span><small>${esc(b.subject)} · ${esc(ago(b.date))}</small><code>${esc(b.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : '<p class="git-empty">No branches yet.</p>'}</div>`;++${branches.length ? `<ul class="git-refs">${branches.map((b) => `<li><a href="${base}/tree${qs({ ref: b.name })}"><span>${icon('code')} <b>${esc(b.name)}</b>${b.name === row.default_branch ? ' <span class="git-chip">default</span>' : ''}</span><small>${esc(b.subject)} · ${esc(ago(b.date))}</small><code>${esc(b.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No branches yet', text: 'Push a branch and it appears here.' })}</div>`;}function tagsPage({ row, tags }) {const base = `/${row.owner_username}/${row.name}`;return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Tags</h1></header>${repoTabs(row, 'tags')}--${tags.length ? `<ul class="git-refs">${tags.map((t) => `<li><a href="${base}/tree${qs({ ref: t.name })}"><span>${icon('tag')} <b>${esc(t.name)}</b></span><small>${esc(t.subject)} · ${esc(ago(t.date))}</small><code>${esc(t.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : '<p class="git-empty">No tags yet.</p>'}</div>`;++${tags.length ? `<ul class="git-refs">${tags.map((t) => `<li><a href="${base}/tree${qs({ ref: t.name })}"><span>${icon('tag')} <b>${esc(t.name)}</b></span><small>${esc(t.subject)} · ${esc(ago(t.date))}</small><code>${esc(t.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : kit.empty({ icon: 'tag', title: 'No tags yet', text: 'Tag a commit to mark a release.' })}</div>`;}function releasesPage({ row, releases, viewer }) {const base = `/${row.owner_username}/${row.name}`;const canPublish = viewer && viewer.kind === 'user';return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Releases</h1></header>${repoTabs(row, 'releases')}--${releases.length ? `<ul class="git-releases">${releases.map((r) => `<li><div class="git-release-head"><h2>${esc(r.name || r.tag)}</h2><span class="git-chip">${esc(r.tag)}</span><small>${esc(date(r.published_at))}</small></div>${r.body ? `<div class="git-md">${renderMarkdown(r.body)}</div>` : '<p class="git-empty">No notes yet.</p>'}${r.sha ? `<p class="git-release-sha"><a href="${base}/commit/${esc(r.sha)}"><code>${esc(r.sha.slice(0, 10))}</code></a></p>` : ''}</li>`).join('')}</ul>` : '<p class="git-empty">No releases yet. A tag becomes a release when its notes are written.</p>'}++${releases.length ? `<ul class="git-releases">${releases.map((r) => `<li><div class="git-release-head"><h2>${esc(r.name || r.tag)}</h2><span class="git-chip">${esc(r.tag)}</span><small>${esc(date(r.published_at))}</small></div>${r.body ? `<div class="git-md">${renderMarkdown(r.body)}</div>` : kit.empty({ icon: 'tag', title: 'No notes yet', text: 'Write what changed in this release.' })}${r.sha ? `<p class="git-release-sha"><a href="${base}/commit/${esc(r.sha)}"><code>${esc(r.sha.slice(0, 10))}</code></a></p>` : ''}</li>`).join('')}</ul>` : kit.empty({ icon: 'tag', title: 'No releases yet', text: 'A tag becomes a release when its notes are written.' })}${canPublish ? `<details class="git-fold"><summary class="ovk-btn ovk-btn--ghost">${icon('plus')} Write release notes</summary><form method="post" action="${base}/releases" class="git-form"><label>Tag <input name="tag" required placeholder="v1.0.0"></label><label>Name <input name="name" placeholder="First release"></label><label>Notes <textarea name="body" rows="5" placeholder="What changed"></textarea></label><button class="ovk-btn ovk-btn--primary" type="submit">Publish</button></form></details>` : ''}</div>`;}@@@ -417,7 +417,7 @@ ${canPublish ? `<details class="git-fold"><summary class="ovk-btn ovk-btn--ghostfunction contributorsPage({ row, contributors }) {const base = `/${row.owner_username}/${row.name}`;return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Contributors</h1></header>${repoTabs(row, 'contributors')}--${contributors.length ? `<ul class="git-people">${contributors.map((c) => `<li><span class="git-avatar">${esc(initials(c.name))}</span><span><b>${esc(c.name)}</b>${c.username ? ` <a href="/@${esc(c.username)}">@${esc(c.username)}</a>` : ''}<small>${esc(c.email)}</small></span><em>${plural(c.commits, 'commit')}</em></li>`).join('')}</ul>` : '<p class="git-empty">No contributors yet.</p>'}</div>`;++${contributors.length ? `<ul class="git-people">${contributors.map((c) => `<li><span class="git-avatar">${esc(initials(c.name))}</span><span><b>${esc(c.name)}</b>${c.username ? ` <a href="/@${esc(c.username)}">@${esc(c.username)}</a>` : ''}<small>${esc(c.email)}</small></span><em>${plural(c.commits, 'commit')}</em></li>`).join('')}</ul>` : kit.empty({ icon: 'members', title: 'No contributors yet', text: 'People who commit to this repository appear here.' })}</div>`;}function insightsPage({ row, data }) {@@@ -442,7 +442,7 @@ ${repoTabs(row, 'insights')}// ── Profile, new, messages ───────────────────────────────────function profilePage({ profile }) {return `<div class="git-profile"><header class="git-profile-head"><span class="git-avatar git-avatar--big">${esc(initials(profile.username))}</span><div><h1>@${esc(profile.username)}</h1><p>${plural(profile.repositories.length, 'public repository', 'public repositories')} on OpenVibe Git.</p></div></header>--${profile.repositories.length ? `<ul class="git-cards git-cards--wide">${profile.repositories.map(card).join('')}</ul>` : '<p class="git-empty">No public repositories yet.</p>'}</div>`;++${profile.repositories.length ? `<ul class="git-cards git-cards--wide">${profile.repositories.map(card).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No public repositories yet', text: 'When this person makes one public it appears here.' })}</div>`;}function newPage({ viewer, values = {}, error = null }) {@@@ -64,7 +64,7 @@ function machinesPage({ viewer, machines = [], error = null }) {return `<div class="git-list-page"><header class="git-page-head"><h1>Your computers</h1><p>Repositories on your own computers, published through Link. Read-only: Git can look, never write. Browsing works while the computer is online.</p></header>${error ? `<p class="git-error" role="alert">${esc(error)}</p>` : ''}--${machines.length ? `<ul class="git-refs git-machines">${rows}</ul>` : '<p class="git-empty">No computer is connected yet.</p>'}++${machines.length ? `<ul class="git-refs git-machines">${rows}</ul>` : kit.empty({ icon: 'computer', title: 'No computer is connected yet', text: 'Link publishes the repositories under a folder on your computer. Connect one below to browse them here.', action: { href: '/machines', label: 'Connect a computer', icon: 'computer' } })}<details class="git-fold"><summary class="ovk-btn ovk-btn--primary">${icon('plus')} Connect a computer</summary><form method="post" action="/machines" class="git-form"><label>Name <input name="name" maxlength="80" placeholder="my laptop"></label>@@@ -94,7 +94,7 @@ function machinePage({ machine, repos = [] }) {return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="/machines">Your computers</a></p><h1>${esc(machine.name)}</h1><p>${machine.online ? 'Online now, read-only through Link.' : `Offline since ${esc(ago(machine.offline_since || machine.last_seen_at))}. Start Link to browse.`}</p></header>--${repos.length ? `<ul class="git-refs git-machines">${rows}</ul>` : '<p class="git-empty">No repositories under the root yet.</p>'}++${repos.length ? `<ul class="git-refs git-machines">${rows}</ul>` : kit.empty({ icon: 'code', title: 'No repositories under the root yet', text: 'Choose a folder on the computer when Link asks where to publish from.' })}</div>`;}@@@ -144,7 +144,7 @@ function machineCommitsPage({ machine, repo, ref, path = '', commits = [], offli${commits.length ? `<ul class="git-commit-list">${commits.map((c) => `<li><a href="${base}/commits${qs({ ref })}"><span class="git-avatar git-avatar--commit">${icon('commit')}</span><span class="git-commit-main"><b>${esc(c.subject || '(no message)')}</b><small>${esc(c.author)} · ${esc(ago(c.date))}</small></span>--<span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span></a></li>`).join('')}</ul>` : '<p class="git-empty">No commits here yet.</p>'}++<span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span></a></li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No commits here yet', text: 'The first commit starts the history.' })}</div>`;}@@@ -5,6 +5,7 @@* line on the left, and one merge box on the right (one column on a phone, the box first). Buttons are sized to their* words; only Merge is filled; Close is quiet. Every form keeps its fields and routes (server/http/review.js).*/++const kit = require('openvibe-shared/kit');const { esc } = require('../render/html');const { renderMarkdown } = require('../render/markdown');const glyphs = require('openvibe-shared/glyphs');@@@ -60,7 +61,7 @@ function list(r, kind, items) {const path = kind === 'change' ? 'changes' : 'issues';const rows = items.map((c) => `<li><a class="git-row" href="${base(r)}/${path}/${c.number}">${state(c.state)}<span class="git-row-title">${esc(c.title)}</span><span class="git-row-num">#${c.number}</span>${(c.labels || []).map((l) => `<small class="git-label">${esc(l)}</small>`).join('')}</a></li>`).join('');return wrap(r, kind === 'change' ? 'Changes' : 'Issues', `<div class="git-list-bar"><a class="ovk-btn ovk-btn--primary" href="${base(r)}/${path}/new">${ic('plus')}<span>New ${kind}</span></a></div>--${rows ? `<ul class="git-review-list">${rows}</ul>` : `<div class="git-empty">${ic(kind === 'change' ? 'branch' : 'chat', 22)}<p><b>No ${kind === 'change' ? 'changes' : 'issues'} yet</b></p><p class="git-muted">${kind === 'change' ? 'Push a branch, then open a change to ask for it to be merged.' : 'Report a problem or suggest something for this repository.'}</p></div>`}`, path);++${rows ? `<ul class="git-review-list">${rows}</ul>` : kit.empty({ icon: kind === 'change' ? 'code' : 'chat', title: `No ${kind === 'change' ? 'changes' : 'issues'} yet`, text: kind === 'change' ? 'Push a branch, then open a change to ask for it to be merged.' : 'Report a problem or suggest something for this repository.', action: { href: `${base(r)}/${path}/new`, label: `Open ${kind === 'change' ? 'a change' : 'an issue'}`, icon: 'plus' } })}`, path);}function form(r, kind) {@@@ -66,6 +66,18 @@ const { boot } = require('./helpers/boot');assert.equal(doc.status, 200);assert.equal(doc.json().openapi, '3.1.0');++ // Empty states are the shared kit's, with the next action as a button.++ const views = require('../server/views');++ const emptyHome = views.home({ viewer: null, stats: { repositories: 0, pushes_this_week: 0, people: 0, clones_this_week: 0 }, trending: [], activity: [], official: [], baseUrl: 'https://git.openvibe.codes' });++ assert.match(emptyHome, /class="ovk-empty"/);++ assert.match(emptyHome, /No repositories yet/);++ assert.match(emptyHome, /ovk-btn[^>]*href="\/auth\/login\?next=%2Fnew"/);++ assert.match(emptyHome, /Nothing has happened yet/);++ assert.match(views.explore({ viewer: null, repositories: [], query: 'zzz', filters: {} }), /Nothing matches/);++ assert.match(views.branchesPage({ row: { owner_username: 'a', name: 'b', default_branch: 'main' }, branches: [] }), /No branches yet/);++ assert.match(views.releasesPage({ row: { owner_username: 'a', name: 'b' }, releases: [], viewer: null }), /No releases yet/);++ assert.match(views.profilePage({ profile: { username: 'nobody', repositories: [] } }), /No public repositories yet/);++// No placeholder content anywhere.assert.doesNotMatch(home.text, /lorem ipsum/i);assert.match(repo.full_name, /sam\/phone-first/);
@@ -4,6 +4,7 @@
@@ -4,6 +4,7 @@
* The CI pages: the runs of one repository and one run's log. Plain and readable, no JavaScript needed. The
* The CI pages: the runs of one repository and one run's log. Plain and readable, no JavaScript needed. The
* verdict is also a commit status ("forge/test") on the change page and in the API.
* verdict is also a commit status ("forge/test") on the change page and in the API.
*/
*/
+const kit = require('openvibe-shared/kit');
const { esc } = require('../render/html');
const { esc } = require('../render/html');
const { repoTabs } = require('./index');
const { repoTabs } = require('./index');
@@ -39,7 +40,7 @@ function list({ row, enabled, runs, context }) {
@@ -39,7 +40,7 @@ function list({ row, enabled, runs, context }) {
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${b}">${esc(row.owner_username)}/${esc(row.name)}</a></p><h1>CI</h1>
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${b}">${esc(row.owner_username)}/${esc(row.name)}</a></p><h1>CI</h1>
<p>${enabled ? `The forge runner tests branch pushes by OpenVibers members and reports <code>${esc(context)}</code>.` : 'Forge CI is not enabled for this repository; GitHub Actions still runs.'}</p></header>
<p>${enabled ? `The forge runner tests branch pushes by OpenVibers members and reports <code>${esc(context)}</code>.` : 'Forge CI is not enabled for this repository; GitHub Actions still runs.'}</p></header>
${repoTabs(row, 'ci')}
${repoTabs(row, 'ci')}
-${runs.length ? `<ul class="git-ci-list">${rows}</ul>` : '<p class="git-empty">No forge runs yet.</p>'}
+${runs.length ? `<ul class="git-ci-list">${rows}</ul>` : kit.empty({ icon: 'gear', title: 'No forge runs yet', text: 'A run appears here when someone pushes a branch.' })}
<p class="git-muted git-small">The runner clones the branch, installs dependencies through an allowlisted proxy, then runs the tests with no network at all. Logs stay on the run they belong to.</p></div>`;
<p class="git-muted git-small">The runner clones the branch, installs dependencies through an allowlisted proxy, then runs the tests with no network at all. Logs stay on the run they belong to.</p></div>`;
}
}
@@ -52,17 +52,17 @@ function home({ viewer, stats, trending = [], activity = [], official = [], base
@@ -52,17 +52,17 @@ function home({ viewer, stats, trending = [], activity = [], official = [], base
<section class="git-section" aria-labelledby="git-trending-h">
<section class="git-section" aria-labelledby="git-trending-h">
<div class="git-section-head"><h2 id="git-trending-h">Trending this week</h2><a href="/explore?sort=trending">All repositories</a></div>
<div class="git-section-head"><h2 id="git-trending-h">Trending this week</h2><a href="/explore?sort=trending">All repositories</a></div>
- ${trending.length ? `<ul class="git-cards">${trending.map(card).join('')}</ul>` : '<p class="git-empty">No repositories yet. <a href="/new">Make the first one</a>.</p>'}
+ ${trending.length ? `<ul class="git-cards">${trending.map(card).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No repositories yet', text: signedIn ? 'Make the first one: an empty repository, a starter app or a mirror of a public GitHub URL.' : 'Sign in and make the first one: an empty repository, a starter app or a mirror of a public GitHub URL.', action: { href: signedIn ? '/new' : '/auth/login?next=%2Fnew', label: 'New repository', icon: 'plus' } })}
</section>
</section>
<div class="git-split">
<div class="git-split">
<section class="git-section" aria-labelledby="git-activity-h">
<section class="git-section" aria-labelledby="git-activity-h">
<div class="git-section-head"><h2 id="git-activity-h">Recent activity</h2></div>
<div class="git-section-head"><h2 id="git-activity-h">Recent activity</h2></div>
- ${activity.length ? `<ul class="git-activity">${activity.map(activityRow).join('')}</ul>` : '<p class="git-empty">Nothing has happened yet.</p>'}
+ ${activity.length ? `<ul class="git-activity">${activity.map(activityRow).join('')}</ul>` : kit.empty({ icon: 'clock', title: 'Nothing has happened yet', text: 'Pushes, changes and issues appear here as people work.', action: { href: '/explore', label: 'Explore repositories', icon: 'search' } })}
</section>
</section>
<section class="git-section" aria-labelledby="git-official-h">
<section class="git-section" aria-labelledby="git-official-h">
<div class="git-section-head"><h2 id="git-official-h">Official OpenVibe repositories</h2></div>
<div class="git-section-head"><h2 id="git-official-h">Official OpenVibe repositories</h2></div>
- ${official.length ? `<ul class="git-official">${official.map((r) => `<li><a class="git-official-main" href="/${esc(r.full_name)}"><span class="git-avatar git-avatar--org">${icon('code')}</span><span><b>${esc(r.name)}</b><small>${esc(r.description || 'The OpenVibe projects.')}</small></span></a>${r.github_full_name ? `<a class="git-official-gh" href="https://github.com/${esc(r.github_full_name)}" rel="noopener">${icon('open')} GitHub</a>` : ''}</li>`).join('')}</ul>` : '<p class="git-empty">The official repositories are on their way.</p>'}
+ ${official.length ? `<ul class="git-official">${official.map((r) => `<li><a class="git-official-main" href="/${esc(r.full_name)}"><span class="git-avatar git-avatar--org">${icon('code')}</span><span><b>${esc(r.name)}</b><small>${esc(r.description || 'The OpenVibe projects.')}</small></span></a>${r.github_full_name ? `<a class="git-official-gh" href="https://github.com/${esc(r.github_full_name)}" rel="noopener">${icon('open')} GitHub</a>` : ''}</li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'The official repositories are on their way', text: 'The OpenVibe projects appear here once their mirrors are up.' })}
</section>
</section>
</div>
</div>
@@ -111,7 +111,7 @@ function explore({ viewer, repositories = [], query = '', filters = {}, next = n
@@ -111,7 +111,7 @@ function explore({ viewer, repositories = [], query = '', filters = {}, next = n
${chip('mine', 'Mine', keep({ mine: 1 }), filters.mine)}
${chip('mine', 'Mine', keep({ mine: 1 }), filters.mine)}
${chip('starred', 'Starred', keep({ starred: 1 }), filters.starred)}
${chip('starred', 'Starred', keep({ starred: 1 }), filters.starred)}
</nav>
</nav>
-${repositories.length ? `<ul class="git-cards git-cards--wide">${repositories.map(card).join('')}</ul>` : `<p class="git-empty">Nothing matches. ${query ? `No repository matches “${esc(query)}”.` : 'Try another filter.'} <a href="/new">Make one</a>.</p>`}
+${repositories.length ? `<ul class="git-cards git-cards--wide">${repositories.map(card).join('')}</ul>` : kit.empty({ icon: 'search', title: 'Nothing matches', text: query ? `No repository matches “${query}”. Try another filter, or make one.` : 'Try another filter, or make one.', action: { href: '/new', label: 'Make a repository', icon: 'plus' } })}
${next ? `<a class="ovk-btn ovk-btn--ghost git-more" href="/explore${qs({ ...base, ...(filters.official ? { official: 1 } : {}), ...(filters.mirror ? { mirror: 1 } : {}), ...(filters.mine ? { mine: 1 } : {}), ...(filters.starred ? { starred: 1 } : {}), cursor: next })}">More</a>` : ''}
${next ? `<a class="ovk-btn ovk-btn--ghost git-more" href="/explore${qs({ ...base, ...(filters.official ? { official: 1 } : {}), ...(filters.mirror ? { mirror: 1 } : {}), ...(filters.mine ? { mine: 1 } : {}), ...(filters.starred ? { starred: 1 } : {}), cursor: next })}">More</a>` : ''}
</div>`;
</div>`;
}
}
@@ -348,7 +348,7 @@ ${commits.length ? `<ul class="git-commit-list">${commits.map((c) => `<li><a hre
@@ -348,7 +348,7 @@ ${commits.length ? `<ul class="git-commit-list">${commits.map((c) => `<li><a hre
<span class="git-avatar git-avatar--commit">${icon('code')}</span>
<span class="git-avatar git-avatar--commit">${icon('code')}</span>
<span class="git-commit-main"><b>${esc(c.subject || c.message || '(no message)')}</b><small>${esc(c.author.name)} · ${esc(ago(c.date))}</small></span>
<span class="git-commit-main"><b>${esc(c.subject || c.message || '(no message)')}</b><small>${esc(c.author.name)} · ${esc(ago(c.date))}</small></span>
<span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span>
<span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span>
-</a></li>`).join('')}</ul>` : '<p class="git-empty">No commits here yet.</p>'}
+</a></li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No commits here yet', text: 'The first commit starts the history.' })}
${next ? `<a class="ovk-btn ovk-btn--ghost git-more" href="${base}/commits${qs({ ref, path: filePath, cursor: next })}">More commits</a>` : ''}
${next ? `<a class="ovk-btn ovk-btn--ghost git-more" href="${base}/commits${qs({ ref, path: filePath, cursor: next })}">More commits</a>` : ''}
</div>`;
</div>`;
}
}
@@ -396,20 +396,20 @@ ${diff && commit.diff ? `<div class="git-diff" data-git-diff><div class="git-dif
@@ -396,20 +396,20 @@ ${diff && commit.diff ? `<div class="git-diff" data-git-diff><div class="git-dif
function branchesPage({ row, branches }) {
function branchesPage({ row, branches }) {
const base = `/${row.owner_username}/${row.name}`;
const base = `/${row.owner_username}/${row.name}`;
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Branches</h1></header>${repoTabs(row, 'branches')}
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Branches</h1></header>${repoTabs(row, 'branches')}
-${branches.length ? `<ul class="git-refs">${branches.map((b) => `<li><a href="${base}/tree${qs({ ref: b.name })}"><span>${icon('code')} <b>${esc(b.name)}</b>${b.name === row.default_branch ? ' <span class="git-chip">default</span>' : ''}</span><small>${esc(b.subject)} · ${esc(ago(b.date))}</small><code>${esc(b.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : '<p class="git-empty">No branches yet.</p>'}</div>`;
+${branches.length ? `<ul class="git-refs">${branches.map((b) => `<li><a href="${base}/tree${qs({ ref: b.name })}"><span>${icon('code')} <b>${esc(b.name)}</b>${b.name === row.default_branch ? ' <span class="git-chip">default</span>' : ''}</span><small>${esc(b.subject)} · ${esc(ago(b.date))}</small><code>${esc(b.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No branches yet', text: 'Push a branch and it appears here.' })}</div>`;
}
}
function tagsPage({ row, tags }) {
function tagsPage({ row, tags }) {
const base = `/${row.owner_username}/${row.name}`;
const base = `/${row.owner_username}/${row.name}`;
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Tags</h1></header>${repoTabs(row, 'tags')}
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Tags</h1></header>${repoTabs(row, 'tags')}
-${tags.length ? `<ul class="git-refs">${tags.map((t) => `<li><a href="${base}/tree${qs({ ref: t.name })}"><span>${icon('tag')} <b>${esc(t.name)}</b></span><small>${esc(t.subject)} · ${esc(ago(t.date))}</small><code>${esc(t.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : '<p class="git-empty">No tags yet.</p>'}</div>`;
+${tags.length ? `<ul class="git-refs">${tags.map((t) => `<li><a href="${base}/tree${qs({ ref: t.name })}"><span>${icon('tag')} <b>${esc(t.name)}</b></span><small>${esc(t.subject)} · ${esc(ago(t.date))}</small><code>${esc(t.sha.slice(0, 7))}</code></a></li>`).join('')}</ul>` : kit.empty({ icon: 'tag', title: 'No tags yet', text: 'Tag a commit to mark a release.' })}</div>`;
}
}
function releasesPage({ row, releases, viewer }) {
function releasesPage({ row, releases, viewer }) {
const base = `/${row.owner_username}/${row.name}`;
const base = `/${row.owner_username}/${row.name}`;
const canPublish = viewer && viewer.kind === 'user';
const canPublish = viewer && viewer.kind === 'user';
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Releases</h1></header>${repoTabs(row, 'releases')}
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Releases</h1></header>${repoTabs(row, 'releases')}
-${releases.length ? `<ul class="git-releases">${releases.map((r) => `<li><div class="git-release-head"><h2>${esc(r.name || r.tag)}</h2><span class="git-chip">${esc(r.tag)}</span><small>${esc(date(r.published_at))}</small></div>${r.body ? `<div class="git-md">${renderMarkdown(r.body)}</div>` : '<p class="git-empty">No notes yet.</p>'}${r.sha ? `<p class="git-release-sha"><a href="${base}/commit/${esc(r.sha)}"><code>${esc(r.sha.slice(0, 10))}</code></a></p>` : ''}</li>`).join('')}</ul>` : '<p class="git-empty">No releases yet. A tag becomes a release when its notes are written.</p>'}
+${releases.length ? `<ul class="git-releases">${releases.map((r) => `<li><div class="git-release-head"><h2>${esc(r.name || r.tag)}</h2><span class="git-chip">${esc(r.tag)}</span><small>${esc(date(r.published_at))}</small></div>${r.body ? `<div class="git-md">${renderMarkdown(r.body)}</div>` : kit.empty({ icon: 'tag', title: 'No notes yet', text: 'Write what changed in this release.' })}${r.sha ? `<p class="git-release-sha"><a href="${base}/commit/${esc(r.sha)}"><code>${esc(r.sha.slice(0, 10))}</code></a></p>` : ''}</li>`).join('')}</ul>` : kit.empty({ icon: 'tag', title: 'No releases yet', text: 'A tag becomes a release when its notes are written.' })}
${canPublish ? `<details class="git-fold"><summary class="ovk-btn ovk-btn--ghost">${icon('plus')} Write release notes</summary><form method="post" action="${base}/releases" class="git-form"><label>Tag <input name="tag" required placeholder="v1.0.0"></label><label>Name <input name="name" placeholder="First release"></label><label>Notes <textarea name="body" rows="5" placeholder="What changed"></textarea></label><button class="ovk-btn ovk-btn--primary" type="submit">Publish</button></form></details>` : ''}
${canPublish ? `<details class="git-fold"><summary class="ovk-btn ovk-btn--ghost">${icon('plus')} Write release notes</summary><form method="post" action="${base}/releases" class="git-form"><label>Tag <input name="tag" required placeholder="v1.0.0"></label><label>Name <input name="name" placeholder="First release"></label><label>Notes <textarea name="body" rows="5" placeholder="What changed"></textarea></label><button class="ovk-btn ovk-btn--primary" type="submit">Publish</button></form></details>` : ''}
</div>`;
</div>`;
}
}
@@ -417,7 +417,7 @@ ${canPublish ? `<details class="git-fold"><summary class="ovk-btn ovk-btn--ghost
@@ -417,7 +417,7 @@ ${canPublish ? `<details class="git-fold"><summary class="ovk-btn ovk-btn--ghost
function contributorsPage({ row, contributors }) {
function contributorsPage({ row, contributors }) {
const base = `/${row.owner_username}/${row.name}`;
const base = `/${row.owner_username}/${row.name}`;
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Contributors</h1></header>${repoTabs(row, 'contributors')}
return `<div class="git-list-page"><header class="git-page-head"><p class="git-crumb"><a href="${base}">${esc(row.full_name || `${row.owner_username}/${row.name}`)}</a></p><h1>Contributors</h1></header>${repoTabs(row, 'contributors')}
-${contributors.length ? `<ul class="git-people">${contributors.map((c) => `<li><span class="git-avatar">${esc(initials(c.name))}</span><span><b>${esc(c.name)}</b>${c.username ? ` <a href="/@${esc(c.username)}">@${esc(c.username)}</a>` : ''}<small>${esc(c.email)}</small></span><em>${plural(c.commits, 'commit')}</em></li>`).join('')}</ul>` : '<p class="git-empty">No contributors yet.</p>'}</div>`;
+${contributors.length ? `<ul class="git-people">${contributors.map((c) => `<li><span class="git-avatar">${esc(initials(c.name))}</span><span><b>${esc(c.name)}</b>${c.username ? ` <a href="/@${esc(c.username)}">@${esc(c.username)}</a>` : ''}<small>${esc(c.email)}</small></span><em>${plural(c.commits, 'commit')}</em></li>`).join('')}</ul>` : kit.empty({ icon: 'members', title: 'No contributors yet', text: 'People who commit to this repository appear here.' })}</div>`;
}
}
function insightsPage({ row, data }) {
function insightsPage({ row, data }) {
@@ -442,7 +442,7 @@ ${repoTabs(row, 'insights')}
@@ -442,7 +442,7 @@ ${repoTabs(row, 'insights')}
// ── Profile, new, messages ───────────────────────────────────
// ── Profile, new, messages ───────────────────────────────────
function profilePage({ profile }) {
function profilePage({ profile }) {
return `<div class="git-profile"><header class="git-profile-head"><span class="git-avatar git-avatar--big">${esc(initials(profile.username))}</span><div><h1>@${esc(profile.username)}</h1><p>${plural(profile.repositories.length, 'public repository', 'public repositories')} on OpenVibe Git.</p></div></header>
return `<div class="git-profile"><header class="git-profile-head"><span class="git-avatar git-avatar--big">${esc(initials(profile.username))}</span><div><h1>@${esc(profile.username)}</h1><p>${plural(profile.repositories.length, 'public repository', 'public repositories')} on OpenVibe Git.</p></div></header>
-${profile.repositories.length ? `<ul class="git-cards git-cards--wide">${profile.repositories.map(card).join('')}</ul>` : '<p class="git-empty">No public repositories yet.</p>'}</div>`;
+${profile.repositories.length ? `<ul class="git-cards git-cards--wide">${profile.repositories.map(card).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No public repositories yet', text: 'When this person makes one public it appears here.' })}</div>`;
}
}
function newPage({ viewer, values = {}, error = null }) {
function newPage({ viewer, values = {}, error = null }) {
@@ -64,7 +64,7 @@ function machinesPage({ viewer, machines = [], error = null }) {
@@ -64,7 +64,7 @@ function machinesPage({ viewer, machines = [], error = null }) {
return `<div class="git-list-page">
return `<div class="git-list-page">
<header class="git-page-head"><h1>Your computers</h1><p>Repositories on your own computers, published through Link. Read-only: Git can look, never write. Browsing works while the computer is online.</p></header>
<header class="git-page-head"><h1>Your computers</h1><p>Repositories on your own computers, published through Link. Read-only: Git can look, never write. Browsing works while the computer is online.</p></header>
${error ? `<p class="git-error" role="alert">${esc(error)}</p>` : ''}
${error ? `<p class="git-error" role="alert">${esc(error)}</p>` : ''}
-${machines.length ? `<ul class="git-refs git-machines">${rows}</ul>` : '<p class="git-empty">No computer is connected yet.</p>'}
+${machines.length ? `<ul class="git-refs git-machines">${rows}</ul>` : kit.empty({ icon: 'computer', title: 'No computer is connected yet', text: 'Link publishes the repositories under a folder on your computer. Connect one below to browse them here.', action: { href: '/machines', label: 'Connect a computer', icon: 'computer' } })}
<details class="git-fold"><summary class="ovk-btn ovk-btn--primary">${icon('plus')} Connect a computer</summary>
<details class="git-fold"><summary class="ovk-btn ovk-btn--primary">${icon('plus')} Connect a computer</summary>
<form method="post" action="/machines" class="git-form">
<form method="post" action="/machines" class="git-form">
<label>Name <input name="name" maxlength="80" placeholder="my laptop"></label>
<label>Name <input name="name" maxlength="80" placeholder="my laptop"></label>
@@ -94,7 +94,7 @@ function machinePage({ machine, repos = [] }) {
@@ -94,7 +94,7 @@ function machinePage({ machine, repos = [] }) {
return `<div class="git-list-page">
return `<div class="git-list-page">
<header class="git-page-head"><p class="git-crumb"><a href="/machines">Your computers</a></p><h1>${esc(machine.name)}</h1>
<header class="git-page-head"><p class="git-crumb"><a href="/machines">Your computers</a></p><h1>${esc(machine.name)}</h1>
<p>${machine.online ? 'Online now, read-only through Link.' : `Offline since ${esc(ago(machine.offline_since || machine.last_seen_at))}. Start Link to browse.`}</p></header>
<p>${machine.online ? 'Online now, read-only through Link.' : `Offline since ${esc(ago(machine.offline_since || machine.last_seen_at))}. Start Link to browse.`}</p></header>
-${repos.length ? `<ul class="git-refs git-machines">${rows}</ul>` : '<p class="git-empty">No repositories under the root yet.</p>'}
+${repos.length ? `<ul class="git-refs git-machines">${rows}</ul>` : kit.empty({ icon: 'code', title: 'No repositories under the root yet', text: 'Choose a folder on the computer when Link asks where to publish from.' })}
</div>`;
</div>`;
}
}
@@ -144,7 +144,7 @@ function machineCommitsPage({ machine, repo, ref, path = '', commits = [], offli
@@ -144,7 +144,7 @@ function machineCommitsPage({ machine, repo, ref, path = '', commits = [], offli
${commits.length ? `<ul class="git-commit-list">${commits.map((c) => `<li><a href="${base}/commits${qs({ ref })}">
${commits.length ? `<ul class="git-commit-list">${commits.map((c) => `<li><a href="${base}/commits${qs({ ref })}">
<span class="git-avatar git-avatar--commit">${icon('commit')}</span>
<span class="git-avatar git-avatar--commit">${icon('commit')}</span>
<span class="git-commit-main"><b>${esc(c.subject || '(no message)')}</b><small>${esc(c.author)} · ${esc(ago(c.date))}</small></span>
<span class="git-commit-main"><b>${esc(c.subject || '(no message)')}</b><small>${esc(c.author)} · ${esc(ago(c.date))}</small></span>
-<span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span></a></li>`).join('')}</ul>` : '<p class="git-empty">No commits here yet.</p>'}
+<span class="git-commit-sha"><code>${esc(c.short || c.sha.slice(0, 7))}</code></span></a></li>`).join('')}</ul>` : kit.empty({ icon: 'code', title: 'No commits here yet', text: 'The first commit starts the history.' })}
</div>`;
</div>`;
}
}
@@ -5,6 +5,7 @@
@@ -5,6 +5,7 @@
* line on the left, and one merge box on the right (one column on a phone, the box first). Buttons are sized to their
* line on the left, and one merge box on the right (one column on a phone, the box first). Buttons are sized to their
* words; only Merge is filled; Close is quiet. Every form keeps its fields and routes (server/http/review.js).
* words; only Merge is filled; Close is quiet. Every form keeps its fields and routes (server/http/review.js).
*/
*/
+const kit = require('openvibe-shared/kit');
const { esc } = require('../render/html');
const { esc } = require('../render/html');
const { renderMarkdown } = require('../render/markdown');
const { renderMarkdown } = require('../render/markdown');
const glyphs = require('openvibe-shared/glyphs');
const glyphs = require('openvibe-shared/glyphs');
@@ -60,7 +61,7 @@ function list(r, kind, items) {
@@ -60,7 +61,7 @@ function list(r, kind, items) {
const path = kind === 'change' ? 'changes' : 'issues';
const path = kind === 'change' ? 'changes' : 'issues';
const rows = items.map((c) => `<li><a class="git-row" href="${base(r)}/${path}/${c.number}">${state(c.state)}<span class="git-row-title">${esc(c.title)}</span><span class="git-row-num">#${c.number}</span>${(c.labels || []).map((l) => `<small class="git-label">${esc(l)}</small>`).join('')}</a></li>`).join('');
const rows = items.map((c) => `<li><a class="git-row" href="${base(r)}/${path}/${c.number}">${state(c.state)}<span class="git-row-title">${esc(c.title)}</span><span class="git-row-num">#${c.number}</span>${(c.labels || []).map((l) => `<small class="git-label">${esc(l)}</small>`).join('')}</a></li>`).join('');
return wrap(r, kind === 'change' ? 'Changes' : 'Issues', `<div class="git-list-bar"><a class="ovk-btn ovk-btn--primary" href="${base(r)}/${path}/new">${ic('plus')}<span>New ${kind}</span></a></div>
return wrap(r, kind === 'change' ? 'Changes' : 'Issues', `<div class="git-list-bar"><a class="ovk-btn ovk-btn--primary" href="${base(r)}/${path}/new">${ic('plus')}<span>New ${kind}</span></a></div>
-${rows ? `<ul class="git-review-list">${rows}</ul>` : `<div class="git-empty">${ic(kind === 'change' ? 'branch' : 'chat', 22)}<p><b>No ${kind === 'change' ? 'changes' : 'issues'} yet</b></p><p class="git-muted">${kind === 'change' ? 'Push a branch, then open a change to ask for it to be merged.' : 'Report a problem or suggest something for this repository.'}</p></div>`}`, path);
+${rows ? `<ul class="git-review-list">${rows}</ul>` : kit.empty({ icon: kind === 'change' ? 'code' : 'chat', title: `No ${kind === 'change' ? 'changes' : 'issues'} yet`, text: kind === 'change' ? 'Push a branch, then open a change to ask for it to be merged.' : 'Report a problem or suggest something for this repository.', action: { href: `${base(r)}/${path}/new`, label: `Open ${kind === 'change' ? 'a change' : 'an issue'}`, icon: 'plus' } })}`, path);
}
}
function form(r, kind) {
function form(r, kind) {
@@ -66,6 +66,18 @@ const { boot } = require('./helpers/boot');
@@ -66,6 +66,18 @@ const { boot } = require('./helpers/boot');
assert.equal(doc.status, 200);
assert.equal(doc.status, 200);
assert.equal(doc.json().openapi, '3.1.0');
assert.equal(doc.json().openapi, '3.1.0');
+ // Empty states are the shared kit's, with the next action as a button.
+ const views = require('../server/views');
+ const emptyHome = views.home({ viewer: null, stats: { repositories: 0, pushes_this_week: 0, people: 0, clones_this_week: 0 }, trending: [], activity: [], official: [], baseUrl: 'https://git.openvibe.codes' });
+ assert.match(emptyHome, /class="ovk-empty"/);
+ assert.match(emptyHome, /No repositories yet/);
+ assert.match(emptyHome, /ovk-btn[^>]*href="\/auth\/login\?next=%2Fnew"/);
+ assert.match(emptyHome, /Nothing has happened yet/);
+ assert.match(views.explore({ viewer: null, repositories: [], query: 'zzz', filters: {} }), /Nothing matches/);
+ assert.match(views.branchesPage({ row: { owner_username: 'a', name: 'b', default_branch: 'main' }, branches: [] }), /No branches yet/);
+ assert.match(views.releasesPage({ row: { owner_username: 'a', name: 'b' }, releases: [], viewer: null }), /No releases yet/);
+ assert.match(views.profilePage({ profile: { username: 'nobody', repositories: [] } }), /No public repositories yet/);
+
// No placeholder content anywhere.
// No placeholder content anywhere.
assert.doesNotMatch(home.text, /lorem ipsum/i);
assert.doesNotMatch(home.text, /lorem ipsum/i);
assert.match(repo.full_name, /sam\/phone-first/);
assert.match(repo.full_name, /sam\/phone-first/);