Fix profiles losing their selected users
Starting an M365 scan wrote the sidebar state back into the active profile via _save_settings(), replacing its user_ids with the sidebar selection (minus Google-only users and anything hidden by the role filter). With no profile active it overwrote profiles[0]. Profiles are loaders, so scan start no longer writes them; profile_id is audit-only. The profile editor built its account list from S._allUsers only, so manually added accounts, users not yet loaded, and users gone from the directory were not rendered and got dropped on the next save. They now render as checked "Manual" / "Not loaded" rows, and user_ids "all" is kept when saved before users load. Applying a profile cleared the pending user IDs before the un-awaited Google merge had added google:<email> users, leaving them unticked. Pending IDs are now kept until all are matched or the user list is final, and _mergeGoogleUsers() re-applies them. A saved google:<email> also matches an M365 user merged to platform 'both'. Adds regression tests for scan start leaving profiles untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b77bf70ff5
commit
bbe9c62ff9
10 files changed
+146
-17
No files matched your search
+52
-10
@@ -225,13 +225,23 @@ function _applyProfile(profile) {
|
||||
log(t('m365_profile_applied', 'Profile loaded') + ': ' + profile.name);
|
||||
}
|
||||
|
||||
// Google-only users (id "google:<email>") arrive later than M365 users, via the
|
||||
// un-awaited _mergeGoogleUsers(). Keep the pending IDs until every one is
|
||||
// matched or the user list is final, else those users stay unticked.
|
||||
function _userListFinal() {
|
||||
return S._allUsers.length > 0 && (!window._googleConnected || S._googleUsersMerged);
|
||||
}
|
||||
|
||||
function _applyPendingProfileUsers() {
|
||||
const ids = window._pendingProfileUserIds;
|
||||
if (!ids || !ids.length || !S._allUsers.length) return;
|
||||
// Select only the users listed in the profile
|
||||
S._allUsers.forEach(u => { u.selected = ids.includes(u.id); });
|
||||
const wanted = new Set(ids);
|
||||
// A saved google:<email> may since have merged onto an M365 user (platform 'both')
|
||||
const matches = u => wanted.has(u.id) || (u.googleEmail && wanted.has('google:' + u.googleEmail));
|
||||
S._allUsers.forEach(u => { u.selected = matches(u); });
|
||||
renderAccountList();
|
||||
window._pendingProfileUserIds = null;
|
||||
const found = S._allUsers.filter(matches).length;
|
||||
if (found >= wanted.size || _userListFinal()) window._pendingProfileUserIds = null;
|
||||
}
|
||||
|
||||
async function saveCurrentAsProfile() {
|
||||
@@ -365,19 +375,23 @@ function _openEditorForProfile(profile) {
|
||||
const srcCheck = (id) => allSources.includes(id) ? 'checked' : '';
|
||||
|
||||
// Build account list from S._allUsers
|
||||
const savedIds = new Set((profile.user_ids || []).map(u => u.id || u));
|
||||
// If no saved IDs match current users, treat as all-selected (new profile or users changed)
|
||||
const anyMatch = savedIds.size > 0 && S._allUsers.some(u => savedIds.has(u.id));
|
||||
const savedAll = profile.user_ids === 'all';
|
||||
const savedList = Array.isArray(profile.user_ids) ? profile.user_ids : [];
|
||||
const savedIds = new Set(savedList.map(u => u.id || u));
|
||||
const isSaved = u => savedAll || savedIds.has(u.id)
|
||||
|| !!(u.googleEmail && savedIds.has('google:' + u.googleEmail));
|
||||
// Remember "all" so saving before the user list has loaded doesn't turn it into []
|
||||
window._pmgmtSavedAll = savedAll;
|
||||
const accountRows = S._allUsers.map(u => {
|
||||
// Only check if the user was explicitly saved — default to unchecked like the main window
|
||||
const checked = anyMatch && savedIds.has(u.id) ? 'checked' : '';
|
||||
const checked = isSaved(u) ? 'checked' : '';
|
||||
const platBadge = u.platform === 'both' ? '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:linear-gradient(90deg,#E6F1FB 50%,#EAF3DE 50%);color:#1a4a1a;font-weight:500;border:0.5px solid #b5d4b5">M365+GWS</span>'
|
||||
: (u.platform || 'm365') === 'google' ? '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#EAF3DE;color:#3B6D11;font-weight:500">GWS</span>'
|
||||
: '<span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#E6F1FB;color:#185FA5;font-weight:500">M365</span>';
|
||||
const roleBadge = u.userRole === 'student' ? t('role_student','Elev') : u.userRole === 'staff' ? t('role_staff','Ansat') : t('role_other','Anden');
|
||||
const roleOverrideStyle = u.roleOverride ? 'color:var(--color-text-info);outline:1px solid var(--color-border-info);' : '';
|
||||
return `<label class="pmgmt-acct-row" data-uid="${_esc(u.id)}" data-role="${_esc(u.userRole || 'other')}"><input type="checkbox" ${checked} data-uid="${_esc(u.id)}"><span style="flex:1;color:var(--color-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(u.displayName)}</span>${platBadge}<button type="button" class="pmgmt-role-badge" data-uid="${_esc(u.id)}" onclick="_pmgmtCycleRole(this.getAttribute('data-uid'),event)" style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;border:none;cursor:pointer;${roleOverrideStyle}">${roleBadge}</button></label>`;
|
||||
}).join('');
|
||||
}).join('') + _unresolvedAccountRows(savedList);
|
||||
|
||||
body.innerHTML = `
|
||||
<div>
|
||||
@@ -464,6 +478,28 @@ function _openEditorForProfile(profile) {
|
||||
_renderEditorSources((profile.sources || []).concat(profile.google_sources || []).concat(profile.file_sources || []));
|
||||
}
|
||||
|
||||
// Saved accounts that aren't in S._allUsers — manually added ("manual:<email>"),
|
||||
// not loaded yet, or gone from the directory. Rendered as checked rows so the
|
||||
// next save keeps them instead of silently dropping them.
|
||||
function _unresolvedAccountRows(savedList) {
|
||||
const known = new Set();
|
||||
S._allUsers.forEach(u => {
|
||||
known.add(u.id);
|
||||
if (u.googleEmail) known.add('google:' + u.googleEmail);
|
||||
});
|
||||
const seen = new Set();
|
||||
return savedList.map(u => {
|
||||
const id = u.id || u;
|
||||
if (!id || known.has(id) || seen.has(id)) return '';
|
||||
seen.add(id);
|
||||
const isManual = id.startsWith('manual:');
|
||||
const label = (typeof u === 'object' && u.displayName) || id.replace(/^(manual|google):/, '');
|
||||
const badge = isManual ? t('m365_profile_acct_manual', 'Manual')
|
||||
: t('m365_profile_acct_unresolved', 'Not loaded');
|
||||
return `<label class="pmgmt-acct-row" data-uid="${_esc(id)}" data-role="${_esc((typeof u === 'object' && u.userRole) || 'other')}"><input type="checkbox" checked data-uid="${_esc(id)}"><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${_esc(id)}">${_esc(label)}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">${_esc(badge)}</span></label>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function _peSetDate(val) {
|
||||
if (!val) return;
|
||||
const ms = new Date() - new Date(val);
|
||||
@@ -601,7 +637,9 @@ function _pmgmtAddManual() {
|
||||
if (list.querySelector(`input[data-uid="${id}"]`)) return; // already exists
|
||||
const lbl = document.createElement('label');
|
||||
lbl.className = 'pmgmt-acct-row';
|
||||
lbl.innerHTML = `<input type="checkbox" checked data-uid="${_esc(id)}"><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(email.trim())}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">Manuel</span>`;
|
||||
lbl.dataset.uid = id;
|
||||
lbl.dataset.role = 'other';
|
||||
lbl.innerHTML = `<input type="checkbox" checked data-uid="${_esc(id)}"><span style="flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(email.trim())}</span><span style="font-size:9px;padding:1px 5px;border-radius:10px;background:#D3D1C7;color:#444441">${_esc(t('m365_profile_acct_manual', 'Manual'))}</span>`;
|
||||
list.appendChild(lbl);
|
||||
}
|
||||
|
||||
@@ -635,9 +673,11 @@ async function _pmgmtSaveFullEdit() {
|
||||
const effectiveGoogleSources = googleRendered ? googleSources : (profile.google_sources || []);
|
||||
const effectiveFileSources = fileRendered ? fileSources : (profile.file_sources || []);
|
||||
const allSources = m365Sources.concat(effectiveGoogleSources).concat(effectiveFileSources);
|
||||
const user_ids = Array.from(document.querySelectorAll('#pmgmtAcctList input[type=checkbox]:checked'))
|
||||
let user_ids = Array.from(document.querySelectorAll('#pmgmtAcctList input[type=checkbox]:checked'))
|
||||
.map(cb => cb.dataset.uid)
|
||||
.filter(Boolean);
|
||||
// "all" profile opened before users loaded: there were no rows to tick, so keep "all"
|
||||
if (window._pmgmtSavedAll && !S._allUsers.length && !user_ids.length) user_ids = 'all';
|
||||
const updated = {
|
||||
...profile,
|
||||
name,
|
||||
@@ -769,6 +809,8 @@ window._esc = _esc;
|
||||
window._pmgmtUse = _pmgmtUse;
|
||||
window._pmgmtOpenEditor = _pmgmtOpenEditor;
|
||||
window._openEditorForProfile = _openEditorForProfile;
|
||||
window._unresolvedAccountRows = _unresolvedAccountRows;
|
||||
window._userListFinal = _userListFinal;
|
||||
window._peSetDate = _peSetDate;
|
||||
window._peSetYear = _peSetYear;
|
||||
window._renderEditorSources = _renderEditorSources;
|
||||
|
||||
Reference in new issue
Block a user