projectTree.ts 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. import type { ProjectListItem } from '../api/client';
  2. /**
  3. * Projects that may legally become `projectId`'s parent (#1264).
  4. *
  5. * Its own descendants are excluded as well as itself: nesting a project under
  6. * something already beneath it makes a cycle, which the API rejects anyway, so
  7. * offering it would only produce an error the user cannot act on. Walked from
  8. * the flat list rather than fetched, since every row carries its `parent_id`.
  9. */
  10. export function eligibleParents(
  11. projects: ProjectListItem[],
  12. projectId: number | undefined,
  13. ): ProjectListItem[] {
  14. if (projectId === undefined) return projects;
  15. const blocked = new Set([projectId]);
  16. // Repeat until nothing new is blocked: the list is in no particular order, so
  17. // a grandchild can appear before its parent has been blocked.
  18. let grew = true;
  19. while (grew) {
  20. grew = false;
  21. for (const candidate of projects) {
  22. if (candidate.parent_id !== null && blocked.has(candidate.parent_id) && !blocked.has(candidate.id)) {
  23. blocked.add(candidate.id);
  24. grew = true;
  25. }
  26. }
  27. }
  28. return projects.filter((p) => !blocked.has(p.id));
  29. }
  30. /**
  31. * Projects a picker should offer when filing something away (#2888).
  32. *
  33. * An archived project is one its owner has explicitly put out of the way, so
  34. * leaving it in a picker only lengthens a list they then have to search --
  35. * the reporter had five active projects behind thirty-odd finished ones.
  36. * Completed projects stay: filing a reprint against a finished project is
  37. * ordinary, and "completed" says the work is done, not that it should be
  38. * hidden.
  39. *
  40. * `keepId` names one project that survives whatever its status -- the one the
  41. * thing being edited already belongs to. Without it a controlled `<select>`
  42. * holds a value no option matches, and the browser resets it to the first
  43. * option, which here is "No project": an archive filed in an archived project
  44. * would state, in as many words, that it is filed nowhere.
  45. */
  46. export function assignableProjects(
  47. projects: ProjectListItem[],
  48. keepId?: number | null,
  49. ): ProjectListItem[] {
  50. return projects.filter((p) => p.status !== 'archived' || p.id === keepId);
  51. }