Enterprise UI redesign — Phases 14–22 (Docusign-branded migration console) #1
|
|
@ -0,0 +1,197 @@
|
||||||
|
// Project / customer context — localStorage CRUD + switcher modal
|
||||||
|
|
||||||
|
import { escHtml, uuid, formatDate } from './utils.js';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'migrator_projects';
|
||||||
|
|
||||||
|
// ── Data model ─────────────────────────────────────────────────────────────
|
||||||
|
// localStorage schema:
|
||||||
|
// { active: string|null, projects: Array<{ id, name, createdAt }> }
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { active: null, projects: [] };
|
||||||
|
} catch {
|
||||||
|
return { active: null, projects: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(data) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listProjects() {
|
||||||
|
return load().projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActive() {
|
||||||
|
const data = load();
|
||||||
|
return data.projects.find(p => p.id === data.active) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProject(name) {
|
||||||
|
const data = load();
|
||||||
|
const project = { id: uuid(), name: name.trim(), createdAt: new Date().toISOString() };
|
||||||
|
data.projects.push(project);
|
||||||
|
if (!data.active) data.active = project.id;
|
||||||
|
save(data);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProject(id) {
|
||||||
|
const data = load();
|
||||||
|
data.projects = data.projects.filter(p => p.id !== id);
|
||||||
|
if (data.active === id) {
|
||||||
|
data.active = data.projects[0]?.id || null;
|
||||||
|
}
|
||||||
|
save(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActive(id) {
|
||||||
|
const data = load();
|
||||||
|
if (data.projects.find(p => p.id === id)) {
|
||||||
|
data.active = id;
|
||||||
|
save(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Init: called on app startup ─────────────────────────────────────────────
|
||||||
|
// onUpdate callback is called whenever the active project changes
|
||||||
|
|
||||||
|
let _onUpdate = null;
|
||||||
|
|
||||||
|
export function initProject(onUpdate) {
|
||||||
|
_onUpdate = onUpdate;
|
||||||
|
onUpdate();
|
||||||
|
// Show project modal on first run if no projects exist
|
||||||
|
if (listProjects().length === 0) {
|
||||||
|
showProjectModal(onUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Project switcher modal ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function showProjectModal(onUpdate) {
|
||||||
|
if (onUpdate) _onUpdate = onUpdate;
|
||||||
|
|
||||||
|
const existing = document.getElementById('project-modal');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.id = 'project-modal';
|
||||||
|
wrapper.innerHTML = `
|
||||||
|
<div class="modal-backdrop"></div>
|
||||||
|
<div class="modal-box modal-sm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title">Switch Project</span>
|
||||||
|
<button class="modal-close" id="pm-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="padding-bottom:0">
|
||||||
|
<div class="project-list" id="pm-project-list"></div>
|
||||||
|
<div class="new-project-form">
|
||||||
|
<h4>New Project</h4>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-input" id="pm-new-name"
|
||||||
|
placeholder="Customer name (e.g. Acme Corp)" maxlength="60" />
|
||||||
|
<div class="form-error" id="pm-error"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" id="pm-cancel">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="pm-create">Create Project</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(wrapper);
|
||||||
|
|
||||||
|
renderProjectList();
|
||||||
|
|
||||||
|
document.getElementById('pm-close').onclick = () => wrapper.remove();
|
||||||
|
document.getElementById('pm-cancel').onclick = () => wrapper.remove();
|
||||||
|
document.getElementById('pm-create').onclick = handleCreate;
|
||||||
|
document.getElementById('pm-new-name').addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') handleCreate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus name input
|
||||||
|
setTimeout(() => document.getElementById('pm-new-name')?.focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProjectList() {
|
||||||
|
const list = document.getElementById('pm-project-list');
|
||||||
|
const active = getActive();
|
||||||
|
const projects = listProjects();
|
||||||
|
|
||||||
|
if (!projects.length) {
|
||||||
|
list.innerHTML = `<p style="font-size:13px;color:var(--text-muted);margin-bottom:8px">
|
||||||
|
No projects yet. Create one below to get started.
|
||||||
|
</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = projects.map(p => `
|
||||||
|
<div class="project-row ${p.id === active?.id ? 'active' : ''}" data-id="${escHtml(p.id)}">
|
||||||
|
<div class="project-row-icon">${escHtml(p.name.slice(0, 2).toUpperCase())}</div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="project-row-name">${escHtml(p.name)}</div>
|
||||||
|
<div class="project-row-sub">Created ${formatDate(p.createdAt)}</div>
|
||||||
|
</div>
|
||||||
|
${p.id === active?.id
|
||||||
|
? '<span class="project-row-active-badge">● Active</span>'
|
||||||
|
: `<button class="btn btn-secondary btn-xs pm-delete-btn" data-id="${escHtml(p.id)}" title="Delete project">✕</button>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Activate on row click
|
||||||
|
list.querySelectorAll('.project-row').forEach(row => {
|
||||||
|
row.addEventListener('click', e => {
|
||||||
|
if (e.target.classList.contains('pm-delete-btn')) return;
|
||||||
|
activateProject(row.dataset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete buttons
|
||||||
|
list.querySelectorAll('.pm-delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(btn.dataset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateProject(id) {
|
||||||
|
setActive(id);
|
||||||
|
if (_onUpdate) _onUpdate();
|
||||||
|
const modal = document.getElementById('project-modal');
|
||||||
|
if (modal) modal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(id) {
|
||||||
|
const project = listProjects().find(p => p.id === id);
|
||||||
|
if (!project) return;
|
||||||
|
if (!confirm(`Delete project "${project.name}"? This cannot be undone.`)) return;
|
||||||
|
deleteProject(id);
|
||||||
|
if (_onUpdate) _onUpdate();
|
||||||
|
renderProjectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
const input = document.getElementById('pm-new-name');
|
||||||
|
const errorEl = document.getElementById('pm-error');
|
||||||
|
const name = input?.value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
if (errorEl) errorEl.textContent = 'Project name is required.';
|
||||||
|
input?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorEl) errorEl.textContent = '';
|
||||||
|
|
||||||
|
const project = createProject(name);
|
||||||
|
setActive(project.id);
|
||||||
|
if (_onUpdate) _onUpdate();
|
||||||
|
const modal = document.getElementById('project-modal');
|
||||||
|
if (modal) modal.remove();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue