mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
UI redesign staging (#6149)
Co-authored-by: Reece Browne <[email protected]> Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
Reece Browne
James Brunton
parent
beb99e273b
commit
c731d5fd5d
@@ -1,75 +0,0 @@
|
||||
import React from "react";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName";
|
||||
import type { StirlingFileStub } from "@app/types/fileContext";
|
||||
import type { FileId } from "@app/types/file";
|
||||
|
||||
const buildFileStub = (
|
||||
overrides: Partial<StirlingFileStub> = {},
|
||||
): StirlingFileStub => ({
|
||||
id: "file-1" as FileId,
|
||||
name: "report.pdf",
|
||||
type: "application/pdf",
|
||||
size: 1024,
|
||||
lastModified: 0,
|
||||
isLeaf: true,
|
||||
originalFileId: "file-1",
|
||||
versionNumber: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const renderName = (file: StirlingFileStub) =>
|
||||
render(
|
||||
<MantineProvider>
|
||||
<FileEditorFileName file={file} />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
describe("FileEditorFileName (desktop)", () => {
|
||||
test("renders red 'not saved' indicator when file has no local path", () => {
|
||||
renderName(buildFileStub());
|
||||
|
||||
const indicator = screen.getByLabelText("fileNotSavedToDisk");
|
||||
expect(indicator).toBeInTheDocument();
|
||||
expect(indicator).toHaveStyle({
|
||||
backgroundColor: "var(--mantine-color-red-6)",
|
||||
});
|
||||
expect(screen.queryByLabelText("unsavedChanges")).toBeNull();
|
||||
expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders yellow 'unsaved changes' indicator when file is dirty", () => {
|
||||
renderName(
|
||||
buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: true }),
|
||||
);
|
||||
|
||||
const indicator = screen.getByLabelText("unsavedChanges");
|
||||
expect(indicator).toBeInTheDocument();
|
||||
expect(indicator).toHaveStyle({
|
||||
backgroundColor: "var(--mantine-color-yellow-6)",
|
||||
});
|
||||
expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull();
|
||||
expect(screen.queryByLabelText("fileSavedToDisk")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders green 'saved' indicator when file is persisted and clean", () => {
|
||||
renderName(
|
||||
buildFileStub({ localFilePath: "/tmp/report.pdf", isDirty: false }),
|
||||
);
|
||||
|
||||
const indicator = screen.getByLabelText("fileSavedToDisk");
|
||||
expect(indicator).toBeInTheDocument();
|
||||
expect(indicator).toHaveStyle({
|
||||
backgroundColor: "var(--mantine-color-green-6)",
|
||||
});
|
||||
expect(screen.queryByLabelText("fileNotSavedToDisk")).toBeNull();
|
||||
expect(screen.queryByLabelText("unsavedChanges")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders the filename alongside the indicator", () => {
|
||||
renderName(buildFileStub({ name: "invoice.pdf" }));
|
||||
expect(screen.getByText("invoice.pdf")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from "react";
|
||||
import { Tooltip } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StirlingFileStub } from "@app/types/fileContext";
|
||||
import { PrivateContent } from "@app/components/shared/PrivateContent";
|
||||
import { truncateCenter } from "@app/utils/textUtils";
|
||||
|
||||
interface FileEditorFileNameProps {
|
||||
file: StirlingFileStub;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const FileEditorFileName = ({
|
||||
file,
|
||||
maxLength = 40,
|
||||
}: FileEditorFileNameProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrivateContent>{truncateCenter(file.name, maxLength)}</PrivateContent>
|
||||
{!file.localFilePath && (
|
||||
<Tooltip label={t("fileNotSavedToDisk", "Not saved to disk")}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--mantine-color-red-6)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
aria-label={t("fileNotSavedToDisk", "Not saved to disk")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{file.localFilePath && file.isDirty && (
|
||||
<Tooltip label={t("unsavedChanges", "Unsaved changes")}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--mantine-color-yellow-6)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
aria-label={t("unsavedChanges", "Unsaved changes")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{file.localFilePath && !file.isDirty && (
|
||||
<Tooltip label={t("fileSavedToDisk", "File saved to disk")}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--mantine-color-green-6)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
aria-label={t("fileSavedToDisk", "File saved to disk")}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileEditorFileName;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Tooltip } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StirlingFileStub } from "@app/types/fileContext";
|
||||
import styles from "@app/components/fileEditor/FileEditorThumbnail.module.css";
|
||||
|
||||
interface FileEditorStatusDotProps {
|
||||
file: StirlingFileStub;
|
||||
}
|
||||
|
||||
export function FileEditorStatusDot({ file }: FileEditorStatusDotProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const label = !file.localFilePath
|
||||
? t("fileNotSavedToDisk", "Not saved to disk")
|
||||
: file.isDirty
|
||||
? t("unsavedChanges", "Unsaved changes")
|
||||
: t("fileSavedToDisk", "Saved to disk");
|
||||
|
||||
const color = !file.localFilePath
|
||||
? "var(--mantine-color-red-6)"
|
||||
: file.isDirty
|
||||
? "var(--mantine-color-yellow-6)"
|
||||
: "var(--mantine-color-green-6)";
|
||||
|
||||
return (
|
||||
<div className={styles.thumbBadgesRight}>
|
||||
<Tooltip label={label}>
|
||||
<span
|
||||
className={styles.statusDot}
|
||||
style={{ backgroundColor: color }}
|
||||
aria-label={label}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Box, Tooltip, rem } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
connectionModeService,
|
||||
type ConnectionMode,
|
||||
} from "@app/services/connectionModeService";
|
||||
import {
|
||||
selfHostedServerMonitor,
|
||||
type SelfHostedServerState,
|
||||
} from "@app/services/selfHostedServerMonitor";
|
||||
import { useBackendHealth } from "@app/hooks/useBackendHealth";
|
||||
import { OPEN_SIGN_IN_EVENT } from "@app/constants/signInEvents";
|
||||
|
||||
interface RightRailFooterExtensionsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ConnectionStatusDot() {
|
||||
const { t } = useTranslation();
|
||||
const [connectionMode, setConnectionMode] = useState<ConnectionMode | null>(
|
||||
null,
|
||||
);
|
||||
const [selfHostedState, setSelfHostedState] = useState<SelfHostedServerState>(
|
||||
() => selfHostedServerMonitor.getSnapshot(),
|
||||
);
|
||||
const { isOnline, checkHealth } = useBackendHealth();
|
||||
|
||||
useEffect(() => {
|
||||
void connectionModeService.getCurrentMode().then(setConnectionMode);
|
||||
const unsubscribe = connectionModeService.subscribeToModeChanges(
|
||||
(config) => {
|
||||
setConnectionMode(config.mode);
|
||||
},
|
||||
);
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return selfHostedServerMonitor.subscribe(setSelfHostedState);
|
||||
}, []);
|
||||
|
||||
const { label, color } = useMemo(() => {
|
||||
if (connectionMode === "saas") {
|
||||
return {
|
||||
label: t("connectionMode.status.saas", "Connected to Stirling Cloud"),
|
||||
color: "#3b82f6",
|
||||
};
|
||||
}
|
||||
if (connectionMode === "selfhosted") {
|
||||
const serverOnline = selfHostedState.isOnline;
|
||||
const serverChecking = selfHostedState.status === "checking";
|
||||
const backendLabel = serverChecking
|
||||
? t(
|
||||
"connectionMode.status.selfhostedChecking",
|
||||
"Connected to self-hosted server (checking...)",
|
||||
)
|
||||
: serverOnline
|
||||
? t(
|
||||
"connectionMode.status.selfhostedOnline",
|
||||
"Connected to self-hosted server",
|
||||
)
|
||||
: t(
|
||||
"connectionMode.status.selfhostedOffline",
|
||||
"Self-hosted server unreachable",
|
||||
);
|
||||
return {
|
||||
label: backendLabel,
|
||||
color: serverChecking
|
||||
? "#fcc419"
|
||||
: serverOnline
|
||||
? "#37b24d"
|
||||
: "#e03131",
|
||||
};
|
||||
}
|
||||
// local
|
||||
return {
|
||||
label: isOnline
|
||||
? t("connectionMode.status.localOnline", "Offline mode running")
|
||||
: t("connectionMode.status.localOffline", "Offline mode running"),
|
||||
color: "#868e96",
|
||||
};
|
||||
}, [connectionMode, selfHostedState, isOnline, t]);
|
||||
|
||||
return (
|
||||
<Tooltip label={label} position="left" offset={12} withArrow withinPortal>
|
||||
<Box
|
||||
component="span"
|
||||
role="status"
|
||||
aria-label={label}
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (connectionMode === "local") {
|
||||
window.dispatchEvent(new CustomEvent(OPEN_SIGN_IN_EVENT));
|
||||
} else {
|
||||
void checkHealth();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: rem(10),
|
||||
height: rem(10),
|
||||
borderRadius: "50%",
|
||||
backgroundColor: color,
|
||||
boxShadow: "var(--status-dot-ring)",
|
||||
display: "inline-block",
|
||||
cursor: "pointer",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function RightRailFooterExtensions({
|
||||
className,
|
||||
}: RightRailFooterExtensionsProps) {
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingBottom: rem(12),
|
||||
}}
|
||||
>
|
||||
<ConnectionStatusDot />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const useConfigNavSections = (
|
||||
isAdmin: boolean = false,
|
||||
runningEE: boolean = false,
|
||||
loginEnabled: boolean = false,
|
||||
onRequestClose: () => void = () => {},
|
||||
): ConfigNavSection[] => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -52,6 +53,7 @@ export const useConfigNavSections = (
|
||||
isAdmin,
|
||||
runningEE,
|
||||
loginEnabled,
|
||||
onRequestClose,
|
||||
);
|
||||
|
||||
const connectionModeSection: ConfigNavSection = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import SaveOutlinedIcon from "@mui/icons-material/SaveOutlined";
|
||||
/**
|
||||
* File action icons for desktop builds.
|
||||
* Overrides the core implementation with desktop-appropriate icons.
|
||||
* The presence of `saveAsIconName` signals RightRail to show the Save As button.
|
||||
* The presence of `saveAsIconName` signals the WorkbenchBar to show the Save As button.
|
||||
*/
|
||||
export function useFileActionIcons() {
|
||||
return {
|
||||
@@ -12,7 +12,7 @@ export function useFileActionIcons() {
|
||||
download: SaveOutlinedIcon,
|
||||
uploadIconName: "folder-rounded" as const,
|
||||
downloadIconName: "save-rounded" as const,
|
||||
// Returning this icon name causes RightRail to render the Save As button.
|
||||
// Returning this icon name causes WorkbenchBar to render the Save As button.
|
||||
// On desktop, downloadFile() without a localPath shows a native save dialog.
|
||||
saveAsIconName: "save-as-rounded" as string | undefined,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useFileActionTerminology() {
|
||||
mobileUpload: t("landing.mobileUpload", "Upload from Mobile"),
|
||||
uploadFromComputer: t("landing.openFromComputer", "Open from computer"),
|
||||
download: t("save", "Save"),
|
||||
downloadAll: t("rightRail.saveAll", "Save All"),
|
||||
downloadAll: t("workbenchBar.saveAll", "Save All"),
|
||||
downloadSelected: t("fileManager.saveSelected", "Save Selected"),
|
||||
downloadUnavailable: t("saveUnavailable", "Save unavailable for this item"),
|
||||
noFilesInStorage: t(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { downloadFile } from "@app/services/downloadService";
|
||||
/**
|
||||
* Desktop-only keyboard shortcut: Ctrl/Cmd+S to save selected files
|
||||
* Only saves files that have a localFilePath (came from disk)
|
||||
* Matches Right Rail button behavior: saves selected files if any, otherwise all files
|
||||
* Matches WorkbenchBar button behavior: saves selected files if any, otherwise all files
|
||||
*/
|
||||
export function useSaveShortcut() {
|
||||
const { selectors, state } = useFileState();
|
||||
|
||||
Reference in New Issue
Block a user