feat: polish alpine shell feedback
This commit is contained in:
@@ -2,19 +2,19 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/demo-assets`
|
### `feat/alpine-polish`
|
||||||
|
|
||||||
Status: in_progress
|
Status: in_progress
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- project has an idempotent PostgreSQL demo seed for live UI screens
|
- Alpine UI pages have consistent feedback and shell state
|
||||||
- demo seed fills workspaces, memberships, invitations, operations, agents, keys and usage
|
- alert()-based errors are removed from primary screens
|
||||||
- seed is controlled by env and does not live in migrations
|
- header identity reflects live session and current workspace
|
||||||
- docs explain how to enable and use demo data
|
- settings and workspace flows avoid crude localStorage/logout workarounds
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/alpine-polish`
|
- `feat/current-workspace-session-model`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,77 @@
|
|||||||
|
|
||||||
.btn-icon.danger:hover { background: var(--red-bg); color: var(--red); border-color: var(--red-border); }
|
.btn-icon.danger:hover { background: var(--red-bg); color: var(--red); border-color: var(--red-border); }
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════
|
||||||
|
TOASTS
|
||||||
|
══════════════════════════════════════════════════ */
|
||||||
|
.toast-stack {
|
||||||
|
position: fixed;
|
||||||
|
top: 84px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1200;
|
||||||
|
width: min(360px, calc(100vw - 32px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: rgba(15, 23, 34, 0.96);
|
||||||
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.32);
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-card.closing {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info { border-color: rgba(56, 189, 248, 0.28); }
|
||||||
|
.toast-success { border-color: rgba(63, 185, 80, 0.32); }
|
||||||
|
.toast-error { border-color: rgba(248, 81, 73, 0.34); }
|
||||||
|
|
||||||
|
.toast-copy {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover { color: var(--text-primary); }
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════
|
||||||
SECTION CARD
|
SECTION CARD
|
||||||
══════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════ */
|
||||||
@@ -1456,3 +1527,12 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.toast-stack {
|
||||||
|
top: 72px;
|
||||||
|
right: 16px;
|
||||||
|
left: 16px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||||
</head>
|
</head>
|
||||||
@@ -292,7 +293,7 @@
|
|||||||
<input class="form-input input-mono" type="text" placeholder="customer-support" autocomplete="off"
|
<input class="form-input input-mono" type="text" placeholder="customer-support" autocomplete="off"
|
||||||
:value="form.slug" @input="onSlugInput($event.target.value)">
|
:value="form.slug" @input="onSlugInput($event.target.value)">
|
||||||
<div class="form-hint" x-show="form.slug">
|
<div class="form-hint" x-show="form.slug">
|
||||||
MCP endpoint: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace') || 'acme-workspace') + '/' + form.slug"></code>
|
MCP endpoint: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace_slug') || 'default') + '/' + form.slug"></code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<script src="js/config.js"></script>
|
<script src="js/config.js"></script>
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script src="js/config.js"></script>
|
<script src="js/config.js"></script>
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||||
@@ -542,21 +543,21 @@
|
|||||||
<div class="danger-zone-title">Rename workspace</div>
|
<div class="danger-zone-title">Rename workspace</div>
|
||||||
<div class="danger-zone-desc">Changes the workspace slug used in all API endpoints. Existing API keys will continue to work. All existing tool call references will break.</div>
|
<div class="danger-zone-desc">Changes the workspace slug used in all API endpoints. Existing API keys will continue to work. All existing tool call references will break.</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-danger" type="button">Rename</button>
|
<button class="btn-danger" id="settings-ws-rename-btn" type="button">Rename</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="danger-zone-action">
|
<div class="danger-zone-action">
|
||||||
<div class="danger-zone-text">
|
<div class="danger-zone-text">
|
||||||
<div class="danger-zone-title">Export all data</div>
|
<div class="danger-zone-title">Export all data</div>
|
||||||
<div class="danger-zone-desc">Download a ZIP archive of all operation configs, schemas, mappings and invocation history as JSON. May take a few minutes.</div>
|
<div class="danger-zone-desc">Download a ZIP archive of all operation configs, schemas, mappings and invocation history as JSON. May take a few minutes.</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-danger" type="button">Export</button>
|
<button class="btn-danger" id="settings-ws-export-btn" type="button">Export</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="danger-zone-action">
|
<div class="danger-zone-action">
|
||||||
<div class="danger-zone-text">
|
<div class="danger-zone-text">
|
||||||
<div class="danger-zone-title">Delete workspace</div>
|
<div class="danger-zone-title">Delete workspace</div>
|
||||||
<div class="danger-zone-desc">Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.</div>
|
<div class="danger-zone-desc">Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-danger" type="button" onclick="if(confirm('Delete workspace? This cannot be undone.')) { localStorage.clear(); window.location.href = 'login.html'; }">Delete workspace</button>
|
<button class="btn-danger" id="settings-ws-delete-btn" type="button">Delete workspace</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script src="../../js/i18n.js"></script>
|
<script src="../../js/i18n.js"></script>
|
||||||
<script src="../../js/workspace.js"></script>
|
<script src="../../js/workspace.js"></script>
|
||||||
<script src="../../js/api.js"></script>
|
<script src="../../js/api.js"></script>
|
||||||
|
<script src="../../js/ui-feedback.js"></script>
|
||||||
<script src="../../js/auth.js"></script>
|
<script src="../../js/auth.js"></script>
|
||||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||||
<script src="../../js/vendor/js-yaml/js-yaml.min.js"></script>
|
<script src="../../js/vendor/js-yaml/js-yaml.min.js"></script>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<script src="js/config.js"></script>
|
<script src="js/config.js"></script>
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -282,7 +282,9 @@ document.addEventListener('alpine:init', function() {
|
|||||||
await this.reload();
|
await this.reload();
|
||||||
this.closeDrawer();
|
this.closeDrawer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to save agent');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to save agent', 'Agent update failed');
|
||||||
|
}
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -294,7 +296,9 @@ document.addEventListener('alpine:init', function() {
|
|||||||
await window.CrankApi.deleteAgent(this.workspaceId, id);
|
await window.CrankApi.deleteAgent(this.workspaceId, id);
|
||||||
await this.reload();
|
await this.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to delete agent');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to delete agent', 'Delete failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -72,7 +72,9 @@ async function revokeKey(id) {
|
|||||||
await window.CrankApi.revokePlatformApiKey(currentWorkspaceId, id);
|
await window.CrankApi.revokePlatformApiKey(currentWorkspaceId, id);
|
||||||
await loadKeys();
|
await loadKeys();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to revoke key');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to revoke key', 'Revoke failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +85,9 @@ async function deleteKey(id) {
|
|||||||
await window.CrankApi.deletePlatformApiKey(currentWorkspaceId, id);
|
await window.CrankApi.deletePlatformApiKey(currentWorkspaceId, id);
|
||||||
await loadKeys();
|
await loadKeys();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to delete key');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to delete key', 'Delete failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +235,9 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedScopes.size) {
|
if (!selectedScopes.size) {
|
||||||
alert('Select at least one scope.');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.info('Select at least one scope before creating a key.', 'Missing scope');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +251,9 @@ document.getElementById('modal-confirm-btn').addEventListener('click', async fun
|
|||||||
document.getElementById('modal-footer-done').style.display = '';
|
document.getElementById('modal-footer-done').style.display = '';
|
||||||
renderTable();
|
renderTable();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to create key');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to create key', 'Key creation failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+80
-3
@@ -3,6 +3,20 @@
|
|||||||
var sessionCache = null;
|
var sessionCache = null;
|
||||||
var sessionPromise = null;
|
var sessionPromise = null;
|
||||||
|
|
||||||
|
function currentWorkspaceLabel() {
|
||||||
|
if (window.getCurrentWorkspace) {
|
||||||
|
var workspace = window.getCurrentWorkspace();
|
||||||
|
if (workspace && workspace.name) {
|
||||||
|
return workspace.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return localStorage.getItem('crank_workspace_slug') || 'workspace';
|
||||||
|
} catch (_error) {
|
||||||
|
return 'workspace';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isLoginPage() {
|
function isLoginPage() {
|
||||||
return /\/html\/login\.html$/.test(window.location.pathname);
|
return /\/html\/login\.html$/.test(window.location.pathname);
|
||||||
}
|
}
|
||||||
@@ -21,12 +35,25 @@
|
|||||||
try {
|
try {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
|
renderShellIdentity(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function primaryMembership(session) {
|
function primaryMembership(session) {
|
||||||
return session && session.memberships && session.memberships.length
|
if (!(session && session.memberships && session.memberships.length)) {
|
||||||
? session.memberships[0]
|
return null;
|
||||||
: null;
|
}
|
||||||
|
if (window.getCurrentWorkspace) {
|
||||||
|
var currentWorkspace = window.getCurrentWorkspace();
|
||||||
|
if (currentWorkspace) {
|
||||||
|
var matched = session.memberships.find(function(item) {
|
||||||
|
return item.workspace && item.workspace.id === currentWorkspace.id;
|
||||||
|
});
|
||||||
|
if (matched) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.memberships[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function initials(displayName, email) {
|
function initials(displayName, email) {
|
||||||
@@ -56,9 +83,47 @@
|
|||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mirroredUser() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||||
|
} catch (_error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderShellIdentity(session) {
|
||||||
|
var membership = primaryMembership(session);
|
||||||
|
var fallbackUser = mirroredUser();
|
||||||
|
var displayName = session && session.user
|
||||||
|
? (session.user.display_name || session.user.email || 'Crank')
|
||||||
|
: (fallbackUser && fallbackUser.name) || 'Crank';
|
||||||
|
var email = session && session.user
|
||||||
|
? session.user.email
|
||||||
|
: fallbackUser && fallbackUser.email;
|
||||||
|
var role = membership
|
||||||
|
? String(membership.role || '').replace(/^\w/, function(char) { return char.toUpperCase(); })
|
||||||
|
: (fallbackUser && fallbackUser.role) || 'Viewer';
|
||||||
|
var workspace = membership && membership.workspace
|
||||||
|
? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id)
|
||||||
|
: currentWorkspaceLabel();
|
||||||
|
var avatarValue = initials(displayName, email);
|
||||||
|
|
||||||
|
document.querySelectorAll('.nav-avatar').forEach(function(element) {
|
||||||
|
element.textContent = avatarValue;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.user-dropdown-name').forEach(function(element) {
|
||||||
|
element.textContent = displayName;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
|
||||||
|
element.textContent = role + ' · ' + workspace;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function replaceSession(session) {
|
function replaceSession(session) {
|
||||||
sessionCache = session;
|
sessionCache = session;
|
||||||
persistUserMirror(session);
|
persistUserMirror(session);
|
||||||
|
renderShellIdentity(session);
|
||||||
|
window.dispatchEvent(new CustomEvent('crank:sessionchange', { detail: session }));
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,4 +209,16 @@
|
|||||||
logout: logout,
|
logout: logout,
|
||||||
handleUnauthorized: handleUnauthorized,
|
handleUnauthorized: handleUnauthorized,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.addEventListener('crank:workspacechange', function() {
|
||||||
|
renderShellIdentity(sessionCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('crank:sessionchange', function(event) {
|
||||||
|
renderShellIdentity(event.detail || sessionCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
renderShellIdentity(sessionCache);
|
||||||
|
});
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -369,7 +369,9 @@ document.addEventListener('alpine:init', function() {
|
|||||||
}).filter(Boolean))).sort();
|
}).filter(Boolean))).sort();
|
||||||
this.stats = computeStats(this.operations);
|
this.stats = computeStats(this.operations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to delete operation');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to delete operation', 'Delete failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+72
-1
@@ -1,5 +1,20 @@
|
|||||||
var settingsSession = null;
|
var settingsSession = null;
|
||||||
|
|
||||||
|
function settingsWorkspaceId() {
|
||||||
|
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||||
|
return workspace ? workspace.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSettingsJson(fileName, value) {
|
||||||
|
var blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
function initials(displayName, email) {
|
function initials(displayName, email) {
|
||||||
var source = displayName || email || 'Crank';
|
var source = displayName || email || 'Crank';
|
||||||
return source
|
return source
|
||||||
@@ -255,7 +270,9 @@ async function loadWorkspaceSettings() {
|
|||||||
|
|
||||||
saveButton.textContent = 'Saved ✓';
|
saveButton.textContent = 'Saved ✓';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to save workspace settings');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to save workspace settings', 'Workspace save failed');
|
||||||
|
}
|
||||||
saveButton.textContent = original;
|
saveButton.textContent = original;
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -268,11 +285,65 @@ async function loadWorkspaceSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindDangerZoneActions() {
|
||||||
|
var renameButton = document.getElementById('settings-ws-rename-btn');
|
||||||
|
var exportButton = document.getElementById('settings-ws-export-btn');
|
||||||
|
var deleteButton = document.getElementById('settings-ws-delete-btn');
|
||||||
|
|
||||||
|
if (renameButton && renameButton.dataset.bound !== 'true') {
|
||||||
|
renameButton.dataset.bound = 'true';
|
||||||
|
renameButton.addEventListener('click', function() {
|
||||||
|
document.getElementById('settings-ws-slug').focus();
|
||||||
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.info('Update the workspace slug above and save changes to rename it.', 'Rename workspace');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportButton && exportButton.dataset.bound !== 'true') {
|
||||||
|
exportButton.dataset.bound = 'true';
|
||||||
|
exportButton.addEventListener('click', async function() {
|
||||||
|
var workspaceId = settingsWorkspaceId();
|
||||||
|
if (!workspaceId || !window.CrankApi) return;
|
||||||
|
try {
|
||||||
|
var snapshot = await window.CrankApi.exportWorkspace(workspaceId);
|
||||||
|
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||||
|
downloadSettingsJson((workspace ? workspace.slug : 'workspace') + '-snapshot.json', snapshot);
|
||||||
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.success('Workspace export downloaded.', 'Export complete');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to export workspace', 'Export failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteButton && deleteButton.dataset.bound !== 'true') {
|
||||||
|
deleteButton.dataset.bound = 'true';
|
||||||
|
deleteButton.addEventListener('click', async function() {
|
||||||
|
var workspaceId = settingsWorkspaceId();
|
||||||
|
if (!workspaceId || !window.CrankApi || !window.CrankAuth) return;
|
||||||
|
if (!window.confirm('Delete workspace? This cannot be undone.')) return;
|
||||||
|
try {
|
||||||
|
await window.CrankApi.deleteWorkspace(workspaceId);
|
||||||
|
await window.CrankAuth.logout();
|
||||||
|
} catch (error) {
|
||||||
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to delete workspace', 'Delete failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
injectLanguageSwitcher();
|
injectLanguageSwitcher();
|
||||||
bindSectionNavigation();
|
bindSectionNavigation();
|
||||||
loadProfile();
|
loadProfile();
|
||||||
bindProfileSave();
|
bindProfileSave();
|
||||||
bindPasswordSave();
|
bindPasswordSave();
|
||||||
|
bindDangerZoneActions();
|
||||||
loadWorkspaceSettings();
|
loadWorkspaceSettings();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
(function() {
|
||||||
|
var CONTAINER_ID = 'crank-toast-container';
|
||||||
|
var nextToastId = 0;
|
||||||
|
|
||||||
|
function ensureContainer() {
|
||||||
|
var container = document.getElementById(CONTAINER_ID);
|
||||||
|
if (container) return container;
|
||||||
|
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = CONTAINER_ID;
|
||||||
|
container.className = 'toast-stack';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss(toast) {
|
||||||
|
if (!toast || !toast.parentNode) return;
|
||||||
|
toast.classList.add('closing');
|
||||||
|
window.setTimeout(function() {
|
||||||
|
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||||
|
}, 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(options) {
|
||||||
|
var config = options || {};
|
||||||
|
var duration = config.duration === 0 ? 0 : (config.duration || 3600);
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
|
||||||
|
toast.id = 'toast_' + (++nextToastId);
|
||||||
|
toast.className = 'toast-card toast-' + (config.type || 'info');
|
||||||
|
toast.innerHTML =
|
||||||
|
'<div class="toast-copy">' +
|
||||||
|
(config.title ? '<div class="toast-title">' + config.title + '</div>' : '') +
|
||||||
|
(config.message ? '<div class="toast-message">' + config.message + '</div>' : '') +
|
||||||
|
'</div>' +
|
||||||
|
'<button class="toast-close" type="button" aria-label="Dismiss">' +
|
||||||
|
'<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">' +
|
||||||
|
'<line x1="1" y1="1" x2="11" y2="11"></line>' +
|
||||||
|
'<line x1="11" y1="1" x2="1" y2="11"></line>' +
|
||||||
|
'</svg>' +
|
||||||
|
'</button>';
|
||||||
|
|
||||||
|
toast.querySelector('.toast-close').addEventListener('click', function() {
|
||||||
|
dismiss(toast);
|
||||||
|
});
|
||||||
|
|
||||||
|
ensureContainer().appendChild(toast);
|
||||||
|
|
||||||
|
if (duration > 0) {
|
||||||
|
window.setTimeout(function() {
|
||||||
|
dismiss(toast);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CrankUi = {
|
||||||
|
notify: notify,
|
||||||
|
success: function(message, title) {
|
||||||
|
return notify({ type: 'success', title: title || 'Done', message: message });
|
||||||
|
},
|
||||||
|
error: function(message, title) {
|
||||||
|
return notify({ type: 'error', title: title || 'Request failed', message: message });
|
||||||
|
},
|
||||||
|
info: function(message, title) {
|
||||||
|
return notify({ type: 'info', title: title || 'Info', message: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}());
|
||||||
@@ -1179,7 +1179,9 @@ async function saveOperation(stayOnPage) {
|
|||||||
try {
|
try {
|
||||||
await persistCurrentDraft(stayOnPage);
|
await persistCurrentDraft(stayOnPage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to save operation');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to save operation', 'Save failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,7 +342,9 @@ async function submitForm() {
|
|||||||
submit.disabled = false;
|
submit.disabled = false;
|
||||||
}, 1400);
|
}, 1400);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to save workspace');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to save workspace', 'Save failed');
|
||||||
|
}
|
||||||
submit.disabled = false;
|
submit.disabled = false;
|
||||||
submit.textContent = originalLabel;
|
submit.textContent = originalLabel;
|
||||||
}
|
}
|
||||||
@@ -404,7 +406,9 @@ async function sendInvite() {
|
|||||||
document.getElementById('invite-success').style.display = '';
|
document.getElementById('invite-success').style.display = '';
|
||||||
await loadWorkspaceAccessData();
|
await loadWorkspaceAccessData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to send invite');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to send invite', 'Invite failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +428,9 @@ async function updateRole(select, userId) {
|
|||||||
renderMembers();
|
renderMembers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
select.value = previous;
|
select.value = previous;
|
||||||
alert(error.message || 'Failed to update role');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to update role', 'Role update failed');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
select.disabled = false;
|
select.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -442,7 +448,9 @@ async function removeMember(userId, name) {
|
|||||||
await window.CrankApi.deleteMembership(workspaceFormState.workspaceId, userId);
|
await window.CrankApi.deleteMembership(workspaceFormState.workspaceId, userId);
|
||||||
await loadWorkspaceAccessData();
|
await loadWorkspaceAccessData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to remove member');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to remove member', 'Member removal failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +463,9 @@ async function revokeInviteById(invitationId) {
|
|||||||
await window.CrankApi.deleteInvitation(workspaceFormState.workspaceId, invitationId);
|
await window.CrankApi.deleteInvitation(workspaceFormState.workspaceId, invitationId);
|
||||||
await loadWorkspaceAccessData();
|
await loadWorkspaceAccessData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to revoke invite');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to revoke invite', 'Invite revoke failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +499,9 @@ async function exportWorkspaceSnapshot() {
|
|||||||
: 'workspace';
|
: 'workspace';
|
||||||
downloadJsonFile(slug + '-snapshot.json', snapshot);
|
downloadJsonFile(slug + '-snapshot.json', snapshot);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to export workspace');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to export workspace', 'Export failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +517,9 @@ async function deleteWorkspaceAction() {
|
|||||||
await window.CrankApi.deleteWorkspace(workspaceFormState.workspaceId);
|
await window.CrankApi.deleteWorkspace(workspaceFormState.workspaceId);
|
||||||
await window.CrankAuth.logout();
|
await window.CrankAuth.logout();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to delete workspace');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to delete workspace', 'Delete failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +550,9 @@ async function initPage() {
|
|||||||
applyWorkspaceRecord(record);
|
applyWorkspaceRecord(record);
|
||||||
await loadWorkspaceAccessData();
|
await loadWorkspaceAccessData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message || 'Failed to load workspace');
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to load workspace', 'Workspace load failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user