Окно создания восстанавливается после F5 вместе с черновиком
usePersistedOpen хранит в localStorage признак «модалка создания открыта» для типов заявок, доступа к заявкам, маршрутов и шагов маршрута (ключ шага — по id маршрута). После F5 окно открывается снова с полями из черновика; любое закрытие сбрасывает признак, а сам черновик остаётся до сохранения или «Очистить». Редактирование не восстанавливается. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
|
||||
import { useFormDraft } from '../../hooks/useFormDraft';
|
||||
import { usePersistedOpen } from '../../hooks/usePersistedOpen';
|
||||
import { toEmployeeOption } from '../../utils/employee';
|
||||
import { replaceRoute } from '../../routing';
|
||||
import { RequestTypeAccess } from '../../models';
|
||||
@@ -44,6 +45,7 @@ const ACCESS_FILTER_EMAIL_STORAGE_KEY = 'approvalAdmin.accessFilterEmail';
|
||||
|
||||
/** Черновик формы выдачи доступа — переживает закрытие модалки, см. useFormDraft. */
|
||||
const CREATE_DRAFT_STORAGE_KEY = 'approvalAdmin.draft.requestTypeAccess';
|
||||
const CREATE_OPEN_STORAGE_KEY = 'approvalAdmin.draftOpen.requestTypeAccess';
|
||||
|
||||
interface IFormState {
|
||||
requestTypeId: string;
|
||||
@@ -62,7 +64,7 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
||||
const access = useRequestTypeAccess(api);
|
||||
const employees = usePortalEmployees(dictionary);
|
||||
|
||||
const [showForm, setShowForm] = React.useState(false);
|
||||
const createOpen = usePersistedOpen(CREATE_OPEN_STORAGE_KEY);
|
||||
const draft = useFormDraft<IFormState>(CREATE_DRAFT_STORAGE_KEY, EMPTY_FORM);
|
||||
const { requestTypeId, emails } = draft.value;
|
||||
const setEmails = (update: (current: string[]) => string[]): void =>
|
||||
@@ -109,7 +111,7 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
||||
: item.sharePointLists.map(l => l.title ?? l.listId).join(', ');
|
||||
|
||||
const openCreate = (): void => {
|
||||
setShowForm(true);
|
||||
createOpen.open();
|
||||
};
|
||||
|
||||
const submitForm = (): void => {
|
||||
@@ -140,7 +142,7 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
||||
if (failedEmails.length < emails.length) access.reload();
|
||||
if (failures.length === 0) {
|
||||
draft.reset();
|
||||
setShowForm(false);
|
||||
createOpen.close();
|
||||
return;
|
||||
}
|
||||
setEmails(() => failedEmails);
|
||||
@@ -240,13 +242,13 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
||||
}
|
||||
/>
|
||||
|
||||
{showForm && (
|
||||
{createOpen.isOpen && (
|
||||
<FormModal
|
||||
title={texts.requestTypeAccess.createTitle}
|
||||
error={submit.error}
|
||||
isSaving={submit.isBusy}
|
||||
onSubmit={submitForm}
|
||||
onClose={() => setShowForm(false)}
|
||||
onClose={createOpen.close}
|
||||
onReset={draft.reset}
|
||||
canReset={draft.isDirty}
|
||||
>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useSharePointListTitle } from '../../hooks/useSharePointListTitle';
|
||||
import { useAsyncSubmit } from '../../hooks/useAsyncSubmit';
|
||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||
import { useFormDraft } from '../../hooks/useFormDraft';
|
||||
import { usePersistedOpen } from '../../hooks/usePersistedOpen';
|
||||
import {
|
||||
ActionsCell,
|
||||
Alert,
|
||||
@@ -60,6 +61,7 @@ const EMPTY_FORM: IFormState = {
|
||||
|
||||
/** Черновик формы создания — переживает закрытие модалки, см. useFormDraft. */
|
||||
const CREATE_DRAFT_STORAGE_KEY = 'approvalAdmin.draft.requestType';
|
||||
const CREATE_OPEN_STORAGE_KEY = 'approvalAdmin.draftOpen.requestType';
|
||||
|
||||
function toFormState(item: RequestType): IFormState {
|
||||
return {
|
||||
@@ -94,7 +96,13 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
const list = useRequestTypes(api);
|
||||
|
||||
const [editing, setEditing] = React.useState<RequestType | undefined>();
|
||||
// showForm — только редактирование; окно создания восстанавливается после F5 (usePersistedOpen).
|
||||
const [showForm, setShowForm] = React.useState(false);
|
||||
const createOpen = usePersistedOpen(CREATE_OPEN_STORAGE_KEY);
|
||||
const closeForm = (): void => {
|
||||
setShowForm(false);
|
||||
createOpen.close();
|
||||
};
|
||||
const draft = useFormDraft<IFormState>(CREATE_DRAFT_STORAGE_KEY, EMPTY_FORM);
|
||||
const [editForm, setEditForm] = React.useState<IFormState>(EMPTY_FORM);
|
||||
const form = editing ? editForm : draft.value;
|
||||
@@ -112,12 +120,14 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
|
||||
const openCreate = (): void => {
|
||||
setEditing(undefined);
|
||||
setShowForm(true);
|
||||
setShowForm(false);
|
||||
createOpen.open();
|
||||
};
|
||||
|
||||
const openEdit = (item: RequestType): void => {
|
||||
setEditing(item);
|
||||
setEditForm(toFormState(item));
|
||||
createOpen.close();
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
@@ -155,7 +165,7 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
submit.run(() =>
|
||||
request.then(() => {
|
||||
if (!editing) draft.reset();
|
||||
setShowForm(false);
|
||||
closeForm();
|
||||
list.reload();
|
||||
})
|
||||
);
|
||||
@@ -204,13 +214,13 @@ export const RequestTypesScreen: React.FC<IRequestTypesScreenProps> = ({ api, sh
|
||||
emptyMessage={list.isLoading ? texts.common.loading : texts.requestTypes.emptyList}
|
||||
/>
|
||||
|
||||
{showForm && (
|
||||
{(showForm || createOpen.isOpen) && (
|
||||
<FormModal
|
||||
title={editing ? texts.requestTypes.editTitle : texts.requestTypes.createTitle}
|
||||
error={submit.error}
|
||||
isSaving={submit.isBusy}
|
||||
onSubmit={submitForm}
|
||||
onClose={() => setShowForm(false)}
|
||||
onClose={closeForm}
|
||||
onReset={editing ? undefined : draft.reset}
|
||||
canReset={draft.isDirty}
|
||||
isDirty={isEditDirty}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { blockIndexOf, blockRanges, markParallel, withOrdersFrom } from '../../u
|
||||
import { reportError } from '../../utils/errors';
|
||||
import { useRouteSteps } from '../../hooks/useRouteSteps';
|
||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||
import { usePersistedOpen } from '../../hooks/usePersistedOpen';
|
||||
import { usePortalEmployees } from '../../hooks/usePortalEmployees';
|
||||
import { usePortalDictionaries } from '../../hooks/usePortalDictionaries';
|
||||
import {
|
||||
@@ -53,7 +54,14 @@ export const RouteStepsScreen: React.FC<IRouteStepsScreenProps> = ({ api, route,
|
||||
const requestTypeName = requestType?.name ?? `#${route.requestTypeId}`;
|
||||
|
||||
const [editing, setEditing] = React.useState<TemplateRouteStep | undefined>();
|
||||
// showForm — только редактирование; окно создания восстанавливается после F5 (usePersistedOpen),
|
||||
// ключ свой у каждого маршрута — как и у черновика (см. RouteStepFormModal).
|
||||
const [showForm, setShowForm] = React.useState(false);
|
||||
const createOpen = usePersistedOpen(`approvalAdmin.draftOpen.routeStep.${route.id}`);
|
||||
const closeForm = (): void => {
|
||||
setShowForm(false);
|
||||
createOpen.close();
|
||||
};
|
||||
|
||||
const deleteTarget = useDeleteTarget<TemplateRouteStep>(
|
||||
item => api.templateRouteSteps.remove(route.id, item.id),
|
||||
@@ -71,11 +79,13 @@ export const RouteStepsScreen: React.FC<IRouteStepsScreenProps> = ({ api, route,
|
||||
|
||||
const openCreate = (): void => {
|
||||
setEditing(undefined);
|
||||
setShowForm(true);
|
||||
setShowForm(false);
|
||||
createOpen.open();
|
||||
};
|
||||
|
||||
const openEdit = (item: TemplateRouteStep): void => {
|
||||
setEditing(item);
|
||||
createOpen.close();
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
@@ -278,7 +288,7 @@ export const RouteStepsScreen: React.FC<IRouteStepsScreenProps> = ({ api, route,
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
{(showForm || createOpen.isOpen) && (
|
||||
<RouteStepFormModal
|
||||
// Undefined, если у маршрута компания не задана (запасной маршрут,
|
||||
// применяется к любой компании инициатора) — тогда подсказка
|
||||
@@ -293,7 +303,7 @@ export const RouteStepsScreen: React.FC<IRouteStepsScreenProps> = ({ api, route,
|
||||
employees={employees.items}
|
||||
employeesLoading={employees.isLoading}
|
||||
onSaved={handleSaved}
|
||||
onClose={() => setShowForm(false)}
|
||||
onClose={closeForm}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useRequestTypes } from '../../hooks/useRequestTypes';
|
||||
import { useTemplateRoutes } from '../../hooks/useTemplateRoutes';
|
||||
import { usePortalDictionaries } from '../../hooks/usePortalDictionaries';
|
||||
import { useDeleteTarget } from '../../hooks/useDeleteTarget';
|
||||
import { usePersistedOpen } from '../../hooks/usePersistedOpen';
|
||||
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
|
||||
import { replaceRoute } from '../../routing';
|
||||
import {
|
||||
@@ -39,6 +40,8 @@ export interface IRoutesScreenProps {
|
||||
// Фильтр по типу заявки переживает перезагрузку: администратор обычно работает
|
||||
// с одним типом заявок подряд, и сбрасывать выбор на каждый заход неудобно.
|
||||
const ROUTES_FILTER_STORAGE_KEY = 'approvalAdmin.routesFilter';
|
||||
/** Окно создания маршрута переживает F5 — поля подставляются из черновика (см. RouteFormModal). */
|
||||
const CREATE_OPEN_STORAGE_KEY = 'approvalAdmin.draftOpen.route';
|
||||
|
||||
export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
|
||||
api,
|
||||
@@ -69,7 +72,13 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
|
||||
const routes = useTemplateRoutes(api, filterRequestTypeId ? Number(filterRequestTypeId) : undefined);
|
||||
|
||||
const [editing, setEditing] = React.useState<TemplateRoute | undefined>();
|
||||
// showForm — только редактирование; окно создания восстанавливается после F5 (usePersistedOpen).
|
||||
const [showForm, setShowForm] = React.useState(false);
|
||||
const createOpen = usePersistedOpen(CREATE_OPEN_STORAGE_KEY);
|
||||
const closeForm = (): void => {
|
||||
setShowForm(false);
|
||||
createOpen.close();
|
||||
};
|
||||
const [copying, setCopying] = React.useState<TemplateRoute | undefined>();
|
||||
|
||||
const deleteTarget = useDeleteTarget<TemplateRoute>(item => api.templateRoutes.remove(item.id), routes.reload);
|
||||
@@ -83,11 +92,13 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
|
||||
|
||||
const openCreate = (): void => {
|
||||
setEditing(undefined);
|
||||
setShowForm(true);
|
||||
setShowForm(false);
|
||||
createOpen.open();
|
||||
};
|
||||
|
||||
const openEdit = (item: TemplateRoute): void => {
|
||||
setEditing(item);
|
||||
createOpen.close();
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
@@ -159,7 +170,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
|
||||
emptyMessage={routes.isLoading ? texts.common.loading : texts.routes.emptyList}
|
||||
/>
|
||||
|
||||
{showForm && (
|
||||
{(showForm || createOpen.isOpen) && (
|
||||
<RouteFormModal
|
||||
api={api}
|
||||
editing={editing}
|
||||
@@ -169,7 +180,7 @@ export const RoutesScreen: React.FC<IRoutesScreenProps> = ({
|
||||
departments={portalDictionaries.departments}
|
||||
dictionaryLoading={portalDictionaries.isLoading}
|
||||
onSaved={routes.reload}
|
||||
onClose={() => setShowForm(false)}
|
||||
onClose={closeForm}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ describe('coerceStored', () => {
|
||||
expect(coerceStored<number[]>([], [1, 2])).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('для boolean-флага отбрасывает значение другого типа', () => {
|
||||
expect(coerceStored(false, true)).toBe(true);
|
||||
expect(coerceStored(false, 'true')).toBe(false);
|
||||
});
|
||||
|
||||
it('при initial === undefined принимает сохранённое как есть', () => {
|
||||
expect(coerceStored<string | undefined>(undefined, 'a')).toBe('a');
|
||||
expect(coerceStored({ x: undefined as string | undefined }, { x: 'a' })).toEqual({ x: 'a' });
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useLocalStorageState } from './useLocalStorageState';
|
||||
|
||||
export interface IPersistedOpen {
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Признак «модалка создания открыта», переживающий F5: после перезагрузки окно
|
||||
* открывается снова, а поля подставляются из черновика (см. useFormDraft).
|
||||
* close() сбрасывает только признак — черновик остаётся до сохранения или «Очистить».
|
||||
*/
|
||||
export function usePersistedOpen(key: string): IPersistedOpen {
|
||||
const [isOpen, setIsOpen] = useLocalStorageState<boolean>(key, false);
|
||||
return { isOpen, open: () => setIsOpen(true), close: () => setIsOpen(false) };
|
||||
}
|
||||
Reference in New Issue
Block a user