fix: Keep dangling project references from blanking the home view

`getManagerProjectsModelArray` mapped every project manager to its project
without checking that the project is in the store, so one row pointing at a
deleted project produced a null the caller then read `id` off — taking down
the whole home view. `getMembershipProjectsModelArray` had the same hole one
level up, destructuring `board.project` before testing `board`.

The projects endpoint fed exactly that state: it derived the id list for the
included records from the manager rows, so a `project_manager` row whose
project no longer exists shipped a manager for a project missing from `items`.
Board memberships had the same gap. Both id lists now come from the records
that actually exist.
This commit is contained in:
Daniel Hiller
2026-08-26 09:22:53 +02:00
parent 266246e242
commit 4add7e43af
2 changed files with 18 additions and 6 deletions
+6 -2
View File
@@ -569,10 +569,12 @@ export default class extends BaseModel {
return this.notificationServices.orderBy(['id.length', 'id']); return this.notificationServices.orderBy(['id.length', 'id']);
} }
// A manager or membership row can outlive the project or board it points to,
// and a single dangling reference must not take the whole view down
getManagerProjectsModelArray() { getManagerProjectsModelArray() {
return this.getProjectManagersQuerySet() return this.getProjectManagersQuerySet()
.toModelArray() .toModelArray()
.map(({ project: projectModel }) => projectModel); .flatMap(({ project: projectModel }) => projectModel || []);
} }
getMembershipProjectsModelArray() { getMembershipProjectsModelArray() {
@@ -580,7 +582,9 @@ export default class extends BaseModel {
return this.getBoardMembershipsQuerySet() return this.getBoardMembershipsQuerySet()
.toModelArray() .toModelArray()
.flatMap(({ board: { project: projectModel } }) => { .flatMap(({ board: boardModel }) => {
const projectModel = boardModel && boardModel.project;
if (!projectModel || projectIds.includes(projectModel.id)) { if (!projectModel || projectIds.includes(projectModel.id)) {
return []; return [];
} }
+12 -4
View File
@@ -124,17 +124,25 @@ module.exports = {
true, true,
); );
const projectIds = [...managerProjectIds, ...membershipProjectIds]; const projects = await Project.qm.getByIds([...managerProjectIds, ...membershipProjectIds]);
const projects = await Project.qm.getByIds(projectIds);
if (sharedProjectIds) { if (sharedProjectIds) {
projectIds.push(...sharedProjectIds);
projects.push(...sharedProjects); projects.push(...sharedProjects);
} }
// A manager row can outlive its project, so the ids for everything included
// below come from the projects that actually exist, never from the memberships
const projectIds = sails.helpers.utils.mapRecords(projects);
const fullyVisibleBoards = await Board.qm.getByProjectIds(fullyVisibleProjectIds); const fullyVisibleBoards = await Board.qm.getByProjectIds(fullyVisibleProjectIds);
const boards = [...fullyVisibleBoards, ...membershipBoards]; const boards = [...fullyVisibleBoards, ...membershipBoards];
const boardIdsSet = new Set(sails.helpers.utils.mapRecords(boards));
const availableBoardMemberships = boardMemberships.filter(({ boardId }) =>
boardIdsSet.has(boardId),
);
const projectFavorites = await ProjectFavorite.qm.getByProjectIdsAndUserId( const projectFavorites = await ProjectFavorite.qm.getByProjectIdsAndUserId(
projectIds, projectIds,
currentUser.id, currentUser.id,
@@ -183,7 +191,7 @@ module.exports = {
projectManagers, projectManagers,
baseCustomFieldGroups, baseCustomFieldGroups,
boards, boards,
boardMemberships, boardMemberships: availableBoardMemberships,
customFields, customFields,
notificationServices, notificationServices,
users: sails.helpers.users.presentMany(users, currentUser), users: sails.helpers.users.presentMany(users, currentUser),