fix: use fresh project status and rights after reload

After F5 without projectId in the address the hub and sidebar showed the
status and edit right saved in sessionStorage, so a project switched to
Архивный or a revoked edit0 right was ignored until the project was
picked again. The fresh list from SharePoint now always wins; the saved
copy is used only while the list is loading. Found in T-HUB-07/09.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-16 12:33:38 +05:00
parent 650bce809a
commit 45c974d0c6
3 changed files with 43 additions and 12 deletions
@@ -4,6 +4,7 @@ import { useRouter } from '../routing/RouterContext';
import { IAccessibleProject } from './projectTypes'; import { IAccessibleProject } from './projectTypes';
import { fetchAccessibleProjects } from './projectRepository'; import { fetchAccessibleProjects } from './projectRepository';
import { readStoredProject, storeProject } from './projectSession'; import { readStoredProject, storeProject } from './projectSession';
import { resolveCurrentProject } from './projectRules';
export type ProjectsLoadState = 'loading' | 'ready' | 'error'; export type ProjectsLoadState = 'loading' | 'ready' | 'error';
@@ -51,13 +52,12 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ child
if (isCancelled) return; if (isCancelled) return;
setProjects(loaded); setProjects(loaded);
setLoadState('ready'); setLoadState('ready');
// В сессии могли остаться устаревшие права — обновляем по свежим данным (как в main). // В сессии могли остаться устаревшие статус и права — сохраняем свежие.
setStoredProject((stored) => { setStoredProject((stored) => {
const fresh = stored ? findProject(loaded, stored.id) : undefined; const fresh = stored ? findProject(loaded, stored.id) : undefined;
if (!stored || !fresh) return stored; if (!fresh) return stored;
const updated = { ...stored, canEdit: fresh.canEdit }; storeProject(fresh);
storeProject(updated); return fresh;
return updated;
}); });
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
@@ -71,13 +71,7 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ child
}, [services, reloadKey]); }, [services, reloadKey]);
const routeProjectId = route.projectId ? Number(route.projectId) : undefined; const routeProjectId = route.projectId ? Number(route.projectId) : undefined;
let currentProject: IAccessibleProject | undefined; const currentProject = resolveCurrentProject(projects, routeProjectId, storedProject);
if (routeProjectId === undefined) {
currentProject = storedProject;
} else {
currentProject =
findProject(projects, routeProjectId) ?? (storedProject?.id === routeProjectId ? storedProject : undefined);
}
const currentProjectId = route.projectId ?? (storedProject ? String(storedProject.id) : undefined); const currentProjectId = route.projectId ?? (storedProject ? String(storedProject.id) : undefined);
const selectProject = React.useCallback( const selectProject = React.useCallback(
@@ -5,6 +5,7 @@ import {
isSectionAvailable, isSectionAvailable,
mergeAccessRows, mergeAccessRows,
pickRecentProjectIds, pickRecentProjectIds,
resolveCurrentProject,
sortProjects sortProjects
} from './projectRules'; } from './projectRules';
import { IAccessibleProject } from './projectTypes'; import { IAccessibleProject } from './projectTypes';
@@ -71,6 +72,25 @@ describe('getStatusKind', () => {
}); });
}); });
describe('resolveCurrentProject', () => {
const stored = project({ id: 51, status: 'Текущий', canEdit: true });
const fresh = project({ id: 51, status: 'Архив', canEdit: false });
it('свежие статус и права из списка главнее сохранённых в сессии (баг T-HUB-07/09)', () => {
expect(resolveCurrentProject([fresh], undefined, stored)).toBe(fresh);
expect(resolveCurrentProject([fresh], 51, stored)).toBe(fresh);
});
it('пока список не загрузился — сохранённый выбор', () => {
expect(resolveCurrentProject([], undefined, stored)).toBe(stored);
});
it('проект из адреса, которого нет ни в списке, ни в сессии, — не выбран', () => {
expect(resolveCurrentProject([fresh], 7, stored)).toBeUndefined();
expect(resolveCurrentProject([], undefined, undefined)).toBeUndefined();
});
});
describe('isSectionAvailable', () => { describe('isSectionAvailable', () => {
it('архивный проект — закрыты все разделы, кроме главной', () => { it('архивный проект — закрыты все разделы, кроме главной', () => {
const archived = project({ status: 'архивный' }); const archived = project({ status: 'архивный' });
@@ -85,6 +85,23 @@ export function isSectionAvailable(page: PageId, project: IAccessibleProject): b
return true; return true;
} }
/**
* Текущий проект: id из адреса, а без него — сохранённый в сессии выбор.
* Данные (статус, права) берутся из свежего списка с сервера; сохранённая копия — только пока список
* не загрузился или если проекта в нём нет. Иначе после смены статуса / прав F5 показывал бы старые.
*/
export function resolveCurrentProject(
projects: IAccessibleProject[],
routeProjectId: number | undefined,
storedProject: IAccessibleProject | undefined
): IAccessibleProject | undefined {
const targetId = routeProjectId ?? storedProject?.id;
if (targetId === undefined) return undefined;
const fresh = projects.filter((project) => project.id === targetId)[0];
if (fresh) return fresh;
return storedProject?.id === targetId ? storedProject : undefined;
}
/** Выгрузка за период недоступна только для архивного проекта. */ /** Выгрузка за период недоступна только для архивного проекта. */
export function isExportAvailable(project: IAccessibleProject): boolean { export function isExportAvailable(project: IAccessibleProject): boolean {
return getStatusKind(project.status) !== 'archive'; return getStatusKind(project.status) !== 'archive';