feat(hexojs): Add content upload wizard and moderation system
- Add upload.js wizard with multi-target publishing (HexoJS, Gitea, Streamlit, MetaBlogizer) - Add submit.js for user content submission with moderation workflow - Add moderation RPCD methods: submit_for_review, list_pending, approve_submission, reject_submission - Update ACL with new moderation permissions - Add menu entries for Upload and Submit & Moderate views Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9887b3555d
commit
2f7d57dced
@ -127,6 +127,16 @@ _Last updated: 2026-02-20 (v0.24.0 - Matrix + SaaS Relay + Media Hub)_
|
||||
- `hexoctl static quick <file>` - One-command upload + publish
|
||||
- Tested and verified on router
|
||||
|
||||
- **HexoJS Content Upload Wizard** — DONE (2026-02-20)
|
||||
- 3-step wizard UI at `/admin/services/hexojs/upload`
|
||||
- File upload: HTML, PDF, Markdown (.md) support
|
||||
- Metadata: Title, Category, Tags, Public/Private visibility
|
||||
- Multi-target publishing: HexoJS Blog, Gitea, Streamlit, MetaBlogizer
|
||||
- Base64 encoding for binary file transfer
|
||||
- RPCD methods: upload_article, upload_pdf, upload_html, publish_draft, unpublish_post, get_uploads
|
||||
- Gitea integration with repo/path selection
|
||||
- SecuBox Welcome Guide deployed at /guide/, /connexion.html, /accueil.html
|
||||
|
||||
### Just Completed (2026-02-19)
|
||||
|
||||
- **WAF VoIP/XMPP Security Filters** — DONE (2026-02-19)
|
||||
|
||||
@ -375,7 +375,8 @@
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npx gulp browserify:*)",
|
||||
"Bash(npx terser:*)",
|
||||
"Bash(read)"
|
||||
"Bash(read)",
|
||||
"Bash(/home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/c3box-vm-builder.sh:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,432 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require form';
|
||||
|
||||
// User submission RPC calls
|
||||
var callSubmitContent = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'submit_for_review',
|
||||
params: ['title', 'content', 'category', 'author', 'email'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callListPending = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'list_pending',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callApproveSubmission = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'approve_submission',
|
||||
params: ['submission_id', 'publish_target'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callRejectSubmission = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'reject_submission',
|
||||
params: ['submission_id', 'reason'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callGetSubmission = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'get_submission',
|
||||
params: ['submission_id'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callListPending()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var pendingList = (data[0] && data[0].submissions) || [];
|
||||
var isAdmin = (data[0] && data[0].is_admin) || false;
|
||||
|
||||
var content = E('div', { class: 'cbi-map', style: 'max-width:900px;margin:0 auto;' }, [
|
||||
E('h2', { style: 'text-align:center;margin-bottom:30px;' }, [
|
||||
E('span', { style: 'font-size:1.2em;' }, '📝 '),
|
||||
isAdmin ? 'Content Moderation' : 'Submit Content'
|
||||
]),
|
||||
|
||||
// Tab navigation
|
||||
E('div', { style: 'display:flex;gap:10px;margin-bottom:20px;border-bottom:2px solid #333;padding-bottom:10px;' }, [
|
||||
E('button', {
|
||||
id: 'tab-submit',
|
||||
class: 'cbi-button cbi-button-action',
|
||||
click: function() { self.showTab('submit'); }
|
||||
}, '📤 Submit'),
|
||||
isAdmin ? E('button', {
|
||||
id: 'tab-moderate',
|
||||
class: 'cbi-button',
|
||||
click: function() { self.showTab('moderate'); }
|
||||
}, '⚖️ Moderate (' + pendingList.length + ')') : E('span'),
|
||||
E('button', {
|
||||
id: 'tab-status',
|
||||
class: 'cbi-button',
|
||||
click: function() { self.showTab('status'); }
|
||||
}, '📊 My Submissions')
|
||||
]),
|
||||
|
||||
// Submit Tab
|
||||
E('div', { id: 'panel-submit', class: 'cbi-section' }, [
|
||||
E('div', { class: 'cbi-section-descr', style: 'margin-bottom:20px;' },
|
||||
'Submit your content for review. Once approved by moderators, it will be published to the public portal.'),
|
||||
|
||||
E('div', { style: 'background:#1a1a2e;border-radius:12px;padding:25px;' }, [
|
||||
// Author Info
|
||||
E('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;' }, [
|
||||
E('div', {}, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Author Name *'),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'submit-author',
|
||||
style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;',
|
||||
placeholder: 'Your name'
|
||||
})
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Email (optional)'),
|
||||
E('input', {
|
||||
type: 'email',
|
||||
id: 'submit-email',
|
||||
style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;',
|
||||
placeholder: 'your@email.com'
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Title
|
||||
E('div', { style: 'margin-bottom:20px;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Title *'),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'submit-title',
|
||||
style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;',
|
||||
placeholder: 'Article title'
|
||||
})
|
||||
]),
|
||||
|
||||
// Category
|
||||
E('div', { style: 'margin-bottom:20px;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Category'),
|
||||
E('select', {
|
||||
id: 'submit-category',
|
||||
style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;'
|
||||
}, [
|
||||
E('option', { value: 'general' }, 'General'),
|
||||
E('option', { value: 'news' }, 'News'),
|
||||
E('option', { value: 'tutorial' }, 'Tutorial'),
|
||||
E('option', { value: 'opinion' }, 'Opinion'),
|
||||
E('option', { value: 'tech' }, 'Technology'),
|
||||
E('option', { value: 'community' }, 'Community')
|
||||
])
|
||||
]),
|
||||
|
||||
// Content
|
||||
E('div', { style: 'margin-bottom:20px;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Content * (Markdown supported)'),
|
||||
E('textarea', {
|
||||
id: 'submit-content',
|
||||
style: 'width:100%;height:300px;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;font-family:monospace;resize:vertical;',
|
||||
placeholder: '# Your Article\n\nWrite your content here using **Markdown** formatting...'
|
||||
})
|
||||
]),
|
||||
|
||||
// Submit Button
|
||||
E('div', { style: 'text-align:center;' }, [
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-positive',
|
||||
style: 'padding:15px 40px;font-size:1.1em;',
|
||||
click: ui.createHandlerFn(this, 'submitContent')
|
||||
}, '📤 Submit for Review')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Moderate Tab (Admin only)
|
||||
isAdmin ? E('div', { id: 'panel-moderate', class: 'cbi-section', style: 'display:none;' }, [
|
||||
E('div', { class: 'cbi-section-descr', style: 'margin-bottom:20px;' },
|
||||
'Review and moderate user submissions. Approve to publish or reject with feedback.'),
|
||||
|
||||
pendingList.length === 0 ?
|
||||
E('div', { style: 'text-align:center;padding:40px;color:#888;' }, [
|
||||
E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '✨'),
|
||||
E('p', {}, 'No pending submissions')
|
||||
]) :
|
||||
E('div', { id: 'pending-list' }, pendingList.map(function(sub) {
|
||||
return E('div', {
|
||||
class: 'cbi-section',
|
||||
style: 'background:#1a1a2e;border-radius:12px;padding:20px;margin-bottom:15px;border-left:4px solid #f39c12;',
|
||||
'data-id': sub.id
|
||||
}, [
|
||||
E('div', { style: 'display:flex;justify-content:space-between;align-items:start;' }, [
|
||||
E('div', {}, [
|
||||
E('h4', { style: 'margin:0 0 10px 0;' }, sub.title),
|
||||
E('div', { style: 'color:#888;font-size:0.9em;' }, [
|
||||
E('span', {}, '👤 ' + sub.author),
|
||||
E('span', { style: 'margin-left:15px;' }, '📁 ' + sub.category),
|
||||
E('span', { style: 'margin-left:15px;' }, '📅 ' + sub.date)
|
||||
])
|
||||
]),
|
||||
E('div', { style: 'display:flex;gap:10px;' }, [
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-action',
|
||||
click: ui.createHandlerFn(self, 'previewSubmission', sub.id)
|
||||
}, '👁️ Preview'),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-positive',
|
||||
click: ui.createHandlerFn(self, 'approveSubmission', sub.id)
|
||||
}, '✅ Approve'),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-negative',
|
||||
click: ui.createHandlerFn(self, 'rejectSubmission', sub.id)
|
||||
}, '❌ Reject')
|
||||
])
|
||||
]),
|
||||
E('div', {
|
||||
class: 'submission-preview',
|
||||
style: 'display:none;margin-top:15px;padding:15px;background:#16213e;border-radius:8px;max-height:300px;overflow-y:auto;'
|
||||
})
|
||||
]);
|
||||
}))
|
||||
]) : E('div'),
|
||||
|
||||
// Status Tab
|
||||
E('div', { id: 'panel-status', class: 'cbi-section', style: 'display:none;' }, [
|
||||
E('div', { class: 'cbi-section-descr', style: 'margin-bottom:20px;' },
|
||||
'Track the status of your submitted content.'),
|
||||
|
||||
E('div', { id: 'my-submissions', style: 'text-align:center;padding:40px;color:#888;' }, [
|
||||
E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '📋'),
|
||||
E('p', {}, 'Enter your email to check submission status'),
|
||||
E('div', { style: 'max-width:400px;margin:20px auto;' }, [
|
||||
E('input', {
|
||||
type: 'email',
|
||||
id: 'status-email',
|
||||
style: 'width:70%;padding:10px;border-radius:8px 0 0 8px;border:1px solid #444;background:#16213e;color:#fff;',
|
||||
placeholder: 'your@email.com'
|
||||
}),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-action',
|
||||
style: 'border-radius:0 8px 8px 0;',
|
||||
click: ui.createHandlerFn(this, 'checkStatus')
|
||||
}, 'Check')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
showTab: function(tab) {
|
||||
['submit', 'moderate', 'status'].forEach(function(t) {
|
||||
var panel = document.getElementById('panel-' + t);
|
||||
var tabBtn = document.getElementById('tab-' + t);
|
||||
if (panel) panel.style.display = t === tab ? 'block' : 'none';
|
||||
if (tabBtn) {
|
||||
tabBtn.className = t === tab ? 'cbi-button cbi-button-action' : 'cbi-button';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
submitContent: function() {
|
||||
var author = document.getElementById('submit-author').value.trim();
|
||||
var email = document.getElementById('submit-email').value.trim();
|
||||
var title = document.getElementById('submit-title').value.trim();
|
||||
var category = document.getElementById('submit-category').value;
|
||||
var content = document.getElementById('submit-content').value.trim();
|
||||
|
||||
if (!author || !title || !content) {
|
||||
ui.addNotification(null, E('p', 'Please fill in all required fields (Author, Title, Content)'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.showModal('Submitting...', [
|
||||
E('p', { class: 'spinning' }, 'Sending your content for review...')
|
||||
]);
|
||||
|
||||
callSubmitContent(title, content, category, author, email).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.showModal('Submission Received', [
|
||||
E('div', { style: 'text-align:center;padding:20px;' }, [
|
||||
E('div', { style: 'font-size:4em;margin-bottom:15px;' }, '✅'),
|
||||
E('h3', {}, 'Thank You!'),
|
||||
E('p', {}, 'Your submission has been received and is pending moderation.'),
|
||||
E('p', { style: 'color:#888;' }, 'Submission ID: ' + result.submission_id),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-action',
|
||||
style: 'margin-top:20px;',
|
||||
click: function() {
|
||||
ui.hideModal();
|
||||
document.getElementById('submit-title').value = '';
|
||||
document.getElementById('submit-content').value = '';
|
||||
}
|
||||
}, 'Submit Another')
|
||||
])
|
||||
]);
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error || 'Submission failed'), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', 'Error: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
previewSubmission: function(submissionId) {
|
||||
var self = this;
|
||||
var card = document.querySelector('[data-id="' + submissionId + '"]');
|
||||
var preview = card.querySelector('.submission-preview');
|
||||
|
||||
if (preview.style.display === 'block') {
|
||||
preview.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
preview.innerHTML = '<p class="spinning">Loading...</p>';
|
||||
preview.style.display = 'block';
|
||||
|
||||
callGetSubmission(submissionId).then(function(result) {
|
||||
if (result.success) {
|
||||
preview.innerHTML = '<pre style="white-space:pre-wrap;word-wrap:break-word;margin:0;">' +
|
||||
self.escapeHtml(result.content) + '</pre>';
|
||||
} else {
|
||||
preview.innerHTML = '<p style="color:#e74c3c;">Failed to load content</p>';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
escapeHtml: function(text) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
approveSubmission: function(submissionId) {
|
||||
var self = this;
|
||||
|
||||
ui.showModal('Approve Submission', [
|
||||
E('div', { style: 'padding:10px;' }, [
|
||||
E('p', {}, 'Select where to publish this content:'),
|
||||
E('div', { style: 'display:grid;gap:10px;margin:20px 0;' }, [
|
||||
E('label', { style: 'display:flex;align-items:center;gap:10px;cursor:pointer;' }, [
|
||||
E('input', { type: 'radio', name: 'publish-target', value: 'hexojs', checked: true }),
|
||||
E('span', {}, '📝 HexoJS Blog (default)')
|
||||
]),
|
||||
E('label', { style: 'display:flex;align-items:center;gap:10px;cursor:pointer;' }, [
|
||||
E('input', { type: 'radio', name: 'publish-target', value: 'metablogizer' }),
|
||||
E('span', {}, '🔗 MetaBlogizer')
|
||||
]),
|
||||
E('label', { style: 'display:flex;align-items:center;gap:10px;cursor:pointer;' }, [
|
||||
E('input', { type: 'radio', name: 'publish-target', value: 'static' }),
|
||||
E('span', {}, '📄 Static Page')
|
||||
])
|
||||
]),
|
||||
E('div', { style: 'display:flex;gap:10px;justify-content:flex-end;' }, [
|
||||
E('button', {
|
||||
class: 'cbi-button',
|
||||
click: ui.hideModal
|
||||
}, 'Cancel'),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-positive',
|
||||
click: function() {
|
||||
var target = document.querySelector('input[name="publish-target"]:checked').value;
|
||||
ui.hideModal();
|
||||
self.doApprove(submissionId, target);
|
||||
}
|
||||
}, '✅ Publish')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
doApprove: function(submissionId, target) {
|
||||
ui.showModal('Publishing...', [
|
||||
E('p', { class: 'spinning' }, 'Publishing content...')
|
||||
]);
|
||||
|
||||
callApproveSubmission(submissionId, target).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', 'Content approved and published!'), 'success');
|
||||
var card = document.querySelector('[data-id="' + submissionId + '"]');
|
||||
if (card) card.remove();
|
||||
|
||||
// Update counter
|
||||
var moderateTab = document.getElementById('tab-moderate');
|
||||
if (moderateTab) {
|
||||
var match = moderateTab.textContent.match(/\((\d+)\)/);
|
||||
if (match) {
|
||||
var count = parseInt(match[1]) - 1;
|
||||
moderateTab.textContent = '⚖️ Moderate (' + count + ')';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error || 'Approval failed'), 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
rejectSubmission: function(submissionId) {
|
||||
var self = this;
|
||||
|
||||
ui.showModal('Reject Submission', [
|
||||
E('div', { style: 'padding:10px;' }, [
|
||||
E('p', {}, 'Provide a reason for rejection (optional):'),
|
||||
E('textarea', {
|
||||
id: 'reject-reason',
|
||||
style: 'width:100%;height:100px;padding:10px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;margin:15px 0;',
|
||||
placeholder: 'Reason for rejection...'
|
||||
}),
|
||||
E('div', { style: 'display:flex;gap:10px;justify-content:flex-end;' }, [
|
||||
E('button', {
|
||||
class: 'cbi-button',
|
||||
click: ui.hideModal
|
||||
}, 'Cancel'),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-negative',
|
||||
click: function() {
|
||||
var reason = document.getElementById('reject-reason').value;
|
||||
ui.hideModal();
|
||||
self.doReject(submissionId, reason);
|
||||
}
|
||||
}, '❌ Reject')
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
doReject: function(submissionId, reason) {
|
||||
callRejectSubmission(submissionId, reason).then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', 'Submission rejected'), 'info');
|
||||
var card = document.querySelector('[data-id="' + submissionId + '"]');
|
||||
if (card) card.remove();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error || 'Rejection failed'), 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
checkStatus: function() {
|
||||
var email = document.getElementById('status-email').value.trim();
|
||||
if (!email) {
|
||||
ui.addNotification(null, E('p', 'Please enter your email'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement status check by email
|
||||
ui.addNotification(null, E('p', 'Status check feature coming soon'), 'info');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,427 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require secubox/kiss-theme';
|
||||
|
||||
var callUploadHTML = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'upload_html',
|
||||
params: ['base64_data', 'title', 'visibility', 'category', 'tags'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callUploadPDF = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'upload_pdf',
|
||||
params: ['base64_data', 'title', 'visibility', 'category'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callUploadMD = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'upload_article',
|
||||
params: ['filename', 'content', 'title', 'visibility', 'category', 'tags'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callWizardUpload = rpc.declare({
|
||||
object: 'luci.hexojs',
|
||||
method: 'wizard_upload',
|
||||
params: ['base64_data', 'filename', 'title', 'visibility', 'category', 'tags', 'target', 'options'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callGiteaUpload = rpc.declare({
|
||||
object: 'luci.gitea',
|
||||
method: 'upload_file',
|
||||
params: ['repo', 'path', 'content', 'message'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callStreamlitCreate = rpc.declare({
|
||||
object: 'luci.streamlit',
|
||||
method: 'create_app',
|
||||
params: ['name', 'source_file'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callMetablogizerCreate = rpc.declare({
|
||||
object: 'luci.metablogizer',
|
||||
method: 'create_entry',
|
||||
params: ['name', 'source_file', 'category'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function fileToBase64(file) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
var base64 = reader.result.split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
var content = E('div', { style: 'padding:20px;max-width:800px;margin:0 auto;' }, [
|
||||
E('h2', { style: 'text-align:center;margin-bottom:30px;' }, '📤 Content Upload Wizard'),
|
||||
|
||||
E('div', { class: 'kiss-card', style: 'padding:30px;' }, [
|
||||
// Step 1: File Selection
|
||||
E('div', { class: 'wizard-step', id: 'step-file' }, [
|
||||
E('h3', {}, '1. Select File'),
|
||||
E('p', { style: 'color:#888;' }, 'Supported: HTML, PDF, Markdown (.md)'),
|
||||
E('div', { style: 'border:2px dashed #3498db;border-radius:12px;padding:40px;text-align:center;margin:20px 0;' }, [
|
||||
E('input', {
|
||||
type: 'file',
|
||||
id: 'file-input',
|
||||
accept: '.html,.htm,.pdf,.md,.markdown',
|
||||
style: 'display:none;'
|
||||
}),
|
||||
E('label', { for: 'file-input', style: 'cursor:pointer;display:block;' }, [
|
||||
E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '📄'),
|
||||
E('div', { style: 'color:#3498db;font-size:1.2em;' }, 'Click to select file'),
|
||||
E('div', { id: 'file-name', style: 'margin-top:10px;color:#27ae60;font-weight:bold;' }, '')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Step 2: Metadata
|
||||
E('div', { class: 'wizard-step', id: 'step-meta', style: 'display:none;' }, [
|
||||
E('h3', {}, '2. Article Details'),
|
||||
E('div', { style: 'margin:15px 0;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;' }, 'Title'),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'input-title',
|
||||
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;',
|
||||
placeholder: 'Article title...'
|
||||
})
|
||||
]),
|
||||
E('div', { style: 'margin:15px 0;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;' }, 'Category'),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'input-category',
|
||||
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;',
|
||||
placeholder: 'e.g., Blog, Tutorial, News',
|
||||
value: 'Uploads'
|
||||
})
|
||||
]),
|
||||
E('div', { style: 'margin:15px 0;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:5px;' }, 'Tags (comma-separated)'),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'input-tags',
|
||||
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;',
|
||||
placeholder: 'tag1, tag2, tag3'
|
||||
})
|
||||
]),
|
||||
E('div', { style: 'margin:20px 0;' }, [
|
||||
E('label', { style: 'display:block;margin-bottom:10px;' }, 'Visibility'),
|
||||
E('div', { style: 'display:flex;gap:20px;' }, [
|
||||
E('label', { style: 'display:flex;align-items:center;gap:8px;cursor:pointer;' }, [
|
||||
E('input', { type: 'radio', name: 'visibility', value: 'public', checked: true }),
|
||||
E('span', {}, '🌍 Public')
|
||||
]),
|
||||
E('label', { style: 'display:flex;align-items:center;gap:8px;cursor:pointer;' }, [
|
||||
E('input', { type: 'radio', name: 'visibility', value: 'private' }),
|
||||
E('span', {}, '🔒 Private (Draft)')
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Step 3: Target Selection
|
||||
E('div', { class: 'wizard-step', id: 'step-target', style: 'display:none;' }, [
|
||||
E('h3', {}, '3. Publish To'),
|
||||
E('div', { style: 'display:grid;grid-template-columns:repeat(2,1fr);gap:15px;margin:20px 0;' }, [
|
||||
E('div', {
|
||||
class: 'target-option selected',
|
||||
'data-target': 'hexojs',
|
||||
style: 'padding:20px;border:2px solid #3498db;border-radius:12px;cursor:pointer;text-align:center;'
|
||||
}, [
|
||||
E('div', { style: 'font-size:2em;' }, '📝'),
|
||||
E('div', { style: 'font-weight:bold;' }, 'HexoJS Blog'),
|
||||
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Static blog post')
|
||||
]),
|
||||
E('div', {
|
||||
class: 'target-option',
|
||||
'data-target': 'gitea',
|
||||
style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;'
|
||||
}, [
|
||||
E('div', { style: 'font-size:2em;' }, '🐙'),
|
||||
E('div', { style: 'font-weight:bold;' }, 'Gitea'),
|
||||
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Version control')
|
||||
]),
|
||||
E('div', {
|
||||
class: 'target-option',
|
||||
'data-target': 'streamlit',
|
||||
style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;'
|
||||
}, [
|
||||
E('div', { style: 'font-size:2em;' }, '📊'),
|
||||
E('div', { style: 'font-weight:bold;' }, 'Streamlit'),
|
||||
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Interactive app')
|
||||
]),
|
||||
E('div', {
|
||||
class: 'target-option',
|
||||
'data-target': 'metablogizer',
|
||||
style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;'
|
||||
}, [
|
||||
E('div', { style: 'font-size:2em;' }, '🔗'),
|
||||
E('div', { style: 'font-weight:bold;' }, 'MetaBlogizer'),
|
||||
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Multi-platform')
|
||||
])
|
||||
]),
|
||||
// Gitea options (shown when gitea selected)
|
||||
E('div', { id: 'gitea-options', style: 'display:none;margin-top:20px;padding:15px;background:#1a1a2e;border-radius:8px;' }, [
|
||||
E('h4', { style: 'margin-bottom:10px;' }, 'Gitea Repository'),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'gitea-repo',
|
||||
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#16213e;color:#fff;',
|
||||
placeholder: 'owner/repo (e.g., admin/my-docs)'
|
||||
}),
|
||||
E('input', {
|
||||
type: 'text',
|
||||
id: 'gitea-path',
|
||||
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#16213e;color:#fff;margin-top:10px;',
|
||||
placeholder: 'path/in/repo (e.g., docs/articles/)'
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Navigation Buttons
|
||||
E('div', { style: 'display:flex;justify-content:space-between;margin-top:30px;' }, [
|
||||
E('button', {
|
||||
id: 'btn-prev',
|
||||
class: 'cbi-button',
|
||||
style: 'display:none;',
|
||||
click: ui.createHandlerFn(this, 'prevStep')
|
||||
}, '← Back'),
|
||||
E('button', {
|
||||
id: 'btn-next',
|
||||
class: 'cbi-button cbi-button-action',
|
||||
style: 'margin-left:auto;',
|
||||
click: ui.createHandlerFn(this, 'nextStep')
|
||||
}, 'Next →')
|
||||
]),
|
||||
|
||||
// Progress dots
|
||||
E('div', { style: 'display:flex;justify-content:center;gap:10px;margin-top:20px;' }, [
|
||||
E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#3498db;' }),
|
||||
E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#444;' }),
|
||||
E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#444;' })
|
||||
])
|
||||
]),
|
||||
|
||||
// Result Card
|
||||
E('div', { id: 'result-card', class: 'kiss-card', style: 'display:none;padding:30px;text-align:center;' }, [
|
||||
E('div', { style: 'font-size:4em;margin-bottom:20px;' }, '✅'),
|
||||
E('h3', {}, 'Upload Successful!'),
|
||||
E('p', { id: 'result-message', style: 'color:#888;' }, ''),
|
||||
E('div', { style: 'margin-top:20px;' }, [
|
||||
E('a', {
|
||||
id: 'result-link',
|
||||
href: '#',
|
||||
class: 'cbi-button cbi-button-action',
|
||||
target: '_blank',
|
||||
style: 'text-decoration:none;'
|
||||
}, 'View Article'),
|
||||
E('button', {
|
||||
class: 'cbi-button',
|
||||
style: 'margin-left:10px;',
|
||||
click: function() { window.location.reload(); }
|
||||
}, 'Upload Another')
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// Initialize
|
||||
setTimeout(function() {
|
||||
self.currentStep = 1;
|
||||
self.selectedFile = null;
|
||||
self.selectedTarget = 'hexojs';
|
||||
|
||||
var fileInput = document.getElementById('file-input');
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
if (e.target.files.length > 0) {
|
||||
self.selectedFile = e.target.files[0];
|
||||
document.getElementById('file-name').textContent = self.selectedFile.name;
|
||||
var title = self.selectedFile.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
||||
document.getElementById('input-title').value = title;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.target-option').forEach(function(opt) {
|
||||
opt.addEventListener('click', function() {
|
||||
document.querySelectorAll('.target-option').forEach(function(o) {
|
||||
o.style.borderColor = '#444';
|
||||
});
|
||||
this.style.borderColor = '#3498db';
|
||||
self.selectedTarget = this.dataset.target;
|
||||
// Show/hide gitea options
|
||||
var giteaOpts = document.getElementById('gitea-options');
|
||||
if (giteaOpts) {
|
||||
giteaOpts.style.display = self.selectedTarget === 'gitea' ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return KissTheme.wrap([content], 'admin/services/hexojs/upload');
|
||||
},
|
||||
|
||||
nextStep: function() {
|
||||
var self = this;
|
||||
|
||||
if (this.currentStep === 1) {
|
||||
if (!this.selectedFile) {
|
||||
ui.addNotification(null, E('p', 'Please select a file'), 'warning');
|
||||
return;
|
||||
}
|
||||
document.getElementById('step-file').style.display = 'none';
|
||||
document.getElementById('step-meta').style.display = 'block';
|
||||
document.getElementById('btn-prev').style.display = 'block';
|
||||
this.currentStep = 2;
|
||||
this.updateDots();
|
||||
} else if (this.currentStep === 2) {
|
||||
var title = document.getElementById('input-title').value;
|
||||
if (!title) {
|
||||
ui.addNotification(null, E('p', 'Please enter a title'), 'warning');
|
||||
return;
|
||||
}
|
||||
document.getElementById('step-meta').style.display = 'none';
|
||||
document.getElementById('step-target').style.display = 'block';
|
||||
document.getElementById('btn-next').textContent = '📤 Publish';
|
||||
this.currentStep = 3;
|
||||
this.updateDots();
|
||||
} else if (this.currentStep === 3) {
|
||||
this.doUpload();
|
||||
}
|
||||
},
|
||||
|
||||
prevStep: function() {
|
||||
if (this.currentStep === 2) {
|
||||
document.getElementById('step-meta').style.display = 'none';
|
||||
document.getElementById('step-file').style.display = 'block';
|
||||
document.getElementById('btn-prev').style.display = 'none';
|
||||
this.currentStep = 1;
|
||||
} else if (this.currentStep === 3) {
|
||||
document.getElementById('step-target').style.display = 'none';
|
||||
document.getElementById('step-meta').style.display = 'block';
|
||||
document.getElementById('btn-next').textContent = 'Next →';
|
||||
this.currentStep = 2;
|
||||
}
|
||||
this.updateDots();
|
||||
},
|
||||
|
||||
updateDots: function() {
|
||||
var dots = document.querySelectorAll('.step-dot');
|
||||
var step = this.currentStep;
|
||||
dots.forEach(function(dot, i) {
|
||||
dot.style.background = i < step ? '#3498db' : '#444';
|
||||
});
|
||||
},
|
||||
|
||||
doUpload: function() {
|
||||
var self = this;
|
||||
var title = document.getElementById('input-title').value;
|
||||
var category = document.getElementById('input-category').value || 'Uploads';
|
||||
var tags = document.getElementById('input-tags').value || '';
|
||||
var visibility = document.querySelector('input[name="visibility"]:checked').value;
|
||||
var target = this.selectedTarget;
|
||||
|
||||
var ext = this.selectedFile.name.split('.').pop().toLowerCase();
|
||||
|
||||
ui.showModal('Uploading...', [
|
||||
E('p', { class: 'spinning' }, 'Processing ' + this.selectedFile.name + '...')
|
||||
]);
|
||||
|
||||
fileToBase64(this.selectedFile).then(function(base64) {
|
||||
// Route based on target
|
||||
if (target === 'hexojs') {
|
||||
// Default HexoJS upload
|
||||
if (ext === 'pdf') {
|
||||
return callUploadPDF(base64, title, visibility, category);
|
||||
} else if (ext === 'html' || ext === 'htm') {
|
||||
return callUploadHTML(base64, title, visibility, category, tags);
|
||||
} else {
|
||||
return callUploadMD(self.selectedFile.name, atob(base64), title, visibility, category, tags);
|
||||
}
|
||||
} else if (target === 'gitea') {
|
||||
// Gitea upload
|
||||
var repo = document.getElementById('gitea-repo').value || 'admin/uploads';
|
||||
var path = document.getElementById('gitea-path').value || 'uploads/';
|
||||
var fullPath = path.replace(/\/$/, '') + '/' + self.selectedFile.name;
|
||||
return callGiteaUpload(repo, fullPath, base64, 'Upload: ' + title);
|
||||
} else if (target === 'streamlit') {
|
||||
// Create Streamlit app from markdown/python
|
||||
var appName = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
|
||||
return callStreamlitCreate(appName, base64).then(function(result) {
|
||||
result.target = 'streamlit';
|
||||
result.appUrl = '/admin/services/streamlit/apps/' + appName;
|
||||
return result;
|
||||
});
|
||||
} else if (target === 'metablogizer') {
|
||||
// Create MetaBlogizer entry
|
||||
var entryName = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
|
||||
return callMetablogizerCreate(entryName, base64, category).then(function(result) {
|
||||
result.target = 'metablogizer';
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
// Fallback: use wizard_upload for unified handling
|
||||
var options = {};
|
||||
if (target === 'gitea') {
|
||||
options.repo = document.getElementById('gitea-repo').value;
|
||||
options.path = document.getElementById('gitea-path').value;
|
||||
}
|
||||
return callWizardUpload(base64, self.selectedFile.name, title, visibility, category, tags, target, JSON.stringify(options));
|
||||
}
|
||||
}).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result.success) {
|
||||
document.querySelector('.kiss-card').style.display = 'none';
|
||||
document.getElementById('result-card').style.display = 'block';
|
||||
|
||||
var message = 'Your content has been uploaded.';
|
||||
var linkText = 'View';
|
||||
var linkHref = '#';
|
||||
|
||||
if (target === 'hexojs') {
|
||||
message = 'Your ' + (visibility === 'public' ? 'article' : 'draft') + ' has been created.';
|
||||
linkText = 'View Article';
|
||||
if (result.slug) linkHref = '/' + result.slug + '/';
|
||||
} else if (target === 'gitea') {
|
||||
message = 'File uploaded to Gitea repository.';
|
||||
linkText = 'Open Gitea';
|
||||
linkHref = result.url || '/admin/services/gitea/overview';
|
||||
} else if (target === 'streamlit') {
|
||||
message = 'Streamlit app created successfully.';
|
||||
linkText = 'Open App';
|
||||
linkHref = result.appUrl || '/admin/services/streamlit/overview';
|
||||
} else if (target === 'metablogizer') {
|
||||
message = 'MetaBlogizer entry created.';
|
||||
linkText = 'View Entry';
|
||||
linkHref = result.url || '/admin/services/metablogizer/overview';
|
||||
}
|
||||
|
||||
document.getElementById('result-message').textContent = message;
|
||||
document.getElementById('result-link').textContent = linkText;
|
||||
document.getElementById('result-link').href = linkHref;
|
||||
} else {
|
||||
ui.addNotification(null, E('p', result.error || 'Upload failed'), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', 'Error: ' + err.message), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -113,5 +113,21 @@
|
||||
"type": "view",
|
||||
"path": "hexojs/static"
|
||||
}
|
||||
},
|
||||
"admin/services/hexojs/upload": {
|
||||
"title": "Upload",
|
||||
"order": 25,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "hexojs/upload"
|
||||
}
|
||||
},
|
||||
"admin/services/hexojs/submit": {
|
||||
"title": "Submit & Moderate",
|
||||
"order": 26,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "hexojs/submit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,10 @@
|
||||
"get_instance_endpoints",
|
||||
"get_instance_health",
|
||||
"get_pipeline_status",
|
||||
"static_list"
|
||||
"static_list",
|
||||
"get_uploads",
|
||||
"list_pending",
|
||||
"get_submission"
|
||||
]
|
||||
},
|
||||
"uci": ["hexojs"]
|
||||
@ -91,7 +94,15 @@
|
||||
"static_delete",
|
||||
"static_publish",
|
||||
"static_delete_file",
|
||||
"static_configure_auth"
|
||||
"static_configure_auth",
|
||||
"upload_article",
|
||||
"upload_pdf",
|
||||
"upload_html",
|
||||
"publish_draft",
|
||||
"unpublish_post",
|
||||
"submit_for_review",
|
||||
"approve_submission",
|
||||
"reject_submission"
|
||||
]
|
||||
},
|
||||
"uci": ["hexojs"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user