Fix stale document viewer navigation races
Dispose the viewer query when the alias changes so navigating between documents shows the skeleton instead of the previous document. Guard the export completion against the current id so a slow export can't overwrite a newer document's preview. Split the documents tab state into requested and loaded refs so an out-of-order or failed refetch can't leave the list showing a different tab than the toolbar. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -28,13 +28,16 @@ import { DocumentViewerPageSkeleton } from "./DocumentViewerPageSkeleton";
|
||||
|
||||
export default function DocumentViewerPageLoader() {
|
||||
const { alias } = useParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<DocumentViewerPageQuery>(documentViewerPageQuery);
|
||||
const [queryRef, loadQuery, disposeQuery] = useQueryLoader<DocumentViewerPageQuery>(documentViewerPageQuery);
|
||||
|
||||
// Dispose on alias change so navigating between documents shows the skeleton
|
||||
// during the transition instead of the previous document's metadata/preview.
|
||||
useEffect(() => {
|
||||
if (alias) {
|
||||
loadQuery({ alias });
|
||||
}
|
||||
}, [loadQuery, alias]);
|
||||
return () => disposeQuery();
|
||||
}, [loadQuery, disposeQuery, alias]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <DocumentViewerPageSkeleton />;
|
||||
|
||||
@@ -108,15 +108,35 @@ export function DocumentsPage({ queryRef }: DocumentsPageProps) {
|
||||
// initial preload was in flight, this reconciles by refetching rather than
|
||||
// showing the wrong slice. Refetch inside a transition so the toolbar and
|
||||
// current results stay mounted (dimmed via `isRefetching`) while it loads.
|
||||
const fetchedVisibility = useRef(queryRef.variables.visibility ?? null);
|
||||
//
|
||||
// `requestedVisibility` de-dupes in-flight requests; `loadedVisibility` only
|
||||
// advances when the *latest* refetch settles, so an out-of-order or failed
|
||||
// refetch can't leave the list showing a different tab than the toolbar.
|
||||
const initialVisibility = queryRef.variables.visibility ?? null;
|
||||
const loadedVisibility = useRef(initialVisibility);
|
||||
const requestedVisibility = useRef(initialVisibility);
|
||||
useEffect(() => {
|
||||
const target = toQueryVariables(tab).visibility ?? null;
|
||||
if (target === fetchedVisibility.current) {
|
||||
if (target === requestedVisibility.current) {
|
||||
return;
|
||||
}
|
||||
fetchedVisibility.current = target;
|
||||
requestedVisibility.current = target;
|
||||
startTransition(() => {
|
||||
refetch(toQueryVariables(tab), { fetchPolicy: "store-or-network" });
|
||||
refetch(toQueryVariables(tab), {
|
||||
fetchPolicy: "store-or-network",
|
||||
onComplete: (error) => {
|
||||
if (requestedVisibility.current !== target) {
|
||||
// A newer tab was requested; ignore this stale settle.
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
// Allow re-selecting this tab to retry after a failed refetch.
|
||||
requestedVisibility.current = loadedVisibility.current;
|
||||
return;
|
||||
}
|
||||
loadedVisibility.current = target;
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [refetch, tab]);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { useMutation } from "#/lib/relay/useMutation";
|
||||
@@ -77,28 +77,41 @@ export function useDocumentExport(kind: DocumentKind, id: string, enabled: boole
|
||||
setDataUri(null);
|
||||
}
|
||||
|
||||
// Track the current target so a slow export that resolves after the id
|
||||
// changed cannot overwrite the preview with the previous document's bytes.
|
||||
const currentId = useRef(id);
|
||||
useEffect(() => {
|
||||
currentId.current = id;
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || dataUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apply = (targetId: string, data: string) => {
|
||||
if (currentId.current === targetId) {
|
||||
setDataUri(data);
|
||||
}
|
||||
};
|
||||
|
||||
switch (kind) {
|
||||
case "Document":
|
||||
exportDocument({
|
||||
variables: { input: { documentId: id } },
|
||||
onCompleted: response => setDataUri(response.exportDocumentPDF.data),
|
||||
onCompleted: response => apply(id, response.exportDocumentPDF.data),
|
||||
}).catch(() => {});
|
||||
break;
|
||||
case "TrustCenterFile":
|
||||
exportFile({
|
||||
variables: { input: { trustCenterFileId: id } },
|
||||
onCompleted: response => setDataUri(response.exportTrustCenterFile.data),
|
||||
onCompleted: response => apply(id, response.exportTrustCenterFile.data),
|
||||
}).catch(() => {});
|
||||
break;
|
||||
case "AuditReport":
|
||||
exportReport({
|
||||
variables: { input: { reportId: id } },
|
||||
onCompleted: response => setDataUri(response.exportReportPDF.data),
|
||||
onCompleted: response => apply(id, response.exportReportPDF.data),
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user