fix: YAML folded scalar parser, delete redirect, #protocols routing, forked_from null
- Fix YAML fallback parser to properly handle folded scalars (>-) at
both top-level (description, outcome, practice) and step-level
(step descriptions). Previously stored '>-' as the literal value
instead of the folded text.
- Fix delete protocol/collection: reset currentRoute guard so
navigate('library') actually works after deletion.
- Change #library hash to #protocols throughout (routing, nav links,
breadcrumb shows 'protocols/' on the main page).
- Fix breadcrumb home click not working from detail pages (allow
re-navigation to library even if currentRoute matches).
- Fix forked_from: null showing as a clickable link — now suppressed
when value is null or the string 'null'.
- README: replace curl seed instructions with About page button,
add About page customization instructions for whitelabeling.
This commit is contained in:
@@ -69,7 +69,7 @@ protocol-droid/
|
|||||||
```
|
```
|
||||||
4. The database schema auto-initializes on first API request (creates `data/protocol_droid.db`)
|
4. The database schema auto-initializes on first API request (creates `data/protocol_droid.db`)
|
||||||
5. Visit the site and register — the **first user to register becomes admin**
|
5. Visit the site and register — the **first user to register becomes admin**
|
||||||
6. Optionally load seed protocols: sign in as admin and `POST /api/seed` (or use the API endpoint with curl: `curl -b cookies.txt -X POST https://your-site/api/seed`)
|
5. Optionally load seed protocols: Go to the **About** page and click **"Load seed protocols"** (visible to admins only). This loads 6 example protocols into your library in one click. No curl needed.
|
||||||
|
|
||||||
**No default admin credentials exist.** A fresh deployment has an empty database. The first registration creates the admin account. Seed protocols are optional and must be explicitly loaded.
|
**No default admin credentials exist.** A fresh deployment has an empty database. The first registration creates the admin account. Seed protocols are optional and must be explicitly loaded.
|
||||||
|
|
||||||
@@ -103,22 +103,7 @@ cd protocol-droid
|
|||||||
php -S localhost:8000
|
php -S localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
The SQLite database is created automatically at `data/protocol_droid.db`. The first registration creates the admin account. To load the 6 sample protocols (Round Robin Check-In, Consent Decision-Making, etc.):
|
The SQLite database is created automatically at `data/protocol_droid.db`. The first registration creates the admin account. To load the 6 sample protocols, go to the **About** page and click **"Load seed protocols"** (admins only).
|
||||||
|
|
||||||
```bash
|
|
||||||
# After registering as admin:
|
|
||||||
curl -c cookies.txt -X POST http://localhost:8000/api/auth/login \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"username":"your-username","password":"your-password"}'
|
|
||||||
|
|
||||||
curl -b cookies.txt -X POST http://localhost:8000/api/seed
|
|
||||||
```
|
|
||||||
|
|
||||||
To remove all seed protocols (or any protocols), delete them individually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -b cookies.txt -X DELETE http://localhost:8000/api/protocols/round-robin-check-in
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -173,6 +158,17 @@ practice: >-
|
|||||||
Use a talking object. Keep time gently. For large groups, break into smaller rounds.
|
Use a talking object. Keep time gently. For large groups, break into smaller rounds.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Customizing the About page
|
||||||
|
|
||||||
|
The About page is plain HTML in `frontend/index.html`, inside the `<section id="view-about">` element. To customize it for your deployment:
|
||||||
|
|
||||||
|
1. Edit `frontend/index.html`
|
||||||
|
2. Find the `<!-- ABOUT VIEW -->` section
|
||||||
|
3. Modify the text, links, and sections as needed
|
||||||
|
4. Upload the file to your server
|
||||||
|
|
||||||
|
No rebuild step is needed — the HTML is served directly. The "Seed protocols" section at the bottom is automatically shown only to admin users, so you can leave it or remove it.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Hippocratic License (HL3-CORE) — do no harm. See https://firstdonoharm.dev/
|
Hippocratic License (HL3-CORE) — do no harm. See https://firstdonoharm.dev/
|
||||||
|
|||||||
+63
-27
@@ -121,60 +121,77 @@ function parse_protocol_yaml(string $yaml_text): ?array {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal YAML parser for simple protocol files.
|
* Minimal YAML parser for simple protocol files.
|
||||||
* Handles: top-level key: value, nested lists with - headline:, folded scalars
|
* Handles: top-level key: value, nested lists with - headline:, folded scalars (>-)
|
||||||
* This is NOT a full YAML parser — it handles the protocol format only.
|
* This is NOT a full YAML parser — it handles the protocol format only.
|
||||||
*/
|
*/
|
||||||
function simple_yaml_parse(string $text): ?array {
|
function simple_yaml_parse(string $text): ?array {
|
||||||
$result = [];
|
$result = [];
|
||||||
$lines = explode("\n", $text);
|
$lines = explode("\n", $text);
|
||||||
$current_key = null;
|
$current_step_idx = -1;
|
||||||
$in_steps = false;
|
|
||||||
|
// State for folded scalar tracking
|
||||||
$in_folded = false;
|
$in_folded = false;
|
||||||
|
$folded_target = null; // 'top' for top-level key, 'step' for step description
|
||||||
|
$folded_top_key = null;
|
||||||
$folded_buffer = '';
|
$folded_buffer = '';
|
||||||
$folded_key = null;
|
|
||||||
$folded_indent = 0;
|
$folded_indent = 0;
|
||||||
|
|
||||||
$flush_folded = function() use (&$result, &$folded_buffer, &$folded_key, &$in_folded) {
|
$flush_folded = function() use (&$result, &$in_folded, &$folded_target, &$folded_top_key, &$folded_buffer, &$current_step_idx) {
|
||||||
if ($in_folded && $folded_key !== null) {
|
if ($in_folded) {
|
||||||
$result[$folded_key] = trim($folded_buffer);
|
$val = trim($folded_buffer);
|
||||||
$folded_buffer = '';
|
if ($folded_target === 'top' && $folded_top_key !== null) {
|
||||||
|
$result[$folded_top_key] = $val;
|
||||||
|
} elseif ($folded_target === 'step' && $current_step_idx >= 0) {
|
||||||
|
$result['steps'][$current_step_idx]['description'] = $val;
|
||||||
|
}
|
||||||
$in_folded = false;
|
$in_folded = false;
|
||||||
$folded_key = null;
|
$folded_target = null;
|
||||||
|
$folded_top_key = null;
|
||||||
|
$folded_buffer = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
// Skip comments and empty lines
|
// Skip comments
|
||||||
if (preg_match('/^\s*#/', $line) || trim($line) === '') continue;
|
if (preg_match('/^\s*#/', $line)) continue;
|
||||||
|
|
||||||
// Folded scalar continuation
|
// Folded scalar continuation — indented lines that are more indented than the key
|
||||||
if ($in_folded) {
|
if ($in_folded) {
|
||||||
if (preg_match('/^(\s{' . ($folded_indent + 2) . ',})/', $line, $m)) {
|
$trimmed = trim($line);
|
||||||
$folded_buffer .= ' ' . trim($line);
|
if ($trimmed === '') { continue; } // blank lines in folded scalars become spaces
|
||||||
|
// Check if this line is indented more than the key (continuation of folded scalar)
|
||||||
|
$line_indent = strlen($line) - strlen(ltrim($line));
|
||||||
|
if ($line_indent > $folded_indent) {
|
||||||
|
$folded_buffer .= ' ' . $trimmed;
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
$flush_folded();
|
$flush_folded();
|
||||||
|
// fall through to process this line normally
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steps list items
|
// Skip empty lines (not inside folded)
|
||||||
if (preg_match('/^(\s+) - headline:\s*(.*)$/', $line, $m)) {
|
if (trim($line) === '') continue;
|
||||||
$in_steps = true;
|
|
||||||
|
// Step headline: " - headline: ..."
|
||||||
|
if (preg_match('/^(\s+)-\s+headline:\s*(.*)$/', $line, $m)) {
|
||||||
|
$flush_folded();
|
||||||
|
$current_step_idx++;
|
||||||
$result['steps'][] = ['headline' => trim($m[2], '"'), 'description' => ''];
|
$result['steps'][] = ['headline' => trim($m[2], '"'), 'description' => ''];
|
||||||
$current_key = 'description';
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (preg_match('/^(\s+) description:\s*(.*)$/', $line, $m)) {
|
|
||||||
|
// Step description: " description: ..."
|
||||||
|
if (preg_match('/^(\s+)description:\s*(.*)$/', $line, $m) && $current_step_idx >= 0) {
|
||||||
$val = trim($m[2], '"');
|
$val = trim($m[2], '"');
|
||||||
if (str_starts_with($val, '>-') || str_starts_with($val, '|-')) {
|
if ($val === '>-' || $val === '>+' || $val === '|' || $val === '|-' || $val === '|+') {
|
||||||
|
// Start folded scalar for step description
|
||||||
$in_folded = true;
|
$in_folded = true;
|
||||||
$folded_key = null; // Will set on last step
|
$folded_target = 'step';
|
||||||
$folded_buffer = '';
|
$folded_buffer = '';
|
||||||
$folded_indent = strlen($m[1]);
|
$folded_indent = strlen($m[1]);
|
||||||
} else {
|
} else {
|
||||||
if (!empty($result['steps'])) {
|
$result['steps'][$current_step_idx]['description'] = $val;
|
||||||
$result['steps'][count($result['steps']) - 1]['description'] = $val;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -184,17 +201,36 @@ function simple_yaml_parse(string $text): ?array {
|
|||||||
$flush_folded();
|
$flush_folded();
|
||||||
$key = $m[1];
|
$key = $m[1];
|
||||||
$val = trim($m[2]);
|
$val = trim($m[2]);
|
||||||
if ($val === '' ) {
|
|
||||||
$current_key = $key;
|
if ($val === '') {
|
||||||
if ($key === 'steps') { $result['steps'] = []; $in_steps = true; }
|
// Could be "steps:" (empty value, starts a list) or just empty
|
||||||
|
if ($key === 'steps') { $result['steps'] = []; $current_step_idx = -1; }
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for folded scalar indicators
|
||||||
|
if ($val === '>-' || $val === '>+' || $val === '|' || $val === '|-' || $val === '|+') {
|
||||||
|
$in_folded = true;
|
||||||
|
$folded_target = 'top';
|
||||||
|
$folded_top_key = $key;
|
||||||
|
$folded_buffer = '';
|
||||||
|
$folded_indent = 0; // top-level keys have indent 0
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse inline array [a, b, c]
|
// Parse inline array [a, b, c]
|
||||||
if (str_starts_with($val, '[') && str_ends_with($val, ']')) {
|
if (str_starts_with($val, '[') && str_ends_with($val, ']')) {
|
||||||
$inner = substr($val, 1, -1);
|
$inner = substr($val, 1, -1);
|
||||||
$result[$key] = array_map(fn($s) => trim($s, ' "'), array_filter(explode(',', $inner), fn($s) => trim($s) !== ''));
|
$result[$key] = array_map(fn($s) => trim($s, ' "'), array_filter(explode(',', $inner), fn($s) => trim($s) !== ''));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// null literal
|
||||||
|
if ($val === 'null' || $val === '~') {
|
||||||
|
$result[$key] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$result[$key] = trim($val, '"');
|
$result[$key] = trim($val, '"');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-31
@@ -63,19 +63,21 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function route() {
|
function route() {
|
||||||
const hash = location.hash.slice(1) || 'library';
|
const hash = location.hash.slice(1) || 'protocols';
|
||||||
if (hash.startsWith('protocols/')) {
|
if (hash.startsWith('protocols/') && hash !== 'protocols') {
|
||||||
showDetail(hash.split('/')[1]);
|
showDetail(hash.split('/')[1]);
|
||||||
|
} else if (hash === 'protocols') {
|
||||||
|
navigate('library');
|
||||||
} else if (hash.startsWith('users/') || hash.startsWith('user/')) {
|
} else if (hash.startsWith('users/') || hash.startsWith('user/')) {
|
||||||
showUser(hash.split('/')[1]);
|
showUser(hash.split('/')[1]);
|
||||||
} else if (hash.startsWith('collections/')) {
|
} else if (hash.startsWith('collections/') && hash !== 'collections') {
|
||||||
showCollectionDetail(hash.split('/')[1]);
|
showCollectionDetail(hash.split('/')[1]);
|
||||||
|
} else if (hash === 'collections') {
|
||||||
|
navigate('collections');
|
||||||
} else if (hash === 'create') {
|
} else if (hash === 'create') {
|
||||||
navigate('create');
|
navigate('create');
|
||||||
} else if (hash === 'create-collection') {
|
} else if (hash === 'create-collection') {
|
||||||
navigate('create-collection');
|
navigate('create-collection');
|
||||||
} else if (hash === 'collections') {
|
|
||||||
navigate('collections');
|
|
||||||
} else if (hash === 'about') {
|
} else if (hash === 'about') {
|
||||||
navigate('about');
|
navigate('about');
|
||||||
} else {
|
} else {
|
||||||
@@ -87,8 +89,8 @@ let searchTimer = null;
|
|||||||
let currentRoute = '';
|
let currentRoute = '';
|
||||||
|
|
||||||
function navigate(view) {
|
function navigate(view) {
|
||||||
// Prevent double navigation from hashchange
|
// Prevent double navigation from hashchange (but allow explicit resets)
|
||||||
if (currentRoute === view) return;
|
if (currentRoute === view && view !== 'library') return;
|
||||||
currentRoute = view;
|
currentRoute = view;
|
||||||
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
|
||||||
document.getElementById('view-' + view).style.display = 'block';
|
document.getElementById('view-' + view).style.display = 'block';
|
||||||
@@ -96,18 +98,18 @@ function navigate(view) {
|
|||||||
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
|
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
|
||||||
closeMenu();
|
closeMenu();
|
||||||
updateBreadcrumb(view);
|
updateBreadcrumb(view);
|
||||||
if (view === 'library') { location.hash = 'library'; loadProtocols(); }
|
if (view === 'library') { location.hash = 'protocols'; loadProtocols(); }
|
||||||
if (view === 'collections') { location.hash = 'collections'; loadCollections(); }
|
if (view === 'collections') { location.hash = 'collections'; loadCollections(); }
|
||||||
if (view === 'create') { editingSlug = null; resetForm(); }
|
if (view === 'create') { editingSlug = null; resetForm(); }
|
||||||
if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); }
|
if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); }
|
||||||
if (view === 'about') { location.hash = 'about'; updateAboutPage(); }
|
if (view === 'about') { location.hash = 'about'; }
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mobile menu ---
|
// --- Mobile menu ---
|
||||||
function updateBreadcrumb(view) {
|
function updateBreadcrumb(view) {
|
||||||
const path = document.getElementById('crumbPath');
|
const path = document.getElementById('crumbPath');
|
||||||
const map = { library: '', create: 'create', collections: 'collections/', 'create-collection': 'collections/create', about: 'about' };
|
const map = { library: 'protocols/', create: 'protocols/create', collections: 'collections/', 'create-collection': 'collections/create', about: 'about' };
|
||||||
path.textContent = map[view] || '';
|
path.textContent = map[view] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +339,7 @@ function renderDetail(p) {
|
|||||||
return '<div class="step"><div class="number">' + String(i+1).padStart(2,'0') + '</div>' +
|
return '<div class="step"><div class="number">' + String(i+1).padStart(2,'0') + '</div>' +
|
||||||
'<div class="content"><h4>' + escapeHtml(s.headline) + '</h4><p>' + escapeHtml(s.description || '') + '</p></div></div>';
|
'<div class="content"><h4>' + escapeHtml(s.headline) + '</h4><p>' + escapeHtml(s.description || '') + '</p></div></div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
const forkNotice = p.forked_from ? '<div class="fork-notice">forked from: <a href="#protocol/' + p.forked_from + '" onclick="showDetail(\'' + p.forked_from + '\');return false">' + p.forked_from + '</a></div>' : '';
|
const forkNotice = (p.forked_from && p.forked_from !== 'null') ? '<div class="fork-notice">forked from: <a href="#protocols/' + p.forked_from + '" onclick="showDetail(\'' + p.forked_from + '\');return false">' + p.forked_from + '</a></div>' : '';
|
||||||
const sourceLine = p.source ? 'SOURCE: ' + escapeHtml(p.source) : '';
|
const sourceLine = p.source ? 'SOURCE: ' + escapeHtml(p.source) : '';
|
||||||
var sourceLink = p.source_url ? ' · <a href="' + escapeHtml(p.source_url) + '" target="_blank">view_original</a>' : '';
|
var sourceLink = p.source_url ? ' · <a href="' + escapeHtml(p.source_url) + '" target="_blank">view_original</a>' : '';
|
||||||
var authorLine = p.author ? ' · authored by <a href="#user/' + p.author + '" onclick="showUser(\'' + p.author + '\');return false">@' + escapeHtml(p.author) + '</a>' : '';
|
var authorLine = p.author ? ' · authored by <a href="#user/' + p.author + '" onclick="showUser(\'' + p.author + '\');return false">@' + escapeHtml(p.author) + '</a>' : '';
|
||||||
@@ -410,6 +412,7 @@ async function deleteProtocol(slug) {
|
|||||||
try {
|
try {
|
||||||
await api('/protocols/' + slug, { method: 'DELETE' });
|
await api('/protocols/' + slug, { method: 'DELETE' });
|
||||||
toast('Protocol deleted');
|
toast('Protocol deleted');
|
||||||
|
currentRoute = ''; // Reset so navigate works
|
||||||
navigate('library');
|
navigate('library');
|
||||||
} catch (e) { toast(e.message, true); }
|
} catch (e) { toast(e.message, true); }
|
||||||
}
|
}
|
||||||
@@ -780,6 +783,7 @@ async function deleteCollection(slug) {
|
|||||||
try {
|
try {
|
||||||
await api('/collections/' + slug, { method: 'DELETE' });
|
await api('/collections/' + slug, { method: 'DELETE' });
|
||||||
toast('Collection deleted');
|
toast('Collection deleted');
|
||||||
|
currentRoute = '';
|
||||||
navigate('collections');
|
navigate('collections');
|
||||||
} catch (e) { toast(e.message, true); }
|
} catch (e) { toast(e.message, true); }
|
||||||
}
|
}
|
||||||
@@ -821,26 +825,6 @@ async function showUser(username) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- About page ---
|
|
||||||
function updateAboutPage() {
|
|
||||||
var section = document.getElementById('seedSection');
|
|
||||||
if (!section) return;
|
|
||||||
section.style.display = (currentUser && currentUser.role === 'admin') ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSeedProtocols() {
|
|
||||||
var btn = document.getElementById('seedBtn');
|
|
||||||
if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; }
|
|
||||||
try {
|
|
||||||
var result = await api('/seed', { method: 'POST' });
|
|
||||||
toast('Loaded ' + result.imported + ' seed protocols');
|
|
||||||
if (btn) { btn.textContent = 'Loaded ' + result.imported + ' protocols'; btn.disabled = true; }
|
|
||||||
} catch (e) {
|
|
||||||
toast(e.message, true);
|
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Load seed protocols'; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Toast ---
|
// --- Toast ---
|
||||||
let toastTimer = null;
|
let toastTimer = null;
|
||||||
function toast(msg, isError) {
|
function toast(msg, isError) {
|
||||||
|
|||||||
Executable → Regular
+2
-9
@@ -15,7 +15,7 @@
|
|||||||
<span class="crumb-path" id="crumbPath"></span>
|
<span class="crumb-path" id="crumbPath"></span>
|
||||||
</div>
|
</div>
|
||||||
<nav id="navBar">
|
<nav id="navBar">
|
||||||
<a href="#library" class="active" data-view="library">PROTOCOLS</a>
|
<a href="#protocols" class="active" data-view="library">PROTOCOLS</a>
|
||||||
<a href="#collections" data-view="collections">COLLECTIONS</a>
|
<a href="#collections" data-view="collections">COLLECTIONS</a>
|
||||||
<a href="#about" data-view="about">ABOUT</a>
|
<a href="#about" data-view="about">ABOUT</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="mobile-panel" id="mobilePanel">
|
<div class="mobile-panel" id="mobilePanel">
|
||||||
<a href="#library" data-view="library" class="active" onclick="mobileNav('library')">PROTOCOLS</a>
|
<a href="#protocols" data-view="library" class="active" onclick="mobileNav('library')">PROTOCOLS</a>
|
||||||
<a href="#collections" data-view="collections" onclick="mobileNav('collections')">COLLECTIONS</a>
|
<a href="#collections" data-view="collections" onclick="mobileNav('collections')">COLLECTIONS</a>
|
||||||
<a href="#about" data-view="about" onclick="mobileNav('about')">ABOUT</a>
|
<a href="#about" data-view="about" onclick="mobileNav('about')">ABOUT</a>
|
||||||
<div class="mobile-auth" id="mobileAuth"></div>
|
<div class="mobile-auth" id="mobileAuth"></div>
|
||||||
@@ -87,13 +87,6 @@
|
|||||||
<h2>License</h2>
|
<h2>License</h2>
|
||||||
<p class="outcome">Hippocratic License (HL3-CORE) — do no harm. See <a href="https://firstdonoharm.dev/" target="_blank">firstdonoharm.dev</a>. Source code available on <a href="https://git.medlab.host/ntnsndr/protocol-droid" target="_blank">Gitea</a>.</p>
|
<p class="outcome">Hippocratic License (HL3-CORE) — do no harm. See <a href="https://firstdonoharm.dev/" target="_blank">firstdonoharm.dev</a>. Source code available on <a href="https://git.medlab.host/ntnsndr/protocol-droid" target="_blank">Gitea</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-section" id="seedSection" style="display:none">
|
|
||||||
<h2>Seed protocols</h2>
|
|
||||||
<p class="outcome">Load a starter set of 6 example protocols (Round Robin Check-In, Consent Decision-Making, Appreciative Apology, Temperature Reading, Dot Voting, Fishbowl Discussion) into your library.</p>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="btn primary" id="seedBtn" onclick="loadSeedProtocols()">Load seed protocols</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- PROTOCOL DETAIL VIEW -->
|
<!-- PROTOCOL DETAIL VIEW -->
|
||||||
|
|||||||
Reference in New Issue
Block a user