ui: harden workspace and secrets rendering
This commit is contained in:
@@ -2,20 +2,19 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/frontend-xss-hardening`
|
### `feat/frontend-shell-unification`
|
||||||
|
|
||||||
Status: in_progress
|
Status: in_progress
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- API-derived data is not inserted into `innerHTML` without escaping
|
- duplicated shell identity logic is removed from `auth.js` and `nav.js`
|
||||||
- shared DOM-safe helpers are introduced for frontend rendering paths that still need markup
|
- one shared source of truth renders avatar, name, role, and menu state
|
||||||
- known unsafe rendering paths from `__REVIEW_FRONT.md` are removed or hardened
|
- pages keep the current shell behavior after the consolidation
|
||||||
- the current UI behavior remains intact after the hardening pass
|
- no page depends on divergent copies of `crank_user` handling
|
||||||
- targeted frontend smoke checks cover the touched pages
|
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/frontend-shell-unification`
|
- `feat/frontend-wizard-modularization`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<link rel="stylesheet" href="css/wizard.css">
|
<link rel="stylesheet" href="css/wizard.css">
|
||||||
<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/dom.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/ui-feedback.js"></script>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<link rel="stylesheet" href="css/pages.css">
|
<link rel="stylesheet" href="css/pages.css">
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<link rel="stylesheet" href="css/logs.css">
|
<link rel="stylesheet" href="css/logs.css">
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<link rel="stylesheet" href="css/settings.css">
|
<link rel="stylesheet" href="css/settings.css">
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<link rel="stylesheet" href="css/pages.css">
|
<link rel="stylesheet" href="css/pages.css">
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<link rel="stylesheet" href="css/usage.css">
|
<link rel="stylesheet" href="css/usage.css">
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/workspace.js"></script>
|
<script src="js/workspace.js"></script>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<link rel="stylesheet" href="css/workspace-setup.css">
|
<link rel="stylesheet" href="css/workspace-setup.css">
|
||||||
<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/dom.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
<script src="js/ui-feedback.js"></script>
|
<script src="js/ui-feedback.js"></script>
|
||||||
<script src="js/auth.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<link rel="stylesheet" href="css/pages.css">
|
<link rel="stylesheet" href="css/pages.css">
|
||||||
<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/dom.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/ui-feedback.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
(function() {
|
||||||
|
function clear(element) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (element.firstChild) {
|
||||||
|
element.removeChild(element.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyState(title, body) {
|
||||||
|
var root = document.createElement('div');
|
||||||
|
root.className = 'empty-state';
|
||||||
|
|
||||||
|
var titleNode = document.createElement('div');
|
||||||
|
titleNode.className = 'empty-state-title';
|
||||||
|
titleNode.textContent = title;
|
||||||
|
root.appendChild(titleNode);
|
||||||
|
|
||||||
|
var bodyNode = document.createElement('div');
|
||||||
|
bodyNode.className = 'empty-state-text';
|
||||||
|
bodyNode.textContent = body;
|
||||||
|
root.appendChild(bodyNode);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CrankDom = {
|
||||||
|
clear: clear,
|
||||||
|
createEmptyState: createEmptyState,
|
||||||
|
};
|
||||||
|
}());
|
||||||
+120
-40
@@ -222,7 +222,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
total: state.secrets.length,
|
total: state.secrets.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
tbody.innerHTML = '';
|
window.CrankDom.clear(tbody);
|
||||||
|
|
||||||
if (state.loading && state.secrets.length === 0) {
|
if (state.loading && state.secrets.length === 0) {
|
||||||
var loadingRow = document.createElement('tr');
|
var loadingRow = document.createElement('tr');
|
||||||
@@ -260,17 +260,38 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
rows.forEach(function (secret) {
|
rows.forEach(function (secret) {
|
||||||
var tr = document.createElement('tr');
|
var tr = document.createElement('tr');
|
||||||
var references = usage[secret.id] || [];
|
var references = usage[secret.id] || [];
|
||||||
tr.innerHTML = [
|
|
||||||
'<td class="col-name">' + secret.name + '</td>',
|
|
||||||
'<td>' + secretKindLabel(secret.kind) + '</td>',
|
|
||||||
'<td class="col-mono">v' + secret.current_version + '</td>',
|
|
||||||
'<td>' + (secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never')) + '</td>',
|
|
||||||
'<td>' + window.tPlural('secrets.used_by_count', references.length, { count: references.length }) + '</td>',
|
|
||||||
'<td><span class="badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active') + '">' + tKey('secrets.status.' + secret.status) + '</span></td>',
|
|
||||||
'<td class="col-actions"></td>'
|
|
||||||
].join('');
|
|
||||||
|
|
||||||
var actions = tr.querySelector('.col-actions');
|
var nameCell = document.createElement('td');
|
||||||
|
nameCell.className = 'col-name';
|
||||||
|
nameCell.textContent = secret.name;
|
||||||
|
tr.appendChild(nameCell);
|
||||||
|
|
||||||
|
var kindCell = document.createElement('td');
|
||||||
|
kindCell.textContent = secretKindLabel(secret.kind);
|
||||||
|
tr.appendChild(kindCell);
|
||||||
|
|
||||||
|
var versionCell = document.createElement('td');
|
||||||
|
versionCell.className = 'col-mono';
|
||||||
|
versionCell.textContent = 'v' + secret.current_version;
|
||||||
|
tr.appendChild(versionCell);
|
||||||
|
|
||||||
|
var lastUsedCell = document.createElement('td');
|
||||||
|
lastUsedCell.textContent = secret.last_used_at ? formatDate(secret.last_used_at) : tKey('secrets.last_used.never');
|
||||||
|
tr.appendChild(lastUsedCell);
|
||||||
|
|
||||||
|
var usedByCell = document.createElement('td');
|
||||||
|
usedByCell.textContent = window.tPlural('secrets.used_by_count', references.length, { count: references.length });
|
||||||
|
tr.appendChild(usedByCell);
|
||||||
|
|
||||||
|
var statusCell = document.createElement('td');
|
||||||
|
var badge = document.createElement('span');
|
||||||
|
badge.className = 'badge ' + (secret.status === 'disabled' ? 'badge-revoked' : 'badge-active');
|
||||||
|
badge.textContent = tKey('secrets.status.' + secret.status);
|
||||||
|
statusCell.appendChild(badge);
|
||||||
|
tr.appendChild(statusCell);
|
||||||
|
|
||||||
|
var actions = document.createElement('td');
|
||||||
|
actions.className = 'col-actions';
|
||||||
var rotateButton = document.createElement('button');
|
var rotateButton = document.createElement('button');
|
||||||
rotateButton.className = 'icon-btn';
|
rotateButton.className = 'icon-btn';
|
||||||
rotateButton.textContent = tKey('secrets.action.rotate');
|
rotateButton.textContent = tKey('secrets.action.rotate');
|
||||||
@@ -286,6 +307,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
await deleteSecret(secret);
|
await deleteSecret(secret);
|
||||||
});
|
});
|
||||||
actions.appendChild(deleteButton);
|
actions.appendChild(deleteButton);
|
||||||
|
tr.appendChild(actions);
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
@@ -304,49 +326,107 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
{ count: state.profiles.length }
|
{ count: state.profiles.length }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.CrankDom.clear(profilesList);
|
||||||
|
|
||||||
if (state.error) {
|
if (state.error) {
|
||||||
profilesList.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + tKey('secrets.profiles.error_title') + '</div><div class="empty-state-text">' + state.error + '</div></div>';
|
profilesList.appendChild(window.CrankDom.createEmptyState(
|
||||||
|
tKey('secrets.profiles.error_title'),
|
||||||
|
state.error
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.loading && state.profiles.length === 0) {
|
if (state.loading && state.profiles.length === 0) {
|
||||||
profilesList.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + tKey('secrets.profiles.loading_title') + '</div><div class="empty-state-text">' + tKey('secrets.profiles.loading_body') + '</div></div>';
|
profilesList.appendChild(window.CrankDom.createEmptyState(
|
||||||
|
tKey('secrets.profiles.loading_title'),
|
||||||
|
tKey('secrets.profiles.loading_body')
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.profiles.length) {
|
if (!state.profiles.length) {
|
||||||
profilesList.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + tKey('secrets.profiles.empty_title') + '</div><div class="empty-state-text">' + tKey('secrets.profiles.empty_body') + '</div></div>';
|
profilesList.appendChild(window.CrankDom.createEmptyState(
|
||||||
|
tKey('secrets.profiles.empty_title'),
|
||||||
|
tKey('secrets.profiles.empty_body')
|
||||||
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
profilesList.innerHTML = state.profiles.map(function (profile) {
|
state.profiles.forEach(function (profile) {
|
||||||
var secretIds = secretIdsForProfile(profile);
|
var secretIds = secretIdsForProfile(profile);
|
||||||
var references = secretIds.map(function (secretId) {
|
var card = document.createElement('div');
|
||||||
|
card.className = 'resource-card';
|
||||||
|
|
||||||
|
var header = document.createElement('div');
|
||||||
|
header.className = 'resource-card-header';
|
||||||
|
var headerBody = document.createElement('div');
|
||||||
|
|
||||||
|
var title = document.createElement('div');
|
||||||
|
title.className = 'resource-card-title';
|
||||||
|
title.textContent = profile.name;
|
||||||
|
headerBody.appendChild(title);
|
||||||
|
|
||||||
|
var subtitle = document.createElement('div');
|
||||||
|
subtitle.className = 'resource-card-subtitle';
|
||||||
|
subtitle.textContent = profileSummary(profile, secretsById);
|
||||||
|
headerBody.appendChild(subtitle);
|
||||||
|
|
||||||
|
var pillRow = document.createElement('div');
|
||||||
|
pillRow.className = 'resource-pill-row';
|
||||||
|
var statusPill = document.createElement('span');
|
||||||
|
statusPill.className = 'resource-status-pill active';
|
||||||
|
statusPill.textContent = authKindLabel(profile.kind);
|
||||||
|
pillRow.appendChild(statusPill);
|
||||||
|
headerBody.appendChild(pillRow);
|
||||||
|
header.appendChild(headerBody);
|
||||||
|
card.appendChild(header);
|
||||||
|
|
||||||
|
var metaGrid = document.createElement('div');
|
||||||
|
metaGrid.className = 'resource-meta-grid';
|
||||||
|
[
|
||||||
|
[tKey('secrets.profiles.meta.created'), formatDate(profile.created_at)],
|
||||||
|
[tKey('secrets.profiles.meta.updated'), formatDate(profile.updated_at)]
|
||||||
|
].forEach(function(entry) {
|
||||||
|
var item = document.createElement('div');
|
||||||
|
item.className = 'resource-meta-item';
|
||||||
|
var label = document.createElement('div');
|
||||||
|
label.className = 'resource-meta-label';
|
||||||
|
label.textContent = entry[0];
|
||||||
|
var value = document.createElement('div');
|
||||||
|
value.className = 'resource-meta-value';
|
||||||
|
value.textContent = entry[1];
|
||||||
|
item.appendChild(label);
|
||||||
|
item.appendChild(value);
|
||||||
|
metaGrid.appendChild(item);
|
||||||
|
});
|
||||||
|
card.appendChild(metaGrid);
|
||||||
|
|
||||||
|
var detail = document.createElement('div');
|
||||||
|
detail.className = 'resource-detail-block';
|
||||||
|
var detailTitle = document.createElement('div');
|
||||||
|
detailTitle.className = 'resource-detail-title';
|
||||||
|
detailTitle.textContent = tKey('secrets.profiles.references');
|
||||||
|
detail.appendChild(detailTitle);
|
||||||
|
|
||||||
|
var refList = document.createElement('div');
|
||||||
|
refList.className = 'secret-ref-list';
|
||||||
|
secretIds.forEach(function(secretId) {
|
||||||
var users = usage[secretId] || [];
|
var users = usage[secretId] || [];
|
||||||
return '<span class="secret-ref-pill"><strong>' + secretName(secretsById, secretId) + '</strong><span>' + tfKey('secrets.profiles.reference_count', { count: users.length }) + '</span></span>';
|
var pill = document.createElement('span');
|
||||||
}).join('');
|
pill.className = 'secret-ref-pill';
|
||||||
return [
|
var strong = document.createElement('strong');
|
||||||
'<div class="resource-card">',
|
strong.textContent = secretName(secretsById, secretId);
|
||||||
' <div class="resource-card-header">',
|
var count = document.createElement('span');
|
||||||
' <div>',
|
count.textContent = tfKey('secrets.profiles.reference_count', { count: users.length });
|
||||||
' <div class="resource-card-title">' + profile.name + '</div>',
|
pill.appendChild(strong);
|
||||||
' <div class="resource-card-subtitle">' + profileSummary(profile, secretsById) + '</div>',
|
pill.appendChild(count);
|
||||||
' <div class="resource-pill-row">',
|
refList.appendChild(pill);
|
||||||
' <span class="resource-status-pill active">' + authKindLabel(profile.kind) + '</span>',
|
});
|
||||||
' </div>',
|
detail.appendChild(refList);
|
||||||
' </div>',
|
card.appendChild(detail);
|
||||||
' </div>',
|
|
||||||
' <div class="resource-meta-grid">',
|
profilesList.appendChild(card);
|
||||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('secrets.profiles.meta.created') + '</div><div class="resource-meta-value">' + formatDate(profile.created_at) + '</div></div>',
|
});
|
||||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('secrets.profiles.meta.updated') + '</div><div class="resource-meta-value">' + formatDate(profile.updated_at) + '</div></div>',
|
|
||||||
' </div>',
|
|
||||||
' <div class="resource-detail-block">',
|
|
||||||
' <div class="resource-detail-title">' + tKey('secrets.profiles.references') + '</div>',
|
|
||||||
' <div class="secret-ref-list">' + references + '</div>',
|
|
||||||
' </div>',
|
|
||||||
'</div>'
|
|
||||||
].join('');
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
|||||||
+51
-15
@@ -110,23 +110,59 @@ function renderWorkspaceList() {
|
|||||||
|
|
||||||
var list = document.getElementById('ws-dropdown-list');
|
var list = document.getElementById('ws-dropdown-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
window.CrankDom.clear(list);
|
||||||
|
|
||||||
list.innerHTML = WS_LIST.map(function(workspace) {
|
WS_LIST.forEach(function(workspace) {
|
||||||
var active = current && workspace.id === current.id;
|
var active = current && workspace.id === current.id;
|
||||||
return '<div class="ws-dropdown-item' + (active ? ' active' : '') + '" onclick="switchWorkspace(\'' + workspace.id + '\')">' +
|
var item = document.createElement('div');
|
||||||
'<div class="ws-item-dot" style="background:' + workspace.color + '">' + workspace.letter + '</div>' +
|
item.className = 'ws-dropdown-item' + (active ? ' active' : '');
|
||||||
'<div class="ws-item-info">' +
|
item.addEventListener('click', function() {
|
||||||
'<div class="ws-item-name">' + workspace.name + '</div>' +
|
switchWorkspace(workspace.id);
|
||||||
'<div class="ws-item-role">' + workspace.role + '</div>' +
|
});
|
||||||
'</div>' +
|
|
||||||
(active ? '<svg width="12" height="12"><use href="' + (window.APP_BASE || '') + 'icons/general/check.svg#icon"/></svg>' : '') +
|
var dot = document.createElement('div');
|
||||||
'</div>';
|
dot.className = 'ws-item-dot';
|
||||||
}).join('') +
|
dot.style.background = workspace.color;
|
||||||
'<div class="ws-dropdown-divider"></div>' +
|
dot.textContent = workspace.letter;
|
||||||
'<a class="ws-dropdown-mgmt-link" href="' + ((window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup') + '" onclick="var dd=document.getElementById(\'ws-dropdown\');if(dd)dd.style.display=\'none\'">' +
|
item.appendChild(dot);
|
||||||
'<svg width="14" height="14"><use href="' + (window.APP_BASE || '') + 'icons/general/settings.svg#icon"/></svg>' +
|
|
||||||
tKey('settings.ws.title') +
|
var info = document.createElement('div');
|
||||||
'</a>';
|
info.className = 'ws-item-info';
|
||||||
|
var name = document.createElement('div');
|
||||||
|
name.className = 'ws-item-name';
|
||||||
|
name.textContent = workspace.name;
|
||||||
|
var role = document.createElement('div');
|
||||||
|
role.className = 'ws-item-role';
|
||||||
|
role.textContent = workspace.role;
|
||||||
|
info.appendChild(name);
|
||||||
|
info.appendChild(role);
|
||||||
|
item.appendChild(info);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
var check = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
check.setAttribute('width', '12');
|
||||||
|
check.setAttribute('height', '12');
|
||||||
|
check.innerHTML = '<use href="' + (window.APP_BASE || '') + 'icons/general/check.svg#icon"></use>';
|
||||||
|
item.appendChild(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
var divider = document.createElement('div');
|
||||||
|
divider.className = 'ws-dropdown-divider';
|
||||||
|
list.appendChild(divider);
|
||||||
|
|
||||||
|
var manageLink = document.createElement('a');
|
||||||
|
manageLink.className = 'ws-dropdown-mgmt-link';
|
||||||
|
manageLink.href = (window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup';
|
||||||
|
manageLink.addEventListener('click', function() {
|
||||||
|
var dd = document.getElementById('ws-dropdown');
|
||||||
|
if (dd) dd.style.display = 'none';
|
||||||
|
});
|
||||||
|
manageLink.innerHTML = '<svg width="14" height="14"><use href="' + (window.APP_BASE || '') + 'icons/general/settings.svg#icon"/></svg>';
|
||||||
|
manageLink.appendChild(document.createTextNode(tKey('settings.ws.title')));
|
||||||
|
list.appendChild(manageLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkspaces() {
|
async function loadWorkspaces() {
|
||||||
|
|||||||
Reference in New Issue
Block a user