release(v2.3.5): make client portals ACL-aware and improve admin UX
This commit is contained in:
@@ -58,6 +58,27 @@ try {
|
||||
require_once $subPath;
|
||||
|
||||
$submittedBy = (string)($_SESSION['username'] ?? '');
|
||||
|
||||
// ─────────────────────────────
|
||||
// Better client IP detection
|
||||
// ─────────────────────────────
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
// Can be a comma-separated list; use the first non-empty
|
||||
$parts = explode(',', (string)$_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
foreach ($parts as $part) {
|
||||
$candidate = trim($part);
|
||||
if ($candidate !== '') {
|
||||
$ip = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
$ip = trim((string)$_SERVER['HTTP_X_REAL_IP']);
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = trim((string)$_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'slug' => $slug,
|
||||
'portalLabel' => $portal['label'] ?? '',
|
||||
@@ -69,7 +90,7 @@ try {
|
||||
'notes' => $notes,
|
||||
],
|
||||
'submittedBy' => $submittedBy,
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||
'ip' => $ip,
|
||||
'userAgent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'createdAt' => gmdate('c'),
|
||||
];
|
||||
|
||||
@@ -1968,7 +1968,7 @@ export function openUserPermissionsModal() {
|
||||
top: 0; left: 0; width: 100vw; height: 100vh;
|
||||
background-color: ${overlayBackground};
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
z-index: 3500;
|
||||
z-index: 10000;
|
||||
`;
|
||||
userPermissionsModal.innerHTML = `
|
||||
<div class="modal-content" style="${modalContentStyles}">
|
||||
|
||||
@@ -217,6 +217,9 @@ let __portalsCache = {};
|
||||
let __portalFolderListLoaded = false;
|
||||
let __portalFolderOptions = [];
|
||||
|
||||
// Remember a newly-created portal to focus its folder field
|
||||
let __portalSlugToFocus = null;
|
||||
|
||||
// Cache portal submissions per slug for CSV export
|
||||
const __portalSubmissionsCache = {};
|
||||
|
||||
@@ -279,13 +282,38 @@ export async function openClientPortalsModal() {
|
||||
(and optionally download) files without seeing your full FileRise UI.
|
||||
</p>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center" style="margin:8px 0 10px;">
|
||||
<button type="button" id="addPortalBtn" class="btn btn-sm btn-success">
|
||||
<i class="material-icons" style="font-size:16px;">cloud_upload</i>
|
||||
<span style="margin-left:4px;">Add portal</span>
|
||||
</button>
|
||||
<span id="clientPortalsStatus" class="small text-muted"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center" style="margin:8px 0 10px;">
|
||||
<div>
|
||||
<button type="button" id="addPortalBtn" class="btn btn-sm btn-success">
|
||||
<i class="material-icons" style="font-size:16px;">cloud_upload</i>
|
||||
<span style="margin-left:4px;">Add portal</span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
id="clientPortalsQuickAddUser"
|
||||
class="btn btn-sm btn-outline-primary ms-1">
|
||||
<i class="material-icons" style="font-size:16px; vertical-align:middle;">person_add</i>
|
||||
<span style="margin-left:4px;">Add user…</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="clientPortalsOpenUserPerms"
|
||||
class="btn btn-sm btn-outline-secondary ms-1">
|
||||
<i class="material-icons" style="font-size:16px; vertical-align:middle;">folder_shared</i>
|
||||
<span style="margin-left:4px;">Folder access…</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="clientPortalsOpenUserGroups"
|
||||
class="btn btn-sm btn-outline-secondary ms-1">
|
||||
<i class="material-icons" style="font-size:16px; vertical-align:middle;">groups</i>
|
||||
<span style="margin-left:4px;">User groups…</span>
|
||||
</button>
|
||||
</div>
|
||||
<span id="clientPortalsStatus" class="small text-muted"></span>
|
||||
</div>
|
||||
|
||||
<div id="clientPortalsBody" style="max-height:60vh; overflow:auto; margin-bottom:12px;">
|
||||
${t('loading')}…
|
||||
@@ -303,6 +331,41 @@ export async function openClientPortalsModal() {
|
||||
document.getElementById('cancelClientPortals').onclick = () => (modal.style.display = 'none');
|
||||
document.getElementById('saveClientPortals').onclick = saveClientPortalsFromUI;
|
||||
document.getElementById('addPortalBtn').onclick = addEmptyPortalRow;
|
||||
const quickAddUserBtn = document.getElementById('clientPortalsQuickAddUser');
|
||||
if (quickAddUserBtn) {
|
||||
quickAddUserBtn.onclick = () => {
|
||||
// Reuse existing admin add-user button / modal
|
||||
const globalBtn = document.getElementById('adminOpenAddUser');
|
||||
if (globalBtn) {
|
||||
globalBtn.click();
|
||||
} else {
|
||||
showToast('Use the Users tab to add a new user.');
|
||||
}
|
||||
};
|
||||
}
|
||||
const openPermsBtn = document.getElementById('clientPortalsOpenUserPerms');
|
||||
if (openPermsBtn) {
|
||||
openPermsBtn.onclick = () => {
|
||||
const btn = document.getElementById('adminOpenUserPermissions');
|
||||
if (btn) {
|
||||
btn.click();
|
||||
} else {
|
||||
showToast('Use the Users tab to edit folder access.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const openGroupsBtn = document.getElementById('clientPortalsOpenUserGroups');
|
||||
if (openGroupsBtn) {
|
||||
openGroupsBtn.onclick = () => {
|
||||
const btn = document.getElementById('adminOpenUserGroups');
|
||||
if (btn) {
|
||||
btn.click();
|
||||
} else {
|
||||
showToast('Use the Users tab to manage user groups.');
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
modal.style.background = overlayBg;
|
||||
const content = modal.querySelector('.modal-content');
|
||||
@@ -358,7 +421,22 @@ async function loadClientPortalsList(useCacheOnly) {
|
||||
const folder = p.folder || '';
|
||||
const clientEmail = p.clientEmail || '';
|
||||
const uploadOnly = !!p.uploadOnly;
|
||||
const allowDownload = p.allowDownload !== false; // default true
|
||||
|
||||
// Backwards compat:
|
||||
// - Old portals only had "uploadOnly":
|
||||
// uploadOnly = true => upload yes, download no
|
||||
// uploadOnly = false => upload yes, download yes
|
||||
// - New portals have explicit allowDownload.
|
||||
let allowDownload;
|
||||
if (Object.prototype.hasOwnProperty.call(p, 'allowDownload')) {
|
||||
allowDownload = p.allowDownload !== false;
|
||||
} else {
|
||||
// Legacy: "upload only" meant no download
|
||||
allowDownload = !uploadOnly;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const expiresAt = p.expiresAt ? String(p.expiresAt).slice(0, 10) : '';
|
||||
const brandColor = p.brandColor || '';
|
||||
const footerText = p.footerText || '';
|
||||
@@ -419,8 +497,8 @@ async function loadClientPortalsList(useCacheOnly) {
|
||||
|
||||
<div class="portal-card-body">
|
||||
<div class="portal-meta-row">
|
||||
<label style="font-weight:600;">
|
||||
Portal slug:
|
||||
<label style="font-weight:600;">
|
||||
Portal slug<span class="text-danger">*</span>:
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
data-portal-field="slug"
|
||||
@@ -439,8 +517,8 @@ async function loadClientPortalsList(useCacheOnly) {
|
||||
|
||||
<div class="portal-meta-row">
|
||||
<div class="portal-folder-row">
|
||||
<label>
|
||||
Folder:
|
||||
<label>
|
||||
Folder<span class="text-danger">*</span>:
|
||||
<input type="text"
|
||||
class="form-control form-control-sm portal-folder-input"
|
||||
data-portal-field="folder"
|
||||
@@ -482,11 +560,11 @@ async function loadClientPortalsList(useCacheOnly) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label style="display:flex; align-items:center; gap:4px;">
|
||||
<label style="display:flex; align-items:center; gap:4px;">
|
||||
<input type="checkbox"
|
||||
data-portal-field="uploadOnly"
|
||||
${uploadOnly ? 'checked' : ''} />
|
||||
<span>Upload only</span>
|
||||
<span>Allow upload</span>
|
||||
</label>
|
||||
|
||||
<label style="display:flex; align-items:center; gap:4px;">
|
||||
@@ -495,7 +573,6 @@ async function loadClientPortalsList(useCacheOnly) {
|
||||
${allowDownload ? 'checked' : ''} />
|
||||
<span>Allow download</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:8px;">
|
||||
<div class="form-group" style="margin-bottom:6px;">
|
||||
@@ -840,6 +917,32 @@ body.querySelectorAll('[data-portal-action="delete"]').forEach(btn => {
|
||||
card.remove();
|
||||
});
|
||||
});
|
||||
// After rendering, if we have a "new" portal to focus, expand it and focus Folder
|
||||
if (__portalSlugToFocus) {
|
||||
const focusSlug = __portalSlugToFocus;
|
||||
__portalSlugToFocus = null;
|
||||
|
||||
const focusCard = body.querySelector(`.portal-card[data-portal-slug="${focusSlug}"]`);
|
||||
if (focusCard) {
|
||||
const header = focusCard.querySelector('.portal-card-header');
|
||||
const bodyEl = focusCard.querySelector('.portal-card-body');
|
||||
const caret = focusCard.querySelector('.portal-card-caret');
|
||||
|
||||
if (header && bodyEl) {
|
||||
header.setAttribute('aria-expanded', 'true');
|
||||
bodyEl.style.display = 'block';
|
||||
if (caret) caret.textContent = '▾';
|
||||
}
|
||||
|
||||
const folderInput = focusCard.querySelector('[data-portal-field="folder"]');
|
||||
if (folderInput) {
|
||||
folderInput.focus();
|
||||
folderInput.select();
|
||||
}
|
||||
|
||||
focusCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
// Keep submissions viewer working
|
||||
attachPortalSubmissionsUI();
|
||||
// Intake presets dropdowns
|
||||
@@ -881,6 +984,8 @@ function addEmptyPortalRow() {
|
||||
expiresAt: ''
|
||||
};
|
||||
|
||||
// After re-render, auto-focus this portal's folder field
|
||||
__portalSlugToFocus = slug;
|
||||
loadClientPortalsList(true);
|
||||
}
|
||||
|
||||
@@ -1421,6 +1526,48 @@ async function saveClientPortalsFromUI() {
|
||||
|
||||
const cards = body.querySelectorAll('.card[data-portal-slug]');
|
||||
const portals = {};
|
||||
const invalid = [];
|
||||
let firstInvalidField = null;
|
||||
|
||||
// Clear previous visual errors
|
||||
cards.forEach(card => {
|
||||
card.style.boxShadow = '';
|
||||
card.style.borderColor = '';
|
||||
card.classList.remove('portal-card-has-error');
|
||||
|
||||
const hint = card.querySelector('.portal-card-error-hint');
|
||||
if (hint) hint.remove();
|
||||
});
|
||||
|
||||
const markCardMissingRequired = (card, message) => {
|
||||
// Mark visually
|
||||
card.classList.add('portal-card-has-error');
|
||||
card.style.borderColor = '#dc3545';
|
||||
card.style.boxShadow = '0 0 0 2px rgba(220,53,69,0.6)';
|
||||
|
||||
// Expand the card so the error is visible even if it was collapsed
|
||||
const header = card.querySelector('.portal-card-header');
|
||||
const bodyEl = card.querySelector('.portal-card-body') || card;
|
||||
const caret = card.querySelector('.portal-card-caret');
|
||||
|
||||
if (header && bodyEl) {
|
||||
header.setAttribute('aria-expanded', 'true');
|
||||
bodyEl.style.display = 'block';
|
||||
if (caret) caret.textContent = '▾';
|
||||
}
|
||||
|
||||
// Small inline hint at top of the card body
|
||||
let hint = bodyEl.querySelector('.portal-card-error-hint');
|
||||
if (!hint) {
|
||||
hint = document.createElement('div');
|
||||
hint.className = 'portal-card-error-hint text-danger small';
|
||||
hint.style.marginBottom = '6px';
|
||||
hint.textContent = message || 'Slug and folder are required. This portal will not be saved until both are filled.';
|
||||
bodyEl.insertBefore(hint, bodyEl.firstChild);
|
||||
} else {
|
||||
hint.textContent = message || hint.textContent;
|
||||
}
|
||||
};
|
||||
|
||||
cards.forEach(card => {
|
||||
const origSlug = card.getAttribute('data-portal-slug') || '';
|
||||
@@ -1453,21 +1600,22 @@ async function saveClientPortalsFromUI() {
|
||||
const lblRef = getVal('[data-portal-field="lblRef"]').trim();
|
||||
const lblNotes = getVal('[data-portal-field="lblNotes"]').trim();
|
||||
|
||||
const uploadOnlyEl = card.querySelector('[data-portal-field="uploadOnly"]');
|
||||
const uploadOnlyEl = card.querySelector('[data-portal-field="uploadOnly"]');
|
||||
const allowDownloadEl = card.querySelector('[data-portal-field="allowDownload"]');
|
||||
const requireFormEl = card.querySelector('[data-portal-field="requireForm"]');
|
||||
const requireFormEl = card.querySelector('[data-portal-field="requireForm"]');
|
||||
|
||||
const uploadOnly = uploadOnlyEl ? !!uploadOnlyEl.checked : true;
|
||||
const uploadOnly = uploadOnlyEl ? !!uploadOnlyEl.checked : true;
|
||||
const allowDownload = allowDownloadEl ? !!allowDownloadEl.checked : false;
|
||||
const requireForm = requireFormEl ? !!requireFormEl.checked : false;
|
||||
const reqNameEl = card.querySelector('[data-portal-field="reqName"]');
|
||||
const requireForm = requireFormEl ? !!requireFormEl.checked : false;
|
||||
|
||||
const reqNameEl = card.querySelector('[data-portal-field="reqName"]');
|
||||
const reqEmailEl = card.querySelector('[data-portal-field="reqEmail"]');
|
||||
const reqRefEl = card.querySelector('[data-portal-field="reqRef"]');
|
||||
const reqRefEl = card.querySelector('[data-portal-field="reqRef"]');
|
||||
const reqNotesEl = card.querySelector('[data-portal-field="reqNotes"]');
|
||||
|
||||
const reqName = reqNameEl ? !!reqNameEl.checked : false;
|
||||
const reqName = reqNameEl ? !!reqNameEl.checked : false;
|
||||
const reqEmail = reqEmailEl ? !!reqEmailEl.checked : false;
|
||||
const reqRef = reqRefEl ? !!reqRefEl.checked : false;
|
||||
const reqRef = reqRefEl ? !!reqRefEl.checked : false;
|
||||
const reqNotes = reqNotesEl ? !!reqNotesEl.checked : false;
|
||||
|
||||
const visNameEl = card.querySelector('[data-portal-field="visName"]');
|
||||
@@ -1487,63 +1635,106 @@ async function saveClientPortalsFromUI() {
|
||||
|
||||
const showThankYouEl = card.querySelector('[data-portal-field="showThankYou"]');
|
||||
const showThankYou = showThankYouEl ? !!showThankYouEl.checked : false;
|
||||
|
||||
const folderInput = card.querySelector('[data-portal-field="folder"]');
|
||||
const slugInput = card.querySelector('[data-portal-field="slug"]');
|
||||
if (slugInput) {
|
||||
const rawSlug = slugInput.value.trim();
|
||||
if (rawSlug) slug = rawSlug;
|
||||
}
|
||||
|
||||
const labelForError = label || slug || origSlug || '(unnamed portal)';
|
||||
|
||||
// Validation: slug + folder required
|
||||
if (!slug || !folder) {
|
||||
invalid.push(labelForError);
|
||||
|
||||
// Remember the first problematic field so we can scroll exactly to it
|
||||
if (!firstInvalidField) {
|
||||
if (!folder && folderInput) {
|
||||
firstInvalidField = folderInput;
|
||||
} else if (!slug && slugInput) {
|
||||
firstInvalidField = slugInput;
|
||||
} else {
|
||||
firstInvalidField = card;
|
||||
}
|
||||
}
|
||||
|
||||
markCardMissingRequired(
|
||||
card,
|
||||
'Slug and folder are required. This portal will not be saved until both are filled.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
portals[slug] = {
|
||||
label,
|
||||
folder,
|
||||
clientEmail,
|
||||
uploadOnly,
|
||||
allowDownload,
|
||||
expiresAt,
|
||||
title,
|
||||
introText,
|
||||
requireForm,
|
||||
brandColor,
|
||||
footerText,
|
||||
logoFile,
|
||||
logoUrl,
|
||||
formDefaults: {
|
||||
name: defName,
|
||||
email: defEmail,
|
||||
reference: defRef,
|
||||
notes: defNotes
|
||||
},
|
||||
formRequired: {
|
||||
name: reqName,
|
||||
email: reqEmail,
|
||||
reference: reqRef,
|
||||
notes: reqNotes
|
||||
},
|
||||
formLabels: {
|
||||
name: lblName,
|
||||
email: lblEmail,
|
||||
reference: lblRef,
|
||||
notes: lblNotes
|
||||
},
|
||||
formVisible: {
|
||||
name: visName,
|
||||
email: visEmail,
|
||||
reference: visRef,
|
||||
notes: visNotes
|
||||
},
|
||||
uploadMaxSizeMb: uploadMaxSizeMb ? parseInt(uploadMaxSizeMb, 10) || 0 : 0,
|
||||
uploadExtWhitelist,
|
||||
uploadMaxPerDay: uploadMaxPerDay ? parseInt(uploadMaxPerDay, 10) || 0 : 0,
|
||||
showThankYou,
|
||||
thankYouText,
|
||||
};
|
||||
label,
|
||||
folder,
|
||||
clientEmail,
|
||||
uploadOnly,
|
||||
allowDownload,
|
||||
expiresAt,
|
||||
title,
|
||||
introText,
|
||||
requireForm,
|
||||
brandColor,
|
||||
footerText,
|
||||
logoFile,
|
||||
logoUrl,
|
||||
formDefaults: {
|
||||
name: defName,
|
||||
email: defEmail,
|
||||
reference: defRef,
|
||||
notes: defNotes
|
||||
},
|
||||
formRequired: {
|
||||
name: reqName,
|
||||
email: reqEmail,
|
||||
reference: reqRef,
|
||||
notes: reqNotes
|
||||
},
|
||||
formLabels: {
|
||||
name: lblName,
|
||||
email: lblEmail,
|
||||
reference: lblRef,
|
||||
notes: lblNotes
|
||||
},
|
||||
formVisible: {
|
||||
name: visName,
|
||||
email: visEmail,
|
||||
reference: visRef,
|
||||
notes: visNotes
|
||||
},
|
||||
uploadMaxSizeMb: uploadMaxSizeMb ? parseInt(uploadMaxSizeMb, 10) || 0 : 0,
|
||||
uploadExtWhitelist,
|
||||
uploadMaxPerDay: uploadMaxPerDay ? parseInt(uploadMaxPerDay, 10) || 0 : 0,
|
||||
showThankYou,
|
||||
thankYouText,
|
||||
};
|
||||
});
|
||||
|
||||
if (invalid.length) {
|
||||
if (status) {
|
||||
status.textContent = 'Please fill slug and folder for highlighted portals.';
|
||||
status.className = 'small text-danger';
|
||||
}
|
||||
|
||||
// Scroll the *first missing field* into view so the admin sees exactly where to fix
|
||||
const targetEl = firstInvalidField || body.querySelector('.portal-card-has-error');
|
||||
if (targetEl) {
|
||||
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// If it's an input, focus + select to make typing instant
|
||||
if (typeof targetEl.focus === 'function') {
|
||||
targetEl.focus();
|
||||
if (typeof targetEl.select === 'function') {
|
||||
targetEl.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showToast('Please set slug and folder for: ' + invalid.join(', '));
|
||||
return; // Don’t hit the API if local validation failed
|
||||
}
|
||||
|
||||
if (status) {
|
||||
status.textContent = 'Saving…';
|
||||
status.className = 'small text-muted';
|
||||
|
||||
@@ -10,10 +10,33 @@ function portalFolder() {
|
||||
return portal.folder || portal.targetFolder || portal.path || 'root';
|
||||
}
|
||||
|
||||
function portalCanUpload() {
|
||||
if (!portal) return false;
|
||||
|
||||
// Prefer explicit flags from backend (PortalController)
|
||||
if (typeof portal.canUpload !== 'undefined') {
|
||||
return !!portal.canUpload;
|
||||
}
|
||||
|
||||
// Fallbacks for older bundles (if you ever add these)
|
||||
if (typeof portal.allowUpload !== 'undefined') {
|
||||
return !!portal.allowUpload;
|
||||
}
|
||||
|
||||
// Legacy behavior: portals were always upload-capable;
|
||||
// uploadOnly only controlled download visibility.
|
||||
return true;
|
||||
}
|
||||
|
||||
function portalCanDownload() {
|
||||
if (!portal) return false;
|
||||
|
||||
// Prefer explicit flags if present
|
||||
// Prefer explicit flag if present (PortalController)
|
||||
if (typeof portal.canDownload !== 'undefined') {
|
||||
return !!portal.canDownload;
|
||||
}
|
||||
|
||||
// Fallback to allowDownload / allowDownloads (older payloads)
|
||||
if (typeof portal.allowDownload !== 'undefined') {
|
||||
return !!portal.allowDownload;
|
||||
}
|
||||
@@ -21,7 +44,7 @@ function portalCanDownload() {
|
||||
return !!portal.allowDownloads;
|
||||
}
|
||||
|
||||
// Fallback: uploadOnly = true => no downloads
|
||||
// Legacy: uploadOnly = true => no downloads
|
||||
if (typeof portal.uploadOnly !== 'undefined') {
|
||||
return !portal.uploadOnly;
|
||||
}
|
||||
@@ -260,7 +283,7 @@ function setupPortalForm(slug) {
|
||||
const formSection = qs('portalFormSection');
|
||||
const uploadSection = qs('portalUploadSection');
|
||||
|
||||
if (!portal || !portal.requireForm) {
|
||||
if (!portal || !portal.requireForm || !portalCanUpload()) {
|
||||
if (formSection) formSection.style.display = 'none';
|
||||
if (uploadSection) uploadSection.style.opacity = '1';
|
||||
return;
|
||||
@@ -549,11 +572,21 @@ function renderPortalInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
const uploadsEnabled = portalCanUpload();
|
||||
const downloadsEnabled = portalCanDownload();
|
||||
|
||||
if (subtitleEl) {
|
||||
const parts = [];
|
||||
if (portal.uploadOnly) parts.push('upload only');
|
||||
if (portalCanDownload()) parts.push('download allowed');
|
||||
subtitleEl.textContent = parts.length ? parts.join(' • ') : '';
|
||||
let text = '';
|
||||
if (uploadsEnabled && downloadsEnabled) {
|
||||
text = 'Upload & download';
|
||||
} else if (uploadsEnabled && !downloadsEnabled) {
|
||||
text = 'Upload only';
|
||||
} else if (!uploadsEnabled && downloadsEnabled) {
|
||||
text = 'Download only';
|
||||
} else {
|
||||
text = 'Access only';
|
||||
}
|
||||
subtitleEl.textContent = text;
|
||||
}
|
||||
|
||||
if (footerEl) {
|
||||
@@ -561,6 +594,26 @@ function renderPortalInfo() {
|
||||
? portal.footerText.trim()
|
||||
: '';
|
||||
}
|
||||
|
||||
const formSection = qs('portalFormSection');
|
||||
const uploadSection = qs('portalUploadSection');
|
||||
|
||||
// If uploads are disabled, hide upload + form (form is only meaningful for uploads)
|
||||
if (!uploadsEnabled) {
|
||||
if (formSection) {
|
||||
formSection.style.display = 'none';
|
||||
}
|
||||
if (uploadSection) {
|
||||
uploadSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const statusEl = qs('portalStatus');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'Uploads are disabled for this portal.';
|
||||
statusEl.classList.remove('text-muted');
|
||||
statusEl.classList.add('text-warning');
|
||||
}
|
||||
}
|
||||
applyPortalFormLabels();
|
||||
const color = portal.brandColor && portal.brandColor.trim();
|
||||
if (color) {
|
||||
@@ -741,6 +794,13 @@ async function loadPortalFiles() {
|
||||
// ----------------- Upload -----------------
|
||||
async function uploadFiles(fileList) {
|
||||
if (!portal || !fileList || !fileList.length) return;
|
||||
|
||||
if (!portalCanUpload()) {
|
||||
showToast('Uploads are disabled for this portal.');
|
||||
setStatus('Uploads are disabled for this portal.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (portal.requireForm && !portalFormDone) {
|
||||
showToast('Please fill in your details before uploading.');
|
||||
return;
|
||||
@@ -900,11 +960,23 @@ async function uploadFiles(fileList) {
|
||||
|
||||
// ----------------- Upload UI wiring -----------------
|
||||
function wireUploadUI() {
|
||||
const drop = qs('portalDropzone');
|
||||
const input = qs('portalFileInput');
|
||||
const drop = qs('portalDropzone');
|
||||
const input = qs('portalFileInput');
|
||||
const refreshBtn = qs('portalRefreshBtn');
|
||||
|
||||
if (drop && input) {
|
||||
const uploadsEnabled = portalCanUpload();
|
||||
const downloadsEnabled = portalCanDownload();
|
||||
|
||||
// Upload UI
|
||||
if (drop) {
|
||||
if (!uploadsEnabled) {
|
||||
// Visually dim + disable clicks
|
||||
drop.classList.add('portal-dropzone-disabled');
|
||||
drop.style.cursor = 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadsEnabled && drop && input) {
|
||||
drop.addEventListener('click', () => input.click());
|
||||
|
||||
input.addEventListener('change', (e) => {
|
||||
@@ -938,10 +1010,15 @@ function wireUploadUI() {
|
||||
});
|
||||
}
|
||||
|
||||
// Download / refresh
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
loadPortalFiles();
|
||||
});
|
||||
if (!downloadsEnabled) {
|
||||
refreshBtn.style.display = 'none';
|
||||
} else {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
loadPortalFiles();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,11 @@
|
||||
.portal-required-star {
|
||||
color: #dc3545;
|
||||
}
|
||||
.portal-dropzone.portal-dropzone-disabled {
|
||||
opacity: 0.5;
|
||||
border-style: solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
Reference in New Issue
Block a user