Карточка: связь вне зеркала ведёт в Jira, «Назад» у окна 404, метки видов работ отдельно, пустое описание (#124, #42 M-5, #103, #56 M-4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
943763c774
commit
7b297fef23
@ -73,6 +73,34 @@ describe("карточка задачи", () => {
|
|||||||
expect(screen.getByRole("link", { name: "AURORA-9002" }).closest(".issue")).not.toBeNull(); // O-10: цвет темы
|
expect(screen.getByRole("link", { name: "AURORA-9002" }).closest(".issue")).not.toBeNull(); // O-10: цвет темы
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("связь на ключ вне зеркала ведёт в Jira, а не в карточку (#103)", async () => {
|
||||||
|
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", {
|
||||||
|
links_out: [{ target_key: "AURORA-1999", link_type: "ASUPR_Связанные", summary: null, status: null }], links_in: [],
|
||||||
|
})));
|
||||||
|
open("/jira/issues/AURORA-9001");
|
||||||
|
const link = await screen.findByRole("link", { name: /AURORA-1999/ });
|
||||||
|
expect(link).toHaveAttribute("href", "https://sdlc.go.rshbank.ru/jira/browse/AURORA-1999");
|
||||||
|
expect(link).toHaveAttribute("target", "_blank");
|
||||||
|
expect(link.closest("li")).toHaveTextContent("нет в зеркале");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("«Прочие поля»: вид работы отдельно от меток (#42 M-5)", async () => {
|
||||||
|
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { labels: ["AWPCA", "kind-dev"] })));
|
||||||
|
open("/jira/issues/AURORA-9001");
|
||||||
|
await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9001" });
|
||||||
|
const other = screen.getByRole("region", { name: "Прочие поля" });
|
||||||
|
expect(within(other).getByText("Вид работы в Jira").nextSibling).toHaveTextContent("разработка");
|
||||||
|
expect(within(other).getByText("Метки").nextSibling).toHaveTextContent("AWPCA");
|
||||||
|
expect(within(other).queryByText(/kind-dev/)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("пустое описание (одни пробелы) — без раздела «Описание» (#56 M-4)", async () => {
|
||||||
|
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { description: " \n\n" })));
|
||||||
|
open("/jira/issues/AURORA-9001");
|
||||||
|
await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9001" });
|
||||||
|
expect(screen.queryByRole("heading", { name: "Описание" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("без связей — «связей нет»", async () => {
|
it("без связей — «связей нет»", async () => {
|
||||||
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { links_out: [], links_in: [] })));
|
mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { links_out: [], links_in: [] })));
|
||||||
open("/jira/issues/AURORA-9001");
|
open("/jira/issues/AURORA-9001");
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import type { ChangeEvent, IssueAttachment, IssueComment, IssueDetail } from "..
|
|||||||
import { JIRA_URL } from "../board/constants.ts";
|
import { JIRA_URL } from "../board/constants.ts";
|
||||||
import { fmtDate, fmtStamp } from "../../../ui/format.ts";
|
import { fmtDate, fmtStamp } from "../../../ui/format.ts";
|
||||||
import { EventList } from "../feed/EventList.tsx";
|
import { EventList } from "../feed/EventList.tsx";
|
||||||
import { groupLinks, isOverdue, todayISO } from "./card.ts";
|
import { groupLinks, isOverdue, splitKindLabels, todayISO } from "./card.ts";
|
||||||
import { useCommentsHidden } from "./commentsPref.ts";
|
import { useCommentsHidden } from "./commentsPref.ts";
|
||||||
import { IssueLink } from "./IssueLink.tsx";
|
import { IssueLink } from "./IssueLink.tsx";
|
||||||
import { Badge, Button, IconButton, IconClose, IconExternal, Notice, Panel } from "../../../ui/index.ts";
|
import { Badge, Button, IconButton, IconClose, IconExternal, Notice, Panel } from "../../../ui/index.ts";
|
||||||
@ -22,26 +22,27 @@ import "./issues.css";
|
|||||||
|
|
||||||
const DESC_LINES = 8;
|
const DESC_LINES = 8;
|
||||||
|
|
||||||
export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClose: () => void }) {
|
export function IssueCard({ issueKey: key, onClose, onBack }: { issueKey: string; onClose: () => void; onBack?: () => void }) {
|
||||||
const issue = useIssue(key);
|
const issue = useIssue(key);
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const closed = useClosedStatuses();
|
const closed = useClosedStatuses();
|
||||||
const dl = useAttachmentDownload(key); // одно состояние на вложение для всех кнопок карточки
|
const dl = useAttachmentDownload(key); // одно состояние на вложение для всех кнопок карточки
|
||||||
const close = <IconButton label="Закрыть" icon={<IconClose />} onClick={onClose} />;
|
const close = <IconButton label="Закрыть" icon={<IconClose />} onClick={onClose} />;
|
||||||
|
const back = onBack && <Button onClick={onBack}>Назад</Button>;
|
||||||
if (issue.isPending) {
|
if (issue.isPending) {
|
||||||
return <div className="issue" data-testid="issue-card"><div className="issue-head"><div className="row1"><span className="key">{key}</span><span className="spacer" />{close}</div><p className="hint">загрузка {key}…</p></div></div>;
|
return <div className="issue" data-testid="issue-card"><div className="issue-head"><div className="row1"><span className="key">{key}</span><span className="spacer" />{close}</div><p className="hint">загрузка {key}…</p></div></div>;
|
||||||
}
|
}
|
||||||
if (issue.error instanceof ApiError && issue.error.status === 404) {
|
if (issue.error instanceof ApiError && issue.error.status === 404) {
|
||||||
return (
|
return (
|
||||||
<div className="issue issue--state" data-testid="issue-card">
|
<div className="issue issue--state" data-testid="issue-card">
|
||||||
<Notice tone="info" action={<Button onClick={onClose}>Закрыть</Button>}>Задачи {key} нет в зеркале хаба.</Notice>
|
<Notice tone="info" action={<>{back}<Button onClick={onClose}>Закрыть</Button></>}>Задачи {key} нет в зеркале хаба.</Notice>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (issue.isError && !issue.data) {
|
if (issue.isError && !issue.data) {
|
||||||
return (
|
return (
|
||||||
<div className="issue issue--state" data-testid="issue-card">
|
<div className="issue issue--state" data-testid="issue-card">
|
||||||
<Notice tone="error" action={<><Button onClick={() => void issue.refetch()}>Повторить</Button><Button onClick={onClose}>Закрыть</Button></>}>
|
<Notice tone="error" action={<><Button onClick={() => void issue.refetch()}>Повторить</Button>{back}<Button onClick={onClose}>Закрыть</Button></>}>
|
||||||
{issue.error.message}
|
{issue.error.message}
|
||||||
</Notice>
|
</Notice>
|
||||||
</div>
|
</div>
|
||||||
@ -76,14 +77,14 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos
|
|||||||
<div className="cols">
|
<div className="cols">
|
||||||
<div className="main">
|
<div className="main">
|
||||||
<Links issue={i} linkTypes={(settings.data?.link_types ?? {}) as Record<string, string>} />
|
<Links issue={i} linkTypes={(settings.data?.link_types ?? {}) as Record<string, string>} />
|
||||||
{i.description && <Description text={i.description} projectKey={settings.data?.project_key} wiki={wiki} />}
|
{i.description?.trim() && <Description text={i.description} projectKey={settings.data?.project_key} wiki={wiki} />}
|
||||||
{atts && atts.length > 0 && <AttachmentList items={atts} dl={dl} />}
|
{atts && atts.length > 0 && <AttachmentList items={atts} dl={dl} />}
|
||||||
<Comments items={i.comments ?? []} projectKey={settings.data?.project_key} wiki={wiki} atts={atts ?? []} dl={dl} />
|
<Comments items={i.comments ?? []} projectKey={settings.data?.project_key} wiki={wiki} atts={atts ?? []} dl={dl} />
|
||||||
{i.changes && <History items={i.changes} />}
|
{i.changes && <History items={i.changes} />}
|
||||||
</div>
|
</div>
|
||||||
<Panel className="side">
|
<Panel className="side">
|
||||||
<Notes key={i.key} issue={i} />
|
<Notes key={i.key} issue={i} />
|
||||||
<OtherFields issue={i} />
|
<OtherFields issue={i} kindLabels={settings.data?.kind_labels as Record<string, string> | undefined} />
|
||||||
<Commands issue={i} />
|
<Commands issue={i} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
@ -107,6 +108,8 @@ function wikiAttachments(atts: IssueAttachment[] | undefined, dl: Download): Wik
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const absent = (l: { summary: string | null; status: string | null }) => l.summary === null && l.status === null;
|
||||||
|
|
||||||
function Links({ issue, linkTypes }: { issue: IssueDetail; linkTypes: Record<string, string> }) {
|
function Links({ issue, linkTypes }: { issue: IssueDetail; linkTypes: Record<string, string> }) {
|
||||||
const groups = groupLinks(issue, linkTypes);
|
const groups = groupLinks(issue, linkTypes);
|
||||||
return (
|
return (
|
||||||
@ -120,8 +123,10 @@ function Links({ issue, linkTypes }: { issue: IssueDetail; linkTypes: Record<str
|
|||||||
{links.map((l, n) => (
|
{links.map((l, n) => (
|
||||||
<li key={`${n}:${l.key}:${l.tag ?? ""}`}>
|
<li key={`${n}:${l.key}:${l.tag ?? ""}`}>
|
||||||
{l.tag && <><span className="tag">{l.tag}</span>{" "}</>}
|
{l.tag && <><span className="tag">{l.tag}</span>{" "}</>}
|
||||||
<IssueLink issueKey={l.key} />{" "}
|
{absent(l)
|
||||||
{l.summary === null && l.status === null
|
? <a href={`${JIRA_URL}/browse/${l.key}`} target="_blank" rel="noopener">{l.key}<IconExternal size={12} /></a>
|
||||||
|
: <IssueLink issueKey={l.key} />}{" "}
|
||||||
|
{absent(l)
|
||||||
? <span className="sum absent">нет в зеркале</span>
|
? <span className="sum absent">нет в зеркале</span>
|
||||||
: <><span className="sum">{l.summary}</span>{l.status && <>{" "}<Badge>{l.status}</Badge></>}</>}
|
: <><span className="sum">{l.summary}</span>{l.status && <>{" "}<Badge>{l.status}</Badge></>}</>}
|
||||||
</li>
|
</li>
|
||||||
@ -222,9 +227,11 @@ function History({ items }: { items: ChangeEvent[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OtherFields({ issue: i }: { issue: IssueDetail }) {
|
function OtherFields({ issue: i, kindLabels }: { issue: IssueDetail; kindLabels?: Record<string, string> }) {
|
||||||
|
const { kinds, rest } = splitKindLabels(i.labels, kindLabels);
|
||||||
const rows: [string, string | null][] = [
|
const rows: [string, string | null][] = [
|
||||||
["Тип", i.issue_type], ["Приоритет", i.priority], ["Автор", i.reporter], ["Метки", i.labels.join(", ") || null],
|
["Тип", i.issue_type], ["Приоритет", i.priority], ["Автор", i.reporter],
|
||||||
|
["Вид работы в Jira", kinds.join(", ") || null], ["Метки", rest.join(", ") || null],
|
||||||
["Плановое начало", i.planned_start], ["Обновлена в Jira", i.jira_updated ? fmtStamp(i.jira_updated) : null],
|
["Плановое начало", i.planned_start], ["Обновлена в Jira", i.jira_updated ? fmtStamp(i.jira_updated) : null],
|
||||||
["Сосед по связям", i.neighbor ? "да" : null],
|
["Сосед по связям", i.neighbor ? "да" : null],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -168,4 +168,63 @@ describe("карточка окном", () => {
|
|||||||
render(<App router={at(["/app/jira/issues/AURORA-2"])} />);
|
render(<App router={at(["/app/jira/issues/AURORA-2"])} />);
|
||||||
expect(await screen.findByRole("dialog", { name: "AURORA-2" })).toBeInTheDocument();
|
expect(await screen.findByRole("dialog", { name: "AURORA-2" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("задача не в зеркале после перехода (#103)", () => {
|
||||||
|
const mirror = () => mockFetch((_m, url) => {
|
||||||
|
const p = url.pathname;
|
||||||
|
if (p === "/issues/AURORA-1999") return json(404, {});
|
||||||
|
if (p === "/issues/AURORA-9002") return json(200, issue("AURORA-9002", {
|
||||||
|
links_out: [{ target_key: "AURORA-1999", link_type: "ASUPR_Связанные", summary: "Тема", status: "Новая" }], links_in: [],
|
||||||
|
}));
|
||||||
|
if (p === "/settings") return json(200, settings);
|
||||||
|
if (p === "/projects") return json(200, { projects: [{ label: "AWPCA", name: "АРМ КА", board_id: null, board_type: null, created_at: "", issues_count: 9 }] });
|
||||||
|
if (p === "/issues") return json(200, { issues: rows });
|
||||||
|
if (p === "/batches") return json(200, { batches: [] });
|
||||||
|
return json(200, {});
|
||||||
|
});
|
||||||
|
const msg = "Задачи AURORA-1999 нет в зеркале хаба.";
|
||||||
|
|
||||||
|
it("«Назад» возвращает прежнюю карточку; при новом переходе «Закрыть» закрывает окно", async () => {
|
||||||
|
mirror();
|
||||||
|
const router = at(["/app/jira/issues"]);
|
||||||
|
render(<App router={router} />);
|
||||||
|
await userEvent.click(await screen.findByRole("link", { name: "AURORA-9002" }));
|
||||||
|
await userEvent.click(await within(dialog()).findByRole("link", { name: "AURORA-1999" }));
|
||||||
|
expect(await within(dialog()).findByText(msg)).toBeInTheDocument();
|
||||||
|
await userEvent.click(within(dialog()).getByRole("button", { name: "Назад" }));
|
||||||
|
expect(await within(dialog()).findByRole("heading", { name: /AURORA-9002/ })).toBeInTheDocument();
|
||||||
|
await userEvent.click(within(dialog()).getByRole("link", { name: "AURORA-1999" }));
|
||||||
|
await within(dialog()).findByText(msg);
|
||||||
|
await userEvent.click(within(dialog()).getByRole("button", { name: "Закрыть" }));
|
||||||
|
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||||
|
expect(router.state.location.pathname).toBe("/app/jira/issues");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("прямой адрес 404 — только «Закрыть»", async () => {
|
||||||
|
mirror();
|
||||||
|
render(<App router={at(["/app/jira/issues/AURORA-1999"])} />);
|
||||||
|
expect(await within(await screen.findByRole("dialog")).findByText(msg)).toBeInTheDocument();
|
||||||
|
expect(within(dialog()).getByRole("button", { name: "Закрыть" })).toBeInTheDocument();
|
||||||
|
expect(within(dialog()).queryByRole("button", { name: "Назад" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("«Назад» есть только после перехода: из списка — нет; из прямого адреса после перехода — есть", async () => {
|
||||||
|
mirror();
|
||||||
|
render(<App router={at(["/app/jira/issues"])} />);
|
||||||
|
await userEvent.click(await screen.findByRole("link", { name: "AURORA-9002" }));
|
||||||
|
await within(dialog()).findByRole("heading", { name: /AURORA-9002/ });
|
||||||
|
expect(within(dialog()).queryByRole("button", { name: "Назад" })).toBeNull();
|
||||||
|
await userEvent.click(within(dialog()).getByRole("link", { name: "AURORA-1999" }));
|
||||||
|
await within(dialog()).findByText(msg);
|
||||||
|
expect(within(dialog()).getByRole("button", { name: "Назад" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("переход из прямого адреса — «Назад» есть", async () => {
|
||||||
|
mirror();
|
||||||
|
render(<App router={at(["/app/jira/issues/AURORA-9002"])} />);
|
||||||
|
await userEvent.click(await within(await screen.findByRole("dialog")).findByRole("link", { name: "AURORA-1999" }));
|
||||||
|
await within(dialog()).findByText(msg);
|
||||||
|
expect(within(dialog()).getByRole("button", { name: "Назад" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { IssueCard } from "./IssueCard.tsx";
|
|||||||
// окно карточки (design 13): <dialog> держит фокус и делает фон недоступным; Esc и щелчок по фону — onClose.
|
// окно карточки (design 13): <dialog> держит фокус и делает фон недоступным; Esc и щелчок по фону — onClose.
|
||||||
// Сами окно не закрываем: оно исчезает вместе с адресом карточки. Если браузер закрыл его сам (Esc без активации
|
// Сами окно не закрываем: оно исчезает вместе с адресом карточки. Если браузер закрыл его сам (Esc без активации
|
||||||
// пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок).
|
// пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок).
|
||||||
export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: () => void }) {
|
export function IssueModal({ issueKey, onClose, onBack }: { issueKey: string; onClose: () => void; onBack?: () => void }) {
|
||||||
// I-2: закрывать щелчком по фону, только если и нажатие было на фоне — выделение текста, отпущенное за краем окна,
|
// I-2: закрывать щелчком по фону, только если и нажатие было на фоне — выделение текста, отпущенное за краем окна,
|
||||||
// даёт click на самом <dialog>, но нажатие было внутри содержимого
|
// даёт click на самом <dialog>, но нажатие было внутри содержимого
|
||||||
const downOnBackdrop = useRef(false);
|
const downOnBackdrop = useRef(false);
|
||||||
@ -26,7 +26,7 @@ export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: (
|
|||||||
downOnBackdrop.current = false;
|
downOnBackdrop.current = false;
|
||||||
if (e.target === e.currentTarget && fromBackdrop) onClose();
|
if (e.target === e.currentTarget && fromBackdrop) onClose();
|
||||||
}}>
|
}}>
|
||||||
<ErrorBoundary key={issueKey}><IssueCard issueKey={issueKey} onClose={onClose} /></ErrorBoundary>
|
<ErrorBoundary key={issueKey}><IssueCard issueKey={issueKey} onClose={onClose} onBack={onBack} /></ErrorBoundary>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import settings from "../board/__fixtures__/settings.json";
|
import settings from "../board/__fixtures__/settings.json";
|
||||||
import { groupLinks, isOverdue, todayISO } from "./card.ts";
|
import { groupLinks, isOverdue, splitKindLabels, todayISO } from "./card.ts";
|
||||||
|
|
||||||
const types = settings.link_types as Record<string, string>;
|
const types = settings.link_types as Record<string, string>;
|
||||||
const closed = new Set(["Готово", "Снято"]);
|
const closed = new Set(["Готово", "Снято"]);
|
||||||
@ -50,3 +50,13 @@ describe("просрочка", () => {
|
|||||||
expect(todayISO(new Date(2026, 0, 5, 23, 30))).toBe("2026-01-05");
|
expect(todayISO(new Date(2026, 0, 5, 23, 30))).toBe("2026-01-05");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("splitKindLabels", () => {
|
||||||
|
it("метки видов работ отдельно от прочих", () => {
|
||||||
|
expect(splitKindLabels(["AWPCA", "kind-dev", "kind-ui"], { разработка: "kind-dev", UI: "kind-ui" }))
|
||||||
|
.toEqual({ kinds: ["разработка", "UI"], rest: ["AWPCA"] });
|
||||||
|
});
|
||||||
|
it("без kind_labels — все метки прочие (Review Focus 4)", () => {
|
||||||
|
expect(splitKindLabels(["AWPCA", "kind-dev"], undefined)).toEqual({ kinds: [], rest: ["AWPCA", "kind-dev"] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -31,3 +31,14 @@ export function isOverdue(due: string, status: string | null | undefined, closed
|
|||||||
|
|
||||||
export const todayISO = (d = new Date()): string =>
|
export const todayISO = (d = new Date()): string =>
|
||||||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
// метки задачи: виды работ (метка из kind_labels → название вида) отдельно от прочих меток
|
||||||
|
export function splitKindLabels(labels: string[], kindLabels: Record<string, string> | undefined): { kinds: string[]; rest: string[] } {
|
||||||
|
const byLabel = new Map(Object.entries(kindLabels ?? {}).map(([kind, label]) => [label, kind]));
|
||||||
|
const kinds: string[] = [], rest: string[] = [];
|
||||||
|
for (const l of labels) {
|
||||||
|
const k = byLabel.get(l);
|
||||||
|
if (k !== undefined) kinds.push(k); else rest.push(l);
|
||||||
|
}
|
||||||
|
return { kinds, rest };
|
||||||
|
}
|
||||||
|
|||||||
@ -42,8 +42,11 @@ export function useIssueOverlay(): Overlay {
|
|||||||
|
|
||||||
const abortClose = useCallback(() => { closingDirect.current = false; }, []);
|
const abortClose = useCallback(() => { closingDirect.current = false; }, []);
|
||||||
|
|
||||||
|
// «Назад» в окне — только если оно открыто переходом из другой карточки (из списка depth = 1; прямой адрес — direct, depth 0)
|
||||||
|
const onBack = card && (card.depth >= 2 || (card.direct && card.depth >= 1)) ? () => void navigate(-1) : undefined;
|
||||||
|
|
||||||
const element = issueKey
|
const element = issueKey
|
||||||
? <CloseAbortContext value={abortClose}><IssueModal issueKey={issueKey} onClose={close} /></CloseAbortContext>
|
? <CloseAbortContext value={abortClose}><IssueModal issueKey={issueKey} onClose={close} onBack={onBack} /></CloseAbortContext>
|
||||||
: null;
|
: null;
|
||||||
return { pageLocation, element };
|
return { pageLocation, element };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user