Fix note-field space bar hijacking video, rework Outline topics as documents
- Lesson page's global keyboard shortcuts (space/arrows for play-pause/ seek/volume) fired regardless of focus, so typing a space in the notes textarea toggled the video instead of typing. Now skipped entirely whenever a text field/select has focus. - Outline topics no longer spawn their own top-level collection each time. A topic is now a document inside one fixed, user-configured collection (Settings -> Outline Integration -> Default collection), and a lesson's note becomes a child document nested under its topic - matching how notes actually get organized in a real Outline instance instead of cluttering the collections list with one per topic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,29 @@ before.
|
|||||||
session), including Settings → Test Connection hitting the real HTTP
|
session), including Settings → Test Connection hitting the real HTTP
|
||||||
path.
|
path.
|
||||||
|
|
||||||
|
## Outline integration follow-up fixes (this session)
|
||||||
|
|
||||||
|
- **Space bar bug**: the lesson page's global keyboard-shortcut handler
|
||||||
|
(space/arrows for play-pause/seek/volume) fired regardless of what had
|
||||||
|
focus, so typing a space in the notes textarea toggled the video instead.
|
||||||
|
Fixed with an `isTypingTarget()` guard that skips the shortcut handler
|
||||||
|
entirely when a textarea/input/select/contenteditable has focus.
|
||||||
|
- **Topic model redesign**: topics used to each spawn their own top-level
|
||||||
|
Outline *collection*. Changed so a topic is instead a *document* inside
|
||||||
|
one fixed, user-configured collection (Settings → Outline Integration →
|
||||||
|
Default collection, e.g. "Training Notes") - a lesson's note becomes a
|
||||||
|
child document nested under its topic document, matching how the user
|
||||||
|
actually organizes their real Outline instance. `list_outline_topics()`
|
||||||
|
fetches the collection's documents and filters to `parentDocumentId is
|
||||||
|
None` locally rather than trusting `documents.list`'s `collectionId`/
|
||||||
|
`parentDocumentId` request filters - both are marked deprecated in
|
||||||
|
Outline's own API spec with unclear recursive-vs-top-level semantics, so
|
||||||
|
filtering the response ourselves is the version that can't be wrong.
|
||||||
|
Verified end-to-end against a local mock Outline server: resolving a new
|
||||||
|
topic creates a top-level doc, pushing a note creates a child doc under
|
||||||
|
it, and the child never leaks back into the topic chooser as if it were
|
||||||
|
a topic itself.
|
||||||
|
|
||||||
## Known limitations still open
|
## Known limitations still open
|
||||||
- App is unauthenticated by design (matches upstream) — settings and hidden-path
|
- App is unauthenticated by design (matches upstream) — settings and hidden-path
|
||||||
curation apply app-wide, not per-browser/per-user.
|
curation apply app-wide, not per-browser/per-user.
|
||||||
|
|||||||
+95
-29
@@ -911,7 +911,7 @@ OUTLINE_CONFIG_FILE = os.path.join(DATA_DIR, 'outline_config.json')
|
|||||||
|
|
||||||
|
|
||||||
def load_outline_config() -> Dict[str, str]:
|
def load_outline_config() -> Dict[str, str]:
|
||||||
"""Load the Outline base URL + API token. Never exposed via GET beyond 'configured'."""
|
"""Load the Outline base URL + API token + default collection. Never exposed via GET beyond 'configured'."""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(OUTLINE_CONFIG_FILE):
|
if os.path.exists(OUTLINE_CONFIG_FILE):
|
||||||
with open(OUTLINE_CONFIG_FILE, 'r') as f:
|
with open(OUTLINE_CONFIG_FILE, 'r') as f:
|
||||||
@@ -919,20 +919,33 @@ def load_outline_config() -> Dict[str, str]:
|
|||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
return {
|
return {
|
||||||
'base_url': data.get('base_url', ''),
|
'base_url': data.get('base_url', ''),
|
||||||
'api_token': data.get('api_token', '')
|
'api_token': data.get('api_token', ''),
|
||||||
|
'default_collection_id': data.get('default_collection_id', ''),
|
||||||
|
'default_collection_name': data.get('default_collection_name', '')
|
||||||
}
|
}
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
print(f"Could not load Outline config: {e}")
|
print(f"Could not load Outline config: {e}")
|
||||||
return {'base_url': '', 'api_token': ''}
|
return {'base_url': '', 'api_token': '', 'default_collection_id': '', 'default_collection_name': ''}
|
||||||
|
|
||||||
|
|
||||||
def save_outline_config(base_url: str, api_token: str) -> None:
|
def save_outline_config(base_url: str, api_token: str,
|
||||||
"""Save the Outline base URL + API token. A blank api_token keeps the
|
default_collection_id: Optional[str] = None,
|
||||||
previously stored one, so the URL can be updated without re-pasting it."""
|
default_collection_name: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Save the Outline base URL / API token / default collection. A blank
|
||||||
|
api_token keeps the previously stored one, so the URL can be updated
|
||||||
|
without re-pasting it; default_collection_id/name are only touched when
|
||||||
|
explicitly passed (None means 'leave as-is'), so saving the URL/token
|
||||||
|
doesn't clear an already-chosen collection.
|
||||||
|
"""
|
||||||
current = load_outline_config()
|
current = load_outline_config()
|
||||||
current['base_url'] = base_url.rstrip('/')
|
current['base_url'] = base_url.rstrip('/')
|
||||||
if api_token:
|
if api_token:
|
||||||
current['api_token'] = api_token
|
current['api_token'] = api_token
|
||||||
|
if default_collection_id is not None:
|
||||||
|
current['default_collection_id'] = default_collection_id
|
||||||
|
if default_collection_name is not None:
|
||||||
|
current['default_collection_name'] = default_collection_name
|
||||||
os.makedirs(DATA_DIR, exist_ok=True)
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
with open(OUTLINE_CONFIG_FILE, 'w') as f:
|
with open(OUTLINE_CONFIG_FILE, 'w') as f:
|
||||||
json.dump(current, f, indent=2)
|
json.dump(current, f, indent=2)
|
||||||
@@ -965,39 +978,72 @@ def _outline_request(path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
raise RuntimeError(f'Could not reach Outline: {e.reason}')
|
raise RuntimeError(f'Could not reach Outline: {e.reason}')
|
||||||
|
|
||||||
|
|
||||||
|
def list_outline_topics() -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Top-level documents in the configured default collection - each one is
|
||||||
|
a "topic" a lesson note can be filed under. Fetches every document in
|
||||||
|
the collection and filters to parentDocumentId is None locally, rather
|
||||||
|
than relying on documents.list's collectionId/parentDocumentId filter
|
||||||
|
params directly - both are marked deprecated in Outline's own API spec
|
||||||
|
and their exact recursive-vs-top-level behavior isn't documented, so
|
||||||
|
filtering the response ourselves is the version that can't be wrong.
|
||||||
|
"""
|
||||||
|
config = load_outline_config()
|
||||||
|
collection_id = config['default_collection_id']
|
||||||
|
if not collection_id:
|
||||||
|
raise RuntimeError('No default Outline collection set - pick one in Settings first')
|
||||||
|
|
||||||
|
result = _outline_request('documents.list', {'collectionId': collection_id, 'limit': 100})
|
||||||
|
return [
|
||||||
|
{'id': d['id'], 'name': d['title']}
|
||||||
|
for d in result.get('data', [])
|
||||||
|
if not d.get('parentDocumentId')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def resolve_outline_topic(name: str) -> Dict[str, str]:
|
def resolve_outline_topic(name: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Find an existing Outline collection with this exact name, or create one.
|
Find an existing topic document (a top-level document in the configured
|
||||||
Matching by name first (rather than always creating) means naming a
|
default collection) with this exact title, or create one. Matching by
|
||||||
"new" topic that happens to match one you already made doesn't spawn a
|
name first (rather than always creating) means naming a "new" topic
|
||||||
duplicate collection.
|
that happens to match one you already made doesn't spawn a duplicate.
|
||||||
"""
|
"""
|
||||||
existing = _outline_request('collections.list', {'limit': 100})
|
for topic in list_outline_topics():
|
||||||
for c in existing.get('data', []):
|
if topic['name'] == name:
|
||||||
if c['name'] == name:
|
return topic
|
||||||
return {'id': c['id'], 'name': c['name']}
|
config = load_outline_config()
|
||||||
created = _outline_request('collections.create', {'name': name, 'permission': 'read_write'})
|
created = _outline_request('documents.create', {
|
||||||
|
'title': name,
|
||||||
|
'text': '',
|
||||||
|
'collectionId': config['default_collection_id'],
|
||||||
|
'publish': True
|
||||||
|
})
|
||||||
return {'id': created['data']['id'], 'name': name}
|
return {'id': created['data']['id'], 'name': name}
|
||||||
|
|
||||||
|
|
||||||
def push_note_to_outline(course: Course, lesson_path: str, lesson_title: str,
|
def push_note_to_outline(course: Course, lesson_path: str, lesson_title: str,
|
||||||
topic_id: str, new_topic_name: str) -> Dict[str, Any]:
|
topic_id: str, new_topic_name: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Push a lesson's saved note to Outline as a document in the chosen topic
|
Push a lesson's saved note to Outline as a child document nested under
|
||||||
(collection). Creates the document on the first push and updates the
|
the chosen topic document (which itself lives in the configured default
|
||||||
same one (by the id stashed in the progress file) on every push after
|
collection - see list_outline_topics). Creates the note document on the
|
||||||
that.
|
first push and updates the same one (by the id stashed in the progress
|
||||||
|
file) on every push after that.
|
||||||
|
|
||||||
new_topic_name is a fallback for a topic that somehow never got resolved
|
new_topic_name is a fallback for a topic that somehow never got resolved
|
||||||
client-side (see resolve_outline_topic) - the normal path resolves a
|
client-side (see resolve_outline_topic) - the normal path resolves a
|
||||||
brand-new topic name to a real collection id as soon as it's typed
|
brand-new topic name to a real document id as soon as it's typed
|
||||||
(POST /api/outline/resolve-topic), specifically so this fire-and-forget,
|
(POST /api/outline/resolve-topic), specifically so this fire-and-forget,
|
||||||
can't-read-the-response push never has to decide "is this actually a
|
can't-read-the-response push never has to decide "is this actually a
|
||||||
new topic" on its own. A page that fires pagehide more than once for
|
new topic" on its own. A page that fires pagehide more than once for
|
||||||
the same view (bfcache restore, for instance) would otherwise re-send
|
the same view (bfcache restore, for instance) would otherwise re-send
|
||||||
the same "new topic" intent every time and create a duplicate
|
the same "new topic" intent every time and create a duplicate topic
|
||||||
collection per navigation.
|
document per navigation.
|
||||||
"""
|
"""
|
||||||
|
config = load_outline_config()
|
||||||
|
if not config['default_collection_id']:
|
||||||
|
return {'success': False, 'error': 'No default Outline collection set - pick one in Settings first'}
|
||||||
|
|
||||||
progress = ProgressTracker.load_progress(course)
|
progress = ProgressTracker.load_progress(course)
|
||||||
entry = progress.get(lesson_path, {})
|
entry = progress.get(lesson_path, {})
|
||||||
note = entry.get('note', '')
|
note = entry.get('note', '')
|
||||||
@@ -1018,7 +1064,8 @@ def push_note_to_outline(course: Course, lesson_path: str, lesson_title: str,
|
|||||||
created = _outline_request('documents.create', {
|
created = _outline_request('documents.create', {
|
||||||
'title': title,
|
'title': title,
|
||||||
'text': note,
|
'text': note,
|
||||||
'collectionId': topic_id,
|
'collectionId': config['default_collection_id'],
|
||||||
|
'parentDocumentId': topic_id,
|
||||||
'publish': True
|
'publish': True
|
||||||
})
|
})
|
||||||
document_id = created['data']['id']
|
document_id = created['data']['id']
|
||||||
@@ -1996,20 +2043,29 @@ def update_lesson_note_topic_api():
|
|||||||
|
|
||||||
@app.route('/api/outline/config', methods=['GET'])
|
@app.route('/api/outline/config', methods=['GET'])
|
||||||
def get_outline_config_api():
|
def get_outline_config_api():
|
||||||
"""Whether Outline is configured, and the (non-secret) base URL - never the token."""
|
"""Whether Outline is configured, the (non-secret) base URL, and the default collection - never the token."""
|
||||||
config = load_outline_config()
|
config = load_outline_config()
|
||||||
return jsonify({'configured': bool(config['base_url'] and config['api_token']), 'base_url': config['base_url']})
|
return jsonify({
|
||||||
|
'configured': bool(config['base_url'] and config['api_token']),
|
||||||
|
'base_url': config['base_url'],
|
||||||
|
'default_collection_id': config['default_collection_id'],
|
||||||
|
'default_collection_name': config['default_collection_name']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/outline/config', methods=['POST'])
|
@app.route('/api/outline/config', methods=['POST'])
|
||||||
def set_outline_config_api():
|
def set_outline_config_api():
|
||||||
"""Save the Outline base URL / API token."""
|
"""Save the Outline base URL / API token / default collection."""
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
base_url = (data.get('base_url') or '').strip()
|
base_url = (data.get('base_url') or '').strip()
|
||||||
api_token = (data.get('api_token') or '').strip()
|
api_token = (data.get('api_token') or '').strip()
|
||||||
if not base_url:
|
if not base_url:
|
||||||
return jsonify({'error': 'Outline URL is required'}), 400
|
return jsonify({'error': 'Outline URL is required'}), 400
|
||||||
save_outline_config(base_url, api_token)
|
save_outline_config(
|
||||||
|
base_url, api_token,
|
||||||
|
default_collection_id=data.get('default_collection_id'),
|
||||||
|
default_collection_name=data.get('default_collection_name')
|
||||||
|
)
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
@@ -2025,7 +2081,7 @@ def test_outline_connection_api():
|
|||||||
|
|
||||||
@app.route('/api/outline/collections')
|
@app.route('/api/outline/collections')
|
||||||
def list_outline_collections_api():
|
def list_outline_collections_api():
|
||||||
"""The topic chooser's option list - live from Outline, not locally cached."""
|
"""Every Outline collection - used by the Settings page's default-collection picker."""
|
||||||
try:
|
try:
|
||||||
result = _outline_request('collections.list', {'limit': 100})
|
result = _outline_request('collections.list', {'limit': 100})
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
@@ -2034,10 +2090,20 @@ def list_outline_collections_api():
|
|||||||
return jsonify({'collections': collections})
|
return jsonify({'collections': collections})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/outline/topics')
|
||||||
|
def list_outline_topics_api():
|
||||||
|
"""The lesson page's topic chooser option list - top-level documents in the configured default collection."""
|
||||||
|
try:
|
||||||
|
topics = list_outline_topics()
|
||||||
|
except RuntimeError as e:
|
||||||
|
return jsonify({'topics': [], 'error': str(e)})
|
||||||
|
return jsonify({'topics': topics})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/outline/resolve-topic', methods=['POST'])
|
@app.route('/api/outline/resolve-topic', methods=['POST'])
|
||||||
def resolve_outline_topic_api():
|
def resolve_outline_topic_api():
|
||||||
"""
|
"""
|
||||||
Resolve a freshly-typed topic name to a real Outline collection id right
|
Resolve a freshly-typed topic name to a real Outline document id right
|
||||||
away (find-or-create), rather than deferring to the fire-and-forget
|
away (find-or-create), rather than deferring to the fire-and-forget
|
||||||
pagehide push - see push_note_to_outline's docstring for why that
|
pagehide push - see push_note_to_outline's docstring for why that
|
||||||
matters (a page that fires pagehide more than once, e.g. via bfcache,
|
matters (a page that fires pagehide more than once, e.g. via bfcache,
|
||||||
|
|||||||
@@ -606,11 +606,11 @@
|
|||||||
const topicStatus = document.getElementById('outline-topic-status');
|
const topicStatus = document.getElementById('outline-topic-status');
|
||||||
|
|
||||||
if (topicSelect) {
|
if (topicSelect) {
|
||||||
fetch('/api/outline/collections')
|
fetch('/api/outline/topics')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const newOption = topicSelect.querySelector('option[value="__new__"]');
|
const newOption = topicSelect.querySelector('option[value="__new__"]');
|
||||||
(data.collections || []).forEach(function(c) {
|
(data.topics || []).forEach(function(c) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = c.id;
|
opt.value = c.id;
|
||||||
opt.textContent = c.name;
|
opt.textContent = c.name;
|
||||||
@@ -624,7 +624,7 @@
|
|||||||
newTopicInput.value = INITIAL_TOPIC_NAME;
|
newTopicInput.value = INITIAL_TOPIC_NAME;
|
||||||
}
|
}
|
||||||
if (data.error && topicStatus) {
|
if (data.error && topicStatus) {
|
||||||
topicStatus.textContent = 'Outline not reachable - topic list may be incomplete';
|
topicStatus.textContent = data.error;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -805,8 +805,17 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts - skip while typing in the note (or any
|
||||||
|
// other text field), so space/arrow keys type normally instead
|
||||||
|
// of hijacking video playback.
|
||||||
|
function isTypingTarget(el) {
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = el.tagName;
|
||||||
|
return tag === 'TEXTAREA' || tag === 'INPUT' || tag === 'SELECT' || el.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (isTypingTarget(e.target)) return;
|
||||||
if (activeMedia && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
if (activeMedia && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||||
switch(e.key) {
|
switch(e.key) {
|
||||||
case ' ':
|
case ' ':
|
||||||
|
|||||||
+37
-3
@@ -524,8 +524,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Outline Integration</h2>
|
<h2>Outline Integration</h2>
|
||||||
<p class="setting-desc" style="margin-bottom: 12px;">
|
<p class="setting-desc" style="margin-bottom: 12px;">
|
||||||
Pick a topic on a lesson's note (on the lesson page) to push it to that topic's collection in
|
Pick a topic on a lesson's note (on the lesson page) to push it there when you leave the page -
|
||||||
Outline when you leave the page. Notes without a topic stay local only.
|
each topic becomes a document inside the collection below, and the note is nested under it.
|
||||||
|
Notes without a topic stay local only.
|
||||||
</p>
|
</p>
|
||||||
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||||||
<label for="outline-url">Outline URL
|
<label for="outline-url">Outline URL
|
||||||
@@ -536,6 +537,12 @@
|
|||||||
<label for="outline-token" style="margin-top: 4px;">API token</label>
|
<label for="outline-token" style="margin-top: 4px;">API token</label>
|
||||||
<input type="password" id="outline-token" class="hex-input" style="width: 100%; font-family: var(--font-family);"
|
<input type="password" id="outline-token" class="hex-input" style="width: 100%; font-family: var(--font-family);"
|
||||||
placeholder="Leave blank to keep the current token">
|
placeholder="Leave blank to keep the current token">
|
||||||
|
<label for="outline-collection" style="margin-top: 4px;">Default collection
|
||||||
|
<span class="setting-desc">Where topic documents get created - e.g. "Training Notes"</span>
|
||||||
|
</label>
|
||||||
|
<select id="outline-collection">
|
||||||
|
<option value="">Save your URL/token first, then pick one</option>
|
||||||
|
</select>
|
||||||
<div style="display: flex; gap: 10px; margin-top: 4px;">
|
<div style="display: flex; gap: 10px; margin-top: 4px;">
|
||||||
<button class="btn" onclick="saveOutlineConfig()">Save</button>
|
<button class="btn" onclick="saveOutlineConfig()">Save</button>
|
||||||
<button class="btn btn-secondary" onclick="testOutlineConnection()">Test Connection</button>
|
<button class="btn btn-secondary" onclick="testOutlineConnection()">Test Connection</button>
|
||||||
@@ -1002,6 +1009,25 @@
|
|||||||
document.getElementById('outline-url').value = data.base_url || '';
|
document.getElementById('outline-url').value = data.base_url || '';
|
||||||
document.getElementById('outline-config-status').textContent =
|
document.getElementById('outline-config-status').textContent =
|
||||||
data.configured ? 'Configured' : 'Not configured';
|
data.configured ? 'Configured' : 'Not configured';
|
||||||
|
loadOutlineCollectionsDropdown(data.default_collection_id || '');
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOutlineCollectionsDropdown(selectedId) {
|
||||||
|
const select = document.getElementById('outline-collection');
|
||||||
|
fetch('/api/outline/collections')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const collections = data.collections || [];
|
||||||
|
if (collections.length === 0) {
|
||||||
|
select.innerHTML = '<option value="">No collections found - check your URL/token</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
select.innerHTML = collections.map(c =>
|
||||||
|
`<option value="${c.id}">${c.name}</option>`
|
||||||
|
).join('');
|
||||||
|
if (selectedId) select.value = selectedId;
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -1010,12 +1036,20 @@
|
|||||||
const status = document.getElementById('outline-test-status');
|
const status = document.getElementById('outline-test-status');
|
||||||
const url = document.getElementById('outline-url').value.trim();
|
const url = document.getElementById('outline-url').value.trim();
|
||||||
const token = document.getElementById('outline-token').value.trim();
|
const token = document.getElementById('outline-token').value.trim();
|
||||||
|
const collectionSelect = document.getElementById('outline-collection');
|
||||||
|
const collectionId = collectionSelect.value;
|
||||||
|
const collectionName = collectionId ? collectionSelect.options[collectionSelect.selectedIndex].text : '';
|
||||||
status.style.color = 'var(--text-muted)';
|
status.style.color = 'var(--text-muted)';
|
||||||
status.textContent = 'Saving...';
|
status.textContent = 'Saving...';
|
||||||
fetch('/api/outline/config', {
|
fetch('/api/outline/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ base_url: url, api_token: token })
|
body: JSON.stringify({
|
||||||
|
base_url: url,
|
||||||
|
api_token: token,
|
||||||
|
default_collection_id: collectionId,
|
||||||
|
default_collection_name: collectionName
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|||||||
Reference in New Issue
Block a user