mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Add eye icon on files
This commit is contained in:
@@ -45,6 +45,11 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Sits after the page count, so a long filename must not squeeze it. */
|
||||
.trackLeadAction {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.trackName {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Center, Loader, LoadingOverlay, Stack, Text } from "@mantine/core";
|
||||
import {
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { useFileState } from "@app/contexts/FileContext";
|
||||
import { useNavigationGuard } from "@app/contexts/NavigationContext";
|
||||
import {
|
||||
useNavigationActions,
|
||||
useNavigationGuard,
|
||||
} from "@app/contexts/NavigationContext";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { FileId } from "@app/types/file";
|
||||
import { useTrackWorkspace } from "@app/components/pageTracks/hooks/useTrackWorkspace";
|
||||
import { useTrackSelection } from "@app/components/pageTracks/hooks/useTrackSelection";
|
||||
@@ -82,7 +86,38 @@ export default function PageTracks() {
|
||||
|
||||
const selection = useTrackSelection(workspace);
|
||||
const thumbnails = useTrackThumbnails();
|
||||
const { saving, progress, save } = useTrackSave(workspace, changedFileIds);
|
||||
const { actions: navActions } = useNavigationActions();
|
||||
const { setActiveFileId } = useViewer();
|
||||
|
||||
// The file the user asked to view, held across a save: committing gives it a
|
||||
// new id, and the viewer drops an active file that has left the workbench.
|
||||
const viewTargetRef = useRef<FileId | null>(null);
|
||||
const handleVersioned = useCallback(
|
||||
(previousId: FileId, nextId: FileId) => {
|
||||
if (viewTargetRef.current !== previousId) return;
|
||||
viewTargetRef.current = nextId;
|
||||
setActiveFileId(nextId as string);
|
||||
},
|
||||
[setActiveFileId],
|
||||
);
|
||||
|
||||
const { saving, progress, save } = useTrackSave(workspace, changedFileIds, {
|
||||
onVersioned: handleVersioned,
|
||||
});
|
||||
|
||||
/**
|
||||
* Opens one track's file in the Viewer. Routed through setWorkbench so the
|
||||
* unsaved-changes prompt still fires: viewing a file whose pending edits
|
||||
* haven't been written would show stale pages.
|
||||
*/
|
||||
const openInViewer = useCallback(
|
||||
(fileId: FileId) => {
|
||||
viewTargetRef.current = fileId;
|
||||
setActiveFileId(fileId as string);
|
||||
navActions.setWorkbench("viewer");
|
||||
},
|
||||
[navActions, setActiveFileId],
|
||||
);
|
||||
|
||||
const [draggingIds, setDraggingIds] = useState<Set<string>>(
|
||||
() => new Set<string>(),
|
||||
@@ -357,6 +392,7 @@ export default function PageTracks() {
|
||||
thumbnails={thumbnails}
|
||||
onSelectPage={selection.selectPage}
|
||||
onSelectTrack={selection.selectTrack}
|
||||
onOpenInViewer={openInViewer}
|
||||
onClearSelection={clearSelection}
|
||||
onRotate={rotatePages}
|
||||
onDelete={deletePages}
|
||||
|
||||
@@ -6,6 +6,7 @@ import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import SelectAllIcon from "@mui/icons-material/SelectAll";
|
||||
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
||||
import { ActionIcon } from "@app/ui/ActionIcon";
|
||||
import { Tooltip } from "@app/components/shared/Tooltip";
|
||||
import { FileId } from "@app/types/file";
|
||||
@@ -42,6 +43,7 @@ export interface TrackRowProps {
|
||||
modifiers: PageClickModifiers,
|
||||
) => void;
|
||||
onSelectTrack: (fileId: FileId) => void;
|
||||
onOpenInViewer: (fileId: FileId) => void;
|
||||
/** Called when the click landed on empty lane surface, not on a page. */
|
||||
onClearSelection: () => void;
|
||||
onRotate: (pageIds: string[], delta: number) => void;
|
||||
@@ -59,6 +61,7 @@ function TrackRowImpl({
|
||||
thumbnails,
|
||||
onSelectPage,
|
||||
onSelectTrack,
|
||||
onOpenInViewer,
|
||||
onClearSelection,
|
||||
onRotate,
|
||||
onDelete,
|
||||
@@ -148,6 +151,19 @@ function TrackRowImpl({
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</span>
|
||||
<Tooltip content={t("openInViewer", "Open in Viewer")}>
|
||||
<ActionIcon
|
||||
className={styles.trackLeadAction}
|
||||
variant="quiet"
|
||||
size="sm"
|
||||
aria-label={t("openInViewer", "Open in Viewer")}
|
||||
// An emptied track has nothing to show: saving closes the file.
|
||||
disabled={track.pages.length === 0}
|
||||
onClick={() => onOpenInViewer(track.fileId)}
|
||||
>
|
||||
<VisibilityOutlinedIcon sx={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div className={styles.trackActions}>
|
||||
<Tooltip
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useFileActions, useFileState } from "@app/contexts/FileContext";
|
||||
import {
|
||||
createChildStub,
|
||||
@@ -21,6 +21,15 @@ export interface TrackSaveProgress {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TrackSaveOptions {
|
||||
/**
|
||||
* Called per committed file with its old and new ids. Saving replaces a file
|
||||
* with a new version under a NEW id, so anything holding the old one (the
|
||||
* viewer's active file, for instance) has to be re-pointed.
|
||||
*/
|
||||
onVersioned?: (previousId: FileId, nextId: FileId) => void;
|
||||
}
|
||||
|
||||
export interface TrackSaveHook {
|
||||
saving: boolean;
|
||||
progress: TrackSaveProgress | null;
|
||||
@@ -62,9 +71,12 @@ function toExportDocument(
|
||||
export function useTrackSave(
|
||||
workspace: TrackWorkspace,
|
||||
changedFileIds: FileId[],
|
||||
options: TrackSaveOptions = {},
|
||||
): TrackSaveHook {
|
||||
const { selectors } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
const onVersionedRef = useRef(options.onVersioned);
|
||||
onVersionedRef.current = options.onVersioned;
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [progress, setProgress] = useState<TrackSaveProgress | null>(null);
|
||||
|
||||
@@ -133,6 +145,7 @@ export function useTrackSave(
|
||||
[outputStub],
|
||||
{ silent: true },
|
||||
);
|
||||
onVersionedRef.current?.(entry.fileId, outputStub.id);
|
||||
}
|
||||
|
||||
if (emptied.length > 0) {
|
||||
|
||||
@@ -86,6 +86,14 @@ async function dragPageOver(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The file the viewer is showing, per the sidebar's "viewed" row marker. Note
|
||||
* `.selected` is workbench selection, which is a different thing.
|
||||
*/
|
||||
function viewerActiveFile(page: import("@playwright/test").Page) {
|
||||
return page.locator(".file-sidebar-file-item.viewed .file-sidebar-file-name");
|
||||
}
|
||||
|
||||
/** The tile the insertion line is currently drawn against. */
|
||||
function dropTarget(page: import("@playwright/test").Page) {
|
||||
return page.locator("[data-page-id][data-drop-before]");
|
||||
@@ -475,4 +483,57 @@ test.describe("Page Editor tracks", () => {
|
||||
fs.rmSync(longPdf, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("the eye opens that track's file in the viewer", async ({ page }) => {
|
||||
await openPageEditor(page);
|
||||
const sample = track(page, "sample.pdf");
|
||||
await expect(sample.locator("[data-page-id]")).toHaveCount(1, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
// Second track, so landing on the first file would look like success.
|
||||
await sample.getByRole("button", { name: "Open in Viewer" }).click();
|
||||
|
||||
await expect(page.getByTestId("page-tracks")).toHaveCount(0);
|
||||
await expect(viewerActiveFile(page)).toHaveText("sample.pdf");
|
||||
});
|
||||
|
||||
test("the eye prompts when edits are pending, then views the saved version", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openPageEditor(page);
|
||||
const rotated = track(page, "rotated-pages.pdf");
|
||||
const tiles = rotated.locator("[data-page-id]");
|
||||
await expect(tiles).toHaveCount(4, { timeout: 30_000 });
|
||||
|
||||
// Dirty the very file being opened: the save gives it a new id, so the
|
||||
// viewer target has to follow the version bump.
|
||||
const last = tiles.nth(3);
|
||||
await last.hover();
|
||||
await last.getByRole("button", { name: "Delete page" }).click();
|
||||
await expect(tiles).toHaveCount(3);
|
||||
|
||||
await rotated.getByRole("button", { name: "Open in Viewer" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Unsaved Changes" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Save & Leave" }).click();
|
||||
|
||||
// Landed in the viewer on the file the eye named. Saving gives every
|
||||
// changed file a NEW id, and the viewer drops an active file that has left
|
||||
// the workbench, so this only holds if the target is re-pointed.
|
||||
await expect(page.getByTestId("page-tracks")).toHaveCount(0, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
await expect(viewerActiveFile(page)).toHaveText("rotated-pages.pdf", {
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
// And the pending edit was written rather than dropped.
|
||||
await switchView(page, "Page Editor");
|
||||
const saved = track(page, "rotated-pages.pdf");
|
||||
await expect(saved).toContainText("v2", { timeout: 90_000 });
|
||||
await expect(saved.locator("[data-page-id]")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user