594 lines
20 KiB
JavaScript
594 lines
20 KiB
JavaScript
/**
|
|
* app-bookmark-save.js
|
|
* Bookmark dialog (create/update) + group dialog (create/update).
|
|
*/
|
|
|
|
import { showAsyncFlash } from './app-async-flash.js';
|
|
import { warnOnce } from '../core/app-dom.js';
|
|
|
|
export function init() {
|
|
const openBtn = document.querySelector('[data-bookmark-open-dialog]');
|
|
const bookmarkDialog = document.querySelector('[data-app-bookmark-dialog]');
|
|
if (!(bookmarkDialog instanceof HTMLDialogElement) || !(openBtn instanceof HTMLButtonElement)) return;
|
|
|
|
const bookmarkForm = bookmarkDialog.querySelector('[data-bookmark-save-form]');
|
|
if (!(bookmarkForm instanceof HTMLFormElement)) return;
|
|
|
|
const groupDialog = document.querySelector('[data-app-bookmark-group-dialog]');
|
|
const groupForm = groupDialog instanceof HTMLDialogElement
|
|
? groupDialog.querySelector('[data-bookmark-group-save-form]')
|
|
: null;
|
|
const hasGroupDialog = groupDialog instanceof HTMLDialogElement && groupForm instanceof HTMLFormElement;
|
|
|
|
const root = document.documentElement;
|
|
const csrfKey = String(root.dataset.csrfKey || '').trim();
|
|
const csrfToken = String(root.dataset.csrfToken || '').trim();
|
|
const moduleContext = { module: 'bookmark-save' };
|
|
|
|
if (!csrfKey || !csrfToken) {
|
|
warnOnce('UI_DATA_MISSING', 'Missing CSRF metadata for bookmark dialog', moduleContext);
|
|
return;
|
|
}
|
|
|
|
const nameInput = bookmarkForm.querySelector('input[name="name"]');
|
|
const groupSelect = bookmarkForm.querySelector('[data-bookmark-group-select]');
|
|
const newGroupBtn = bookmarkForm.querySelector('[data-bookmark-new-group]');
|
|
const closeBookmarkBtns = bookmarkDialog.querySelectorAll('[data-bookmark-dialog-close]');
|
|
const submitBtn = bookmarkDialog.querySelector('[data-bookmark-submit-label]');
|
|
const titleEl = bookmarkDialog.querySelector('#app-bookmark-dialog-title');
|
|
|
|
const maxBookmarkMessage = bookmarkDialog.dataset.maxMessage || '';
|
|
const bookmarkMessageSaved = bookmarkDialog.dataset.msgSaved || 'Bookmark saved';
|
|
const bookmarkMessageUpdated = bookmarkDialog.dataset.msgUpdated || 'Bookmark updated';
|
|
const bookmarkMessageErrorGeneric = bookmarkDialog.dataset.msgErrorGeneric || 'Bookmark action failed';
|
|
const bookmarkMessageNameRequired = bookmarkDialog.dataset.msgNameRequired || 'Bookmark name is required';
|
|
const bookmarkApiErrorMessages = {
|
|
name_required: bookmarkDialog.dataset.msgNameRequired || bookmarkMessageErrorGeneric,
|
|
url_required: bookmarkDialog.dataset.msgUrlRequired || bookmarkMessageErrorGeneric,
|
|
group_not_found: bookmarkDialog.dataset.msgGroupNotFound || bookmarkMessageErrorGeneric,
|
|
not_found: bookmarkMessageErrorGeneric,
|
|
create_failed: bookmarkMessageErrorGeneric,
|
|
update_failed: bookmarkMessageErrorGeneric
|
|
};
|
|
|
|
let editingBookmarkId = null;
|
|
let bookmarkReturnState = null;
|
|
|
|
let groupEditingId = null;
|
|
let groupMode = 'create';
|
|
let groupSource = 'sidebar';
|
|
|
|
const syncModalBodyClass = () => {
|
|
const isOpen = bookmarkDialog.open || (hasGroupDialog && groupDialog.open);
|
|
document.body.classList.toggle('modal-is-open', isOpen);
|
|
};
|
|
|
|
const reportError = (code, message, details = {}) => {
|
|
warnOnce(code, message, { ...moduleContext, ...details });
|
|
showAsyncFlash('error', message);
|
|
};
|
|
|
|
const parseJsonSafe = async (response) => {
|
|
try {
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const resolveBookmarkApiErrorMessage = (errorCode) => {
|
|
if (!errorCode) return bookmarkMessageErrorGeneric;
|
|
return bookmarkApiErrorMessages[errorCode] || bookmarkMessageErrorGeneric;
|
|
};
|
|
|
|
const runReloadWithSuccess = (message) => {
|
|
showAsyncFlash('success', message, 2500);
|
|
window.setTimeout(() => window.location.reload(), 450);
|
|
};
|
|
|
|
const closeBookmarkDialog = () => {
|
|
try {
|
|
if (bookmarkDialog.open) bookmarkDialog.close();
|
|
} catch {
|
|
// no-op
|
|
}
|
|
bookmarkForm.removeAttribute('aria-busy');
|
|
syncModalBodyClass();
|
|
};
|
|
|
|
const openBookmarkDialog = () => {
|
|
try {
|
|
bookmarkDialog.showModal();
|
|
syncModalBodyClass();
|
|
return true;
|
|
} catch (error) {
|
|
reportError('UI_DIALOG_OPEN_FAILED', bookmarkMessageErrorGeneric, { error });
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const setBookmarkSubmitPending = (pending) => {
|
|
if (submitBtn instanceof HTMLButtonElement) {
|
|
submitBtn.disabled = pending;
|
|
}
|
|
bookmarkForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
|
};
|
|
|
|
const setBookmarkSubmitLabel = (label) => {
|
|
if (!(submitBtn instanceof HTMLElement)) return;
|
|
const text = String(label || '').trim();
|
|
if (!text) return;
|
|
submitBtn.textContent = text;
|
|
};
|
|
|
|
const applyBookmarkModeUi = (isEditMode) => {
|
|
if (titleEl) {
|
|
titleEl.textContent = isEditMode
|
|
? (bookmarkDialog.dataset.titleEdit || 'Bookmarks')
|
|
: (bookmarkDialog.dataset.titleCreate || 'Bookmarks');
|
|
}
|
|
setBookmarkSubmitLabel(isEditMode
|
|
? (bookmarkDialog.dataset.labelUpdate || 'Save')
|
|
: (bookmarkDialog.dataset.labelSave || 'Save'));
|
|
};
|
|
|
|
const configureBookmarkModeFromTopbar = () => {
|
|
const isEditMode = openBtn.hasAttribute('data-bookmark-existing-id');
|
|
applyBookmarkModeUi(isEditMode);
|
|
|
|
if (isEditMode) {
|
|
editingBookmarkId = openBtn.dataset.bookmarkExistingId || null;
|
|
if (nameInput) nameInput.value = openBtn.dataset.bookmarkExistingName || '';
|
|
if (groupSelect) groupSelect.value = openBtn.dataset.bookmarkExistingGroup || '';
|
|
return;
|
|
}
|
|
|
|
editingBookmarkId = null;
|
|
bookmarkForm.reset();
|
|
if (nameInput) {
|
|
const title = document.title.replace(/\s*[|–—].*$/, '').trim();
|
|
nameInput.value = title || '';
|
|
}
|
|
};
|
|
|
|
const openBookmarkEditFromSidebar = (detail) => {
|
|
const bookmarkId = Number.parseInt(String(detail?.id ?? ''), 10);
|
|
const bookmarkName = String(detail?.name ?? '').trim();
|
|
const groupId = String(detail?.groupId ?? '').trim();
|
|
if (bookmarkId <= 0 || !bookmarkName) {
|
|
reportError('UI_INVALID_STATE', bookmarkMessageErrorGeneric, { detail });
|
|
return;
|
|
}
|
|
|
|
editingBookmarkId = String(bookmarkId);
|
|
applyBookmarkModeUi(true);
|
|
if (nameInput instanceof HTMLInputElement) {
|
|
nameInput.value = bookmarkName;
|
|
}
|
|
if (groupSelect instanceof HTMLSelectElement) {
|
|
groupSelect.value = groupId;
|
|
}
|
|
|
|
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
|
|
nameInput.focus();
|
|
nameInput.select();
|
|
}
|
|
};
|
|
|
|
const captureBookmarkState = () => ({
|
|
isEditMode: editingBookmarkId !== null && editingBookmarkId !== '',
|
|
bookmarkId: editingBookmarkId,
|
|
name: nameInput instanceof HTMLInputElement ? nameInput.value : '',
|
|
groupId: groupSelect instanceof HTMLSelectElement ? groupSelect.value : ''
|
|
});
|
|
|
|
const restoreBookmarkFromState = (selectedGroupId = null) => {
|
|
const state = bookmarkReturnState;
|
|
bookmarkReturnState = null;
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
applyBookmarkModeUi(Boolean(state.isEditMode));
|
|
editingBookmarkId = state.isEditMode ? state.bookmarkId : null;
|
|
if (nameInput instanceof HTMLInputElement) {
|
|
nameInput.value = state.name || '';
|
|
}
|
|
if (groupSelect instanceof HTMLSelectElement) {
|
|
groupSelect.value = selectedGroupId ?? state.groupId ?? '';
|
|
}
|
|
|
|
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
|
|
nameInput.focus();
|
|
nameInput.select();
|
|
}
|
|
};
|
|
|
|
// Bookmark dialog open
|
|
openBtn.addEventListener('click', () => {
|
|
configureBookmarkModeFromTopbar();
|
|
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
|
|
nameInput.select();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('app:bookmark-edit', (event) => {
|
|
const detail = event instanceof CustomEvent ? event.detail : null;
|
|
openBookmarkEditFromSidebar(detail);
|
|
});
|
|
|
|
closeBookmarkBtns.forEach((btn) => {
|
|
btn.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
closeBookmarkDialog();
|
|
});
|
|
});
|
|
|
|
bookmarkDialog.addEventListener('click', (event) => {
|
|
if (event.target === bookmarkDialog) {
|
|
closeBookmarkDialog();
|
|
}
|
|
});
|
|
bookmarkDialog.addEventListener('close', () => {
|
|
bookmarkForm.removeAttribute('aria-busy');
|
|
syncModalBodyClass();
|
|
});
|
|
bookmarkDialog.addEventListener('cancel', () => {
|
|
bookmarkForm.removeAttribute('aria-busy');
|
|
syncModalBodyClass();
|
|
});
|
|
|
|
// Save bookmark
|
|
bookmarkForm.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
|
|
const name = nameInput instanceof HTMLInputElement ? nameInput.value.trim() : '';
|
|
if (!name) {
|
|
showAsyncFlash('warning', bookmarkMessageNameRequired);
|
|
return;
|
|
}
|
|
|
|
const body = new FormData();
|
|
body.set(csrfKey, csrfToken);
|
|
body.set('name', name);
|
|
body.set('group_id', groupSelect instanceof HTMLSelectElement ? groupSelect.value : '');
|
|
|
|
setBookmarkSubmitPending(true);
|
|
if (editingBookmarkId) {
|
|
body.set('id', editingBookmarkId);
|
|
try {
|
|
const response = await fetch(getAppBase() + 'bookmarks/update-data', {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
body
|
|
});
|
|
const json = await parseJsonSafe(response);
|
|
if (!response.ok || !json) {
|
|
reportError('FETCH_FAILED', bookmarkMessageErrorGeneric, { status: response.status });
|
|
return;
|
|
}
|
|
if (!json.ok) {
|
|
reportError('FETCH_FAILED', resolveBookmarkApiErrorMessage(json.error), { error: json.error });
|
|
return;
|
|
}
|
|
|
|
closeBookmarkDialog();
|
|
runReloadWithSuccess(bookmarkMessageUpdated);
|
|
} catch (error) {
|
|
reportError('FETCH_ERROR', bookmarkMessageErrorGeneric, { error });
|
|
} finally {
|
|
setBookmarkSubmitPending(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
body.set('url', currentRelativeUrl());
|
|
try {
|
|
const response = await fetch(getAppBase() + 'bookmarks/save-data', {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
body
|
|
});
|
|
const json = await parseJsonSafe(response);
|
|
if (!response.ok || !json) {
|
|
reportError('FETCH_FAILED', bookmarkMessageErrorGeneric, { status: response.status });
|
|
return;
|
|
}
|
|
if (!json.ok) {
|
|
if (json.error === 'max_reached') {
|
|
showAsyncFlash('warning', maxBookmarkMessage || 'Maximum bookmarks reached');
|
|
return;
|
|
}
|
|
reportError('FETCH_FAILED', resolveBookmarkApiErrorMessage(json.error), { error: json.error });
|
|
return;
|
|
}
|
|
|
|
closeBookmarkDialog();
|
|
runReloadWithSuccess(json.mode === 'updated' ? bookmarkMessageUpdated : bookmarkMessageSaved);
|
|
} catch (error) {
|
|
reportError('FETCH_ERROR', bookmarkMessageErrorGeneric, { error });
|
|
} finally {
|
|
setBookmarkSubmitPending(false);
|
|
}
|
|
});
|
|
|
|
if (!hasGroupDialog) {
|
|
return;
|
|
}
|
|
|
|
const groupNameInput = groupForm.querySelector('input[name="name"]');
|
|
const groupIconInput = groupForm.querySelector('input[name="icon"]');
|
|
const groupIconPicker = groupForm.querySelector('[data-bookmark-group-icon-picker]');
|
|
const groupCloseBtns = groupDialog.querySelectorAll('[data-bookmark-group-dialog-close]');
|
|
const groupTitleEl = groupDialog.querySelector('#app-bookmark-group-dialog-title');
|
|
const groupSubmitBtn = groupDialog.querySelector('[data-bookmark-group-submit-label]');
|
|
|
|
const groupMessageCreated = groupDialog.dataset.msgCreated || 'Group created';
|
|
const groupMessageUpdated = groupDialog.dataset.msgUpdated || 'Group updated';
|
|
const groupMessageErrorGeneric = groupDialog.dataset.msgErrorGeneric || 'Group action failed';
|
|
const groupMessageNameRequired = groupDialog.dataset.msgNameRequired || 'Group name is required';
|
|
const maxGroupMessage = groupDialog.dataset.maxMessage || 'Maximum groups reached';
|
|
const groupApiErrorMessages = {
|
|
name_required: groupMessageNameRequired,
|
|
not_found: groupMessageErrorGeneric,
|
|
create_failed: groupMessageErrorGeneric,
|
|
update_failed: groupMessageErrorGeneric
|
|
};
|
|
|
|
const resolveGroupApiErrorMessage = (errorCode) => {
|
|
if (!errorCode) return groupMessageErrorGeneric;
|
|
return groupApiErrorMessages[errorCode] || groupMessageErrorGeneric;
|
|
};
|
|
|
|
const closeGroupDialog = () => {
|
|
try {
|
|
if (groupDialog.open) groupDialog.close();
|
|
} catch {
|
|
// no-op
|
|
}
|
|
groupForm.removeAttribute('aria-busy');
|
|
syncModalBodyClass();
|
|
};
|
|
|
|
const openGroupDialog = () => {
|
|
try {
|
|
groupDialog.showModal();
|
|
syncModalBodyClass();
|
|
return true;
|
|
} catch (error) {
|
|
reportError('UI_DIALOG_OPEN_FAILED', groupMessageErrorGeneric, { error });
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const setGroupSubmitPending = (pending) => {
|
|
if (groupSubmitBtn instanceof HTMLButtonElement) {
|
|
groupSubmitBtn.disabled = pending;
|
|
}
|
|
groupForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
|
};
|
|
|
|
const setGroupSubmitLabel = (label) => {
|
|
if (!(groupSubmitBtn instanceof HTMLElement)) return;
|
|
const text = String(label || '').trim();
|
|
if (!text) return;
|
|
groupSubmitBtn.textContent = text;
|
|
};
|
|
|
|
const setGroupIcon = (icon) => {
|
|
const value = String(icon || '').trim() || 'bi-folder';
|
|
if (groupIconInput instanceof HTMLInputElement) {
|
|
groupIconInput.value = value;
|
|
}
|
|
if (groupIconPicker instanceof HTMLElement) {
|
|
groupIconPicker.querySelectorAll('.active').forEach((item) => item.classList.remove('active'));
|
|
const selected = groupIconPicker.querySelector(`[data-icon="${value}"]`);
|
|
if (selected instanceof HTMLElement) {
|
|
selected.classList.add('active');
|
|
}
|
|
}
|
|
};
|
|
|
|
const upsertGroupOption = (group) => {
|
|
if (!(groupSelect instanceof HTMLSelectElement)) {
|
|
return;
|
|
}
|
|
const id = String(group?.id ?? '').trim();
|
|
const name = String(group?.name ?? '').trim();
|
|
if (!id || !name) {
|
|
return;
|
|
}
|
|
|
|
const existing = Array.from(groupSelect.options).find((option) => option.value === id);
|
|
if (existing) {
|
|
existing.textContent = name;
|
|
} else {
|
|
const option = document.createElement('option');
|
|
option.value = id;
|
|
option.textContent = name;
|
|
groupSelect.appendChild(option);
|
|
}
|
|
};
|
|
|
|
const configureGroupDialog = ({ mode, source, id = null, name = '', icon = 'bi-folder', restoreState = null }) => {
|
|
groupMode = mode === 'edit' ? 'edit' : 'create';
|
|
groupSource = source === 'bookmark' ? 'bookmark' : 'sidebar';
|
|
groupEditingId = groupMode === 'edit' ? id : null;
|
|
bookmarkReturnState = groupSource === 'bookmark' ? restoreState : null;
|
|
|
|
groupForm.reset();
|
|
setGroupIcon(icon);
|
|
if (groupNameInput instanceof HTMLInputElement) {
|
|
groupNameInput.value = String(name || '');
|
|
}
|
|
|
|
if (groupTitleEl instanceof HTMLElement) {
|
|
groupTitleEl.textContent = groupMode === 'edit'
|
|
? (groupDialog.dataset.titleEdit || 'Edit group')
|
|
: (groupDialog.dataset.titleCreate || 'New group');
|
|
}
|
|
setGroupSubmitLabel(groupMode === 'edit'
|
|
? (groupDialog.dataset.labelUpdate || 'Save')
|
|
: (groupDialog.dataset.labelSave || 'Save'));
|
|
};
|
|
|
|
const handleGroupCancel = () => {
|
|
closeGroupDialog();
|
|
if (groupSource === 'bookmark' && bookmarkReturnState) {
|
|
restoreBookmarkFromState();
|
|
} else {
|
|
bookmarkReturnState = null;
|
|
}
|
|
};
|
|
|
|
const openCreateGroupFromBookmarkDialog = () => {
|
|
const state = captureBookmarkState();
|
|
closeBookmarkDialog();
|
|
configureGroupDialog({
|
|
mode: 'create',
|
|
source: 'bookmark',
|
|
restoreState: state
|
|
});
|
|
if (openGroupDialog() && groupNameInput instanceof HTMLInputElement) {
|
|
groupNameInput.focus();
|
|
groupNameInput.select();
|
|
}
|
|
};
|
|
|
|
if (newGroupBtn instanceof HTMLButtonElement) {
|
|
newGroupBtn.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
openCreateGroupFromBookmarkDialog();
|
|
});
|
|
}
|
|
|
|
if (groupIconPicker instanceof HTMLElement) {
|
|
groupIconPicker.addEventListener('click', (event) => {
|
|
const target = event.target instanceof HTMLElement ? event.target.closest('[data-icon]') : null;
|
|
if (!(target instanceof HTMLElement)) return;
|
|
event.preventDefault();
|
|
setGroupIcon(target.dataset.icon || 'bi-folder');
|
|
});
|
|
}
|
|
|
|
groupCloseBtns.forEach((btn) => {
|
|
btn.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
handleGroupCancel();
|
|
});
|
|
});
|
|
|
|
groupDialog.addEventListener('click', (event) => {
|
|
if (event.target === groupDialog) {
|
|
handleGroupCancel();
|
|
}
|
|
});
|
|
groupDialog.addEventListener('cancel', (event) => {
|
|
event.preventDefault();
|
|
handleGroupCancel();
|
|
});
|
|
groupDialog.addEventListener('close', () => {
|
|
groupForm.removeAttribute('aria-busy');
|
|
syncModalBodyClass();
|
|
});
|
|
|
|
document.addEventListener('app:bookmark-group-edit', (event) => {
|
|
const detail = event instanceof CustomEvent ? event.detail : null;
|
|
const groupId = Number.parseInt(String(detail?.id ?? ''), 10);
|
|
const groupName = String(detail?.name ?? '').trim();
|
|
const groupIcon = String(detail?.icon ?? 'bi-folder').trim() || 'bi-folder';
|
|
if (groupId <= 0 || !groupName) {
|
|
reportError('UI_INVALID_STATE', groupMessageErrorGeneric, { detail });
|
|
return;
|
|
}
|
|
|
|
configureGroupDialog({
|
|
mode: 'edit',
|
|
source: 'sidebar',
|
|
id: groupId,
|
|
name: groupName,
|
|
icon: groupIcon
|
|
});
|
|
if (openGroupDialog() && groupNameInput instanceof HTMLInputElement) {
|
|
groupNameInput.focus();
|
|
groupNameInput.select();
|
|
}
|
|
});
|
|
|
|
groupForm.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
|
|
const name = groupNameInput instanceof HTMLInputElement ? groupNameInput.value.trim() : '';
|
|
if (!name) {
|
|
showAsyncFlash('warning', groupMessageNameRequired);
|
|
return;
|
|
}
|
|
const icon = groupIconInput instanceof HTMLInputElement ? groupIconInput.value : 'bi-folder';
|
|
|
|
const body = new FormData();
|
|
body.set(csrfKey, csrfToken);
|
|
body.set('name', name);
|
|
body.set('icon', icon);
|
|
if (groupMode === 'edit' && groupEditingId) {
|
|
body.set('id', String(groupEditingId));
|
|
}
|
|
|
|
setGroupSubmitPending(true);
|
|
try {
|
|
const response = await fetch(getAppBase() + 'bookmarks/group-save-data', {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
body
|
|
});
|
|
const json = await parseJsonSafe(response);
|
|
if (!response.ok || !json) {
|
|
reportError('FETCH_FAILED', groupMessageErrorGeneric, { status: response.status });
|
|
return;
|
|
}
|
|
if (!json.ok) {
|
|
if (json.error === 'max_reached') {
|
|
showAsyncFlash('warning', maxGroupMessage);
|
|
return;
|
|
}
|
|
reportError('FETCH_FAILED', resolveGroupApiErrorMessage(json.error), { error: json.error });
|
|
return;
|
|
}
|
|
if (!json.group) {
|
|
reportError('UI_INVALID_STATE', groupMessageErrorGeneric, { reason: 'missing_group' });
|
|
return;
|
|
}
|
|
|
|
const mode = json.mode === 'updated' ? 'updated' : 'created';
|
|
const successMessage = mode === 'updated' ? groupMessageUpdated : groupMessageCreated;
|
|
|
|
closeGroupDialog();
|
|
if (groupSource === 'bookmark') {
|
|
upsertGroupOption(json.group);
|
|
restoreBookmarkFromState(String(json.group.id));
|
|
showAsyncFlash('success', successMessage, 2000);
|
|
} else {
|
|
runReloadWithSuccess(successMessage);
|
|
}
|
|
} catch (error) {
|
|
reportError('FETCH_ERROR', groupMessageErrorGeneric, { error });
|
|
} finally {
|
|
setGroupSubmitPending(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getAppBase() {
|
|
const base = document.querySelector('base');
|
|
return base ? base.href : '/';
|
|
}
|
|
|
|
function currentRelativeUrl() {
|
|
const baseEl = document.querySelector('base');
|
|
const basePath = baseEl ? new URL(baseEl.href).pathname : '/';
|
|
const full = window.location.pathname + window.location.search;
|
|
if (full.startsWith(basePath)) {
|
|
return full.slice(basePath.length);
|
|
}
|
|
return full.replace(/^\//, '');
|
|
}
|