T9.8 - D7: archiving stops reading as deletion - for project admins
Archiving already froze a project (the server refuses every write); what did not exist was the way back in. Now: - GET /api/projects?archived=only|all filters the answer BY PER-PROJECT ROLE: a project admin (or super/app admin) on THAT project sees it; everyone else receives an empty list from the same request - archived projects appear nowhere for them, counts and pickers included (the default listing already excluded them for everyone; asking is what got gated). Admin-on-Job-A does not surface archived Job B. - The launcher gains a visibly separate, labelled "Archived projects" section (dashed border, read-only stated in words), rendered only when the server returns rows. Opening one makes it active; the launcher's reconcile learned that an active project whose stored summary says archived:true was opened ON PURPOSE and keeps it, while a project archived out from under someone still drops with the existing explanation. - The creator shows ARCHIVED - READ-ONLY where the project is named (both ctx-bar branches, from the SERVER's answer - the page's project comes from the URL, so a stale local summary is not trusted) and refuses saves with a reason before the round trip. The courtesy; the server's refusal is the rule, verified by calling the endpoints directly (wp upsert AND the material-list write both refuse with "archived" even for an admin). - No unarchive button, no second mechanism, and it fits at 390px. Verification (each probe run alone): NEW tests/archived_check.py 15/15. Regressions: launcher_check 58/58, sample_check 10/10, export_check 20/20, frame_check 38/38. Items: D7 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -427,6 +427,19 @@
|
||||
══════════════════════════════════════════════════════════════════════ -->
|
||||
<div id="proj-status"><div class="proj-loading">Loading projects…</div></div>
|
||||
|
||||
<!-- D7 / T9.8: the way back into an archived project - PROJECT ADMINS ONLY
|
||||
(the server filters; everyone else gets an empty list and this section
|
||||
never renders). Visually its own thing, so nobody opens one thinking
|
||||
it is live: the server refuses every write regardless. -->
|
||||
<section class="section" id="archived-projects" hidden
|
||||
style="border:1px dashed var(--cds-border-subtle-01); background:var(--cds-layer-01); opacity:.92">
|
||||
<h2>Archived projects</h2>
|
||||
<p style="font-size:13px; color:var(--cds-text-secondary)">Read-only. Visible to project
|
||||
admins only. Opening one lets you read everything; nothing on it can be changed while
|
||||
it stays archived.</p>
|
||||
<div id="archived-projects-list"></div>
|
||||
</section>
|
||||
|
||||
<!-- (a) no projects at all -->
|
||||
<section class="section first-run" id="first-run" hidden>
|
||||
<h2>No projects yet</h2>
|
||||
@@ -585,7 +598,31 @@
|
||||
const esc = ProjectData.esc;
|
||||
let _projects = [];
|
||||
|
||||
// D7: render the archived list for whoever the server says may see one.
|
||||
function renderArchivedProjects(){
|
||||
ProjectData.listArchivedProjects().then(rows => {
|
||||
const sec = $('archived-projects');
|
||||
const list = $('archived-projects-list');
|
||||
if(!sec || !list) return;
|
||||
if(!rows.length){ sec.hidden = true; return; }
|
||||
sec.hidden = false;
|
||||
list.innerHTML = rows.map(p =>
|
||||
`<button type="button" class="card-button" style="display:block; width:100%; text-align:left; margin-bottom:8px"
|
||||
data-open-archived="${esc(p.id)}">
|
||||
${esc(p.name || p.id)} ${p.number ? '· ' + esc(p.number) : ''}
|
||||
<span style="font-size:11px; color:var(--cds-text-secondary)"> — archived, read-only</span>
|
||||
</button>`).join('');
|
||||
list.querySelectorAll('[data-open-archived]').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const p = rows.find(x => x.id === b.dataset.openArchived);
|
||||
if(p){ ProjectData.setActive(p); location.reload(); }
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initProjects(){
|
||||
renderArchivedProjects();
|
||||
ProjectData.list().then(list => {
|
||||
_projects = list || [];
|
||||
// A deep link names the project explicitly, and every other page in the
|
||||
@@ -606,7 +643,12 @@
|
||||
if(active && typeof WPUrl !== 'undefined' && WPUrl.get('project') !== active.id){
|
||||
WPUrl.replace({ project: active.id });
|
||||
}
|
||||
const dropped = (active && !_projects.some(p => p.id === active.id)) ? active : null;
|
||||
// D7: a project OPENED FROM THE ARCHIVED LIST is active on purpose - its
|
||||
// stored summary says archived:true, and only someone the server let see
|
||||
// that list could have stored it. A project archived out from under
|
||||
// someone still drops and gets explained, exactly as before.
|
||||
const dropped = (active && !_projects.some(p => p.id === active.id)
|
||||
&& !active.archived) ? active : null;
|
||||
if(dropped) ProjectData.setActive(null);
|
||||
_listLoaded = true;
|
||||
renderProjectEntry();
|
||||
|
||||
Reference in New Issue
Block a user