feat: custom modal system replaces browser prompt() and confirm()
- Added themed dark modal with CSS matching FlowDeck design - showPrompt(title, message, placeholder, callback) replaces prompt() - showConfirm(title, message, callback) replaces confirm() - Updated: newPageInWorkspace, newFolderInWorkspace, deleteWorkspacePage - Modal closes on escape, overlay click, or cancel button - Consistent look across all dialogs on the site
This commit is contained in:
+83
-27
@@ -581,6 +581,14 @@
|
||||
giteaOwner: '{{ gitea_owner|default("") }}',
|
||||
giteaRepo: '{{ gitea_repo|default("") }}',
|
||||
giteaTree: [], // loaded client-side for sidebar
|
||||
// Custom Modal System
|
||||
modal: { show: false, title: '', message: '', input: false, value: '', placeholder: '', confirmText: 'OK', callback: function(){} },
|
||||
showPrompt(title, message, placeholder, callback) {
|
||||
this.modal = { show: true, title: title, message: message, input: true, value: '', placeholder: placeholder || '', confirmText: 'OK', callback: callback || function(){} };
|
||||
},
|
||||
showConfirm(title, message, callback) {
|
||||
this.modal = { show: true, title: title, message: message, input: false, value: '', placeholder: '', confirmText: 'Yes', callback: callback || function(){} };
|
||||
},
|
||||
currentView: 'home',
|
||||
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
||||
contextActions: {},
|
||||
@@ -901,67 +909,73 @@
|
||||
.catch(function() { /* silently ignore */ });
|
||||
},
|
||||
newPageInWorkspace() {
|
||||
const name = prompt('File name:');
|
||||
if (!name || !name.trim()) return;
|
||||
const wk = this.workspaceKey || '';
|
||||
const self = this;
|
||||
this.showPrompt('New File', 'Enter a name for the new file:', 'File name', function(name) {
|
||||
if (!name || !name.trim()) return;
|
||||
const wk = self.workspaceKey || '';
|
||||
// Gitea/remote workspace: create local file in mirror workspace, keep remote focus
|
||||
if (wk.includes('/')) {
|
||||
const wsId = this.localWsId || 0;
|
||||
const wsId = self.localWsId || 0;
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: JSON.stringify({ name: name.trim(), type: 'page', workspace_id: wsId })
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
.catch(err => { self.toast('Failed: ' + err.message, 'error'); });
|
||||
return;
|
||||
}
|
||||
// Local workspace
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: JSON.stringify({ name: name.trim(), type: 'page' })
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
.catch(err => { self.toast('Failed: ' + err.message, 'error'); });
|
||||
});
|
||||
},
|
||||
newFolderInWorkspace() {
|
||||
const name = prompt('Folder name:');
|
||||
if (!name || !name.trim()) return;
|
||||
const wk = this.workspaceKey || '';
|
||||
const self = this;
|
||||
this.showPrompt('New Folder', 'Enter a name for the new folder:', 'Folder name', function(name) {
|
||||
if (!name || !name.trim()) return;
|
||||
const wk = self.workspaceKey || '';
|
||||
// Gitea workspace: create folder in local mirror workspace
|
||||
if (wk.includes('/')) {
|
||||
const wsId = this.localWsId || 0;
|
||||
const wsId = self.localWsId || 0;
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: JSON.stringify({ name: name.trim(), type: 'folder', workspace_id: wsId })
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(data => { this.toast('Folder created in local workspace', 'success'); })
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
.then(data => { self.toast('Folder created in local workspace', 'success'); })
|
||||
.catch(err => { self.toast('Failed: ' + err.message, 'error'); });
|
||||
return;
|
||||
}
|
||||
// Local workspace
|
||||
fetch('/api/local-workspace/items', {
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: JSON.stringify({ name: name.trim(), type: 'folder' })
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(() => { window.location.reload(); })
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
.catch(err => { self.toast('Failed: ' + err.message, 'error'); });
|
||||
});
|
||||
},
|
||||
deleteWorkspacePage(pageId) {
|
||||
if (!confirm('Delete this page?')) return;
|
||||
fetch(`/api/local-workspace/items/${pageId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(() => { window.location.reload(); })
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
const self = this;
|
||||
this.showConfirm("Delete Page", "Are you sure you want to delete this page?", function() {
|
||||
fetch(`/api/local-workspace/items/${pageId}`, {
|
||||
method: "DELETE",
|
||||
headers: { "X-CSRF-Token": self.getCsrfToken() }
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error("Failed"); return r.json(); })
|
||||
.then(() => { window.location.reload(); })
|
||||
.catch(err => { self.toast("Failed: " + err.message, "error"); });
|
||||
});
|
||||
},
|
||||
|
||||
// ── Drag & Drop ──
|
||||
@@ -995,8 +1009,9 @@
|
||||
|
||||
// ── Sub-item creation in folders ──
|
||||
newPageInFolder(parentId) {
|
||||
const name = prompt('File name:');
|
||||
if (!name || !name.trim()) return;
|
||||
const self = this;
|
||||
this.showPrompt('New File', 'Enter a name for the new file:', 'File name', function(name) {
|
||||
if (!name || !name.trim()) return;
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
@@ -1223,5 +1238,46 @@
|
||||
</script>
|
||||
<script src="/static/js/app.js?v=2.4.6" defer></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
<style>
|
||||
.flowdeck-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:2000;display:flex;align-items:center;justify-content:center;}
|
||||
.flowdeck-modal{background:var(--bg-secondary,#222);border:1px solid var(--border,rgba(255,255,255,.08));border-radius:12px;width:100%;max-width:420px;box-shadow:0 20px 60px rgba(0,0,0,0.5);}
|
||||
.flowdeck-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border,rgba(255,255,255,.08));}
|
||||
.flowdeck-modal-header h3{margin:0;font-size:16px;color:var(--text,#fff);}
|
||||
.flowdeck-modal-close{background:none;border:none;color:var(--text-dim,rgba(255,255,255,.4));font-size:20px;cursor:pointer;padding:4px 8px;border-radius:6px;}
|
||||
.flowdeck-modal-close:hover{background:var(--bg-hover,#333);color:var(--text,#fff);}
|
||||
.flowdeck-modal-body{padding:20px;}
|
||||
.flowdeck-modal-body p{color:var(--text-dim,rgba(255,255,255,.6));font-size:14px;margin:0 0 12px 0;}
|
||||
.flowdeck-modal-input{width:100%;padding:10px 12px;background:var(--bg-tertiary,#2A2A2A);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:8px;color:var(--text,#fff);font-size:14px;outline:none;box-sizing:border-box;}
|
||||
.flowdeck-modal-input:focus{border-color:var(--accent,#2383E2);box-shadow:0 0 0 1px var(--accent,#2383E2);}
|
||||
.flowdeck-modal-footer{display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid var(--border,rgba(255,255,255,.08));}
|
||||
.flowdeck-modal-btn{padding:8px 20px;border-radius:8px;font-size:13px;cursor:pointer;border:none;}
|
||||
.flowdeck-modal-btn.cancel{background:var(--bg-tertiary,#333);color:var(--text-dim,rgba(255,255,255,.6));}
|
||||
.flowdeck-modal-btn.cancel:hover{background:var(--bg-hover,#444);}
|
||||
.flowdeck-modal-btn.confirm{background:var(--accent,#2383E2);color:#fff;}
|
||||
.flowdeck-modal-btn.confirm:hover{background:#2C8CEB;}
|
||||
</style>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div class="flowdeck-modal-overlay" x-show="modal.show" x-transition.opacity @click.self="modal.show=false" @keydown.escape="modal.show=false" style="display:none">
|
||||
<div class="flowdeck-modal" x-show="modal.show" x-transition.scale.origin.top">
|
||||
<div class="flowdeck-modal-header">
|
||||
<h3 x-text="modal.title"></h3>
|
||||
<button class="flowdeck-modal-close" @click="modal.show=false">×</button>
|
||||
</div>
|
||||
<div class="flowdeck-modal-body">
|
||||
<p x-text="modal.message" x-show="modal.message"></p>
|
||||
<template x-if="modal.input">
|
||||
<input type="text" x-model="modal.value" class="flowdeck-modal-input" :placeholder="modal.placeholder || ''" @keydown.enter="modal.show=false; modal.callback(modal.value)">
|
||||
</template>
|
||||
</div>
|
||||
<div class="flowdeck-modal-footer">
|
||||
<button class="flowdeck-modal-btn cancel" @click="modal.show=false">Cancel</button>
|
||||
<button class="flowdeck-modal-btn confirm" @click="modal.show=false; modal.callback(modal.value)"><span x-text="modal.confirmText || 'OK'"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user