mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Disable Save-to-server when storage off, fix QR port 0 (#6473)
This commit is contained in:
@@ -39,6 +39,8 @@ interface FileDetailsPanelProps {
|
||||
onRemove: (fileIds: FileId[]) => void;
|
||||
/** Save to server; only shown when at least one selected file is local-only. */
|
||||
onSaveToServer?: (files: StirlingFileStub[]) => void;
|
||||
/** When set, Save to server renders disabled with this tooltip (storage off). */
|
||||
saveToServerDisabledReason?: string | null;
|
||||
}
|
||||
|
||||
export function FileDetailsPanel({
|
||||
@@ -51,6 +53,7 @@ export function FileDetailsPanel({
|
||||
onMove,
|
||||
onRemove,
|
||||
onSaveToServer,
|
||||
saveToServerDisabledReason,
|
||||
}: FileDetailsPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const { sharingEnabled } = useSharingEnabled();
|
||||
@@ -319,15 +322,34 @@ export function FileDetailsPanel({
|
||||
>
|
||||
{t("filesPage.moveTo", "Move to…")}
|
||||
</Button>
|
||||
{/* Save to server; shown when any selected file is local-only. */}
|
||||
{/* Save to server; shown when any selected file is local-only. When
|
||||
storage is off it stays visible but disabled with a tooltip (same
|
||||
treatment as Manage sharing above). */}
|
||||
{onSaveToServer && localOnlyFiles.length > 0 && (
|
||||
<Button
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
variant="default"
|
||||
onClick={() => onSaveToServer(localOnlyFiles)}
|
||||
<Tooltip
|
||||
label={saveToServerDisabledReason}
|
||||
disabled={!saveToServerDisabledReason}
|
||||
withinPortal
|
||||
multiline
|
||||
w={260}
|
||||
>
|
||||
{t("filesPage.saveToServer", "Save to server")}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
variant="default"
|
||||
disabled={Boolean(saveToServerDisabledReason)}
|
||||
onClick={() => onSaveToServer(localOnlyFiles)}
|
||||
styles={{
|
||||
root: {
|
||||
// Keep tooltip hoverable while button is disabled.
|
||||
pointerEvents: saveToServerDisabledReason
|
||||
? "auto"
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("filesPage.saveToServer", "Save to server")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
leftSection={<DeleteIcon fontSize="small" />}
|
||||
|
||||
@@ -78,6 +78,8 @@ interface FileGridProps {
|
||||
onPromptMoveFiles: (fileIds: FileId[]) => void;
|
||||
/** Per-file Save to server; hidden when file already has remoteStorageId. */
|
||||
onSaveToServer?: (file: StirlingFileStub) => void;
|
||||
/** When set, the Save to server item renders disabled with this tooltip. */
|
||||
saveToServerDisabledReason?: string | null;
|
||||
/** When supplied the list-view column headers become sortable. */
|
||||
sortMode?: FilesPageSortMode;
|
||||
onChangeSortMode?: (mode: FilesPageSortMode) => void;
|
||||
@@ -334,6 +336,7 @@ function GridView({
|
||||
onRemoveFiles,
|
||||
onPromptMoveFiles,
|
||||
onSaveToServer,
|
||||
saveToServerDisabledReason,
|
||||
}: FileGridProps) {
|
||||
return (
|
||||
<div className="files-page-grid" role="list">
|
||||
@@ -386,6 +389,7 @@ function GridView({
|
||||
onSaveToServer={
|
||||
onSaveToServer ? () => onSaveToServer(entry.file!) : undefined
|
||||
}
|
||||
saveToServerDisabledReason={saveToServerDisabledReason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -584,6 +588,8 @@ interface FileCardProps {
|
||||
onMove: () => void;
|
||||
/** Kebab Save to server; only fires when file is local-only. */
|
||||
onSaveToServer?: () => void;
|
||||
/** When set, the kebab Save to server is disabled with this tooltip. */
|
||||
saveToServerDisabledReason?: string | null;
|
||||
}
|
||||
|
||||
function FileCard({
|
||||
@@ -599,6 +605,7 @@ function FileCard({
|
||||
onRemove,
|
||||
onMove,
|
||||
onSaveToServer,
|
||||
saveToServerDisabledReason,
|
||||
}: FileCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
@@ -769,17 +776,33 @@ function FileCard({
|
||||
>
|
||||
{t("filesPage.moveTo", "Move to…")}
|
||||
</Menu.Item>
|
||||
{/* Per-file Save to server; hidden when already on server. */}
|
||||
{/* Per-file Save to server; shown for local-only files. When
|
||||
storage is off it stays visible but disabled with a tooltip. */}
|
||||
{onSaveToServer && file.remoteStorageId == null && (
|
||||
<Menu.Item
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSaveToServer();
|
||||
}}
|
||||
<Tooltip
|
||||
label={saveToServerDisabledReason}
|
||||
disabled={!saveToServerDisabledReason}
|
||||
withinPortal
|
||||
position="left"
|
||||
multiline
|
||||
w={240}
|
||||
>
|
||||
{t("filesPage.saveToServer", "Save to server")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
disabled={Boolean(saveToServerDisabledReason)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSaveToServer();
|
||||
}}
|
||||
style={
|
||||
saveToServerDisabledReason
|
||||
? { pointerEvents: "auto" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{t("filesPage.saveToServer", "Save to server")}
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
@@ -813,6 +836,7 @@ function ListView({
|
||||
onRenameFolder,
|
||||
onDeleteFolder,
|
||||
onSaveToServer,
|
||||
saveToServerDisabledReason,
|
||||
onChangeFolderAppearance,
|
||||
onRemoveFiles,
|
||||
onPromptMoveFiles,
|
||||
@@ -946,6 +970,7 @@ function ListView({
|
||||
onSaveToServer={
|
||||
onSaveToServer ? () => onSaveToServer(entry.file!) : undefined
|
||||
}
|
||||
saveToServerDisabledReason={saveToServerDisabledReason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1148,6 +1173,8 @@ interface FileRowProps {
|
||||
onMove: () => void;
|
||||
/** Kebab Save to server; only fires when file is local-only. */
|
||||
onSaveToServer?: () => void;
|
||||
/** When set, the kebab Save to server is disabled with this tooltip. */
|
||||
saveToServerDisabledReason?: string | null;
|
||||
}
|
||||
|
||||
function FileRow({
|
||||
@@ -1163,6 +1190,7 @@ function FileRow({
|
||||
onRemove,
|
||||
onMove,
|
||||
onSaveToServer,
|
||||
saveToServerDisabledReason,
|
||||
}: FileRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const kebabRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -1336,17 +1364,33 @@ function FileRow({
|
||||
>
|
||||
{t("filesPage.moveTo", "Move to…")}
|
||||
</Menu.Item>
|
||||
{/* Per-file Save to server; hidden when already on server. */}
|
||||
{/* Per-file Save to server; shown for local-only files. When
|
||||
storage is off it stays visible but disabled with a tooltip. */}
|
||||
{onSaveToServer && file.remoteStorageId == null && (
|
||||
<Menu.Item
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSaveToServer();
|
||||
}}
|
||||
<Tooltip
|
||||
label={saveToServerDisabledReason}
|
||||
disabled={!saveToServerDisabledReason}
|
||||
withinPortal
|
||||
position="left"
|
||||
multiline
|
||||
w={240}
|
||||
>
|
||||
{t("filesPage.saveToServer", "Save to server")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
disabled={Boolean(saveToServerDisabledReason)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSaveToServer();
|
||||
}}
|
||||
style={
|
||||
saveToServerDisabledReason
|
||||
? { pointerEvents: "auto" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{t("filesPage.saveToServer", "Save to server")}
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
|
||||
@@ -106,6 +106,17 @@ export default function FileManagerView() {
|
||||
const isMobile = useIsMobile();
|
||||
const isMobileUploadAvailable =
|
||||
Boolean(appConfig?.enableMobileScanner) && !isMobile;
|
||||
// Server storage gate; mirrors ConfigController's storageEnabled
|
||||
// (enableLogin && storage.isEnabled). When off, Save-to-server stays
|
||||
// visible but disabled with an explanatory tooltip (discoverability beats
|
||||
// hiding - mirrors the New folder / Manage sharing gates in this view).
|
||||
const uploadEnabled = appConfig?.storageEnabled === true;
|
||||
const saveToServerDisabledReason: string | null = uploadEnabled
|
||||
? null
|
||||
: t(
|
||||
"filesPage.saveToServerDisabledHint",
|
||||
"Saving to the server isn't enabled on this server. Ask your admin to enable it.",
|
||||
);
|
||||
const [mobileUploadModalOpen, setMobileUploadModalOpen] = useState(false);
|
||||
const { actions: navActions } = useNavigationActions();
|
||||
const { requestNavigation } = useNavigationGuard();
|
||||
@@ -1185,19 +1196,35 @@ export default function FileManagerView() {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Save to server; hidden when no local-only file selected. */}
|
||||
{/* Save to server; shown whenever local-only files are
|
||||
selected. When storage is off it stays visible but
|
||||
disabled, tooltip pointing at the admin. */}
|
||||
{localOnlySelectedStubs.length > 0 && (
|
||||
<Tooltip
|
||||
label={t("filesPage.saveToServer", "Save to server")}
|
||||
label={
|
||||
saveToServerDisabledReason ??
|
||||
t("filesPage.saveToServer", "Save to server")
|
||||
}
|
||||
withinPortal
|
||||
multiline={Boolean(saveToServerDisabledReason)}
|
||||
w={saveToServerDisabledReason ? 240 : undefined}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
leftSection={<CloudUploadIcon fontSize="small" />}
|
||||
disabled={Boolean(saveToServerDisabledReason)}
|
||||
onClick={() =>
|
||||
setSaveToServerTarget(localOnlySelectedStubs)
|
||||
}
|
||||
styles={{
|
||||
root: {
|
||||
// Keep the tooltip hoverable while disabled.
|
||||
pointerEvents: saveToServerDisabledReason
|
||||
? "auto"
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
aria-label={t(
|
||||
"filesPage.saveToServer",
|
||||
"Save to server",
|
||||
@@ -1450,6 +1477,7 @@ export default function FileManagerView() {
|
||||
onRemoveFiles={handleRemoveFiles}
|
||||
onPromptMoveFiles={promptMoveFiles}
|
||||
onSaveToServer={(file) => setSaveToServerTarget([file])}
|
||||
saveToServerDisabledReason={saveToServerDisabledReason}
|
||||
// Center-of-grid CTAs when the empty state shows - same
|
||||
// handlers the corner header buttons use so behaviour
|
||||
// (disabled tooltips, native file picker, dialog) is
|
||||
@@ -1495,6 +1523,7 @@ export default function FileManagerView() {
|
||||
onMove={promptMoveFiles}
|
||||
onRemove={handleRemoveFiles}
|
||||
onSaveToServer={(files) => setSaveToServerTarget(files)}
|
||||
saveToServerDisabledReason={saveToServerDisabledReason}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1521,6 +1550,7 @@ export default function FileManagerView() {
|
||||
onMove={promptMoveFiles}
|
||||
onRemove={handleRemoveFiles}
|
||||
onSaveToServer={(files) => setSaveToServerTarget(files)}
|
||||
saveToServerDisabledReason={saveToServerDisabledReason}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
@@ -89,11 +89,21 @@ const BulkUploadToServerModal: React.FC<BulkUploadToServerModalProps> = ({
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to upload files to server:", error);
|
||||
// A 403 means the server has storage turned off (or login disabled,
|
||||
// which gates storage). Say so plainly instead of the generic
|
||||
// "check your settings" message, which reads as a user mistake.
|
||||
const status = (error as { response?: { status?: number } })?.response
|
||||
?.status;
|
||||
setErrorMessage(
|
||||
t(
|
||||
"storageUpload.failure",
|
||||
"Upload failed. Please check your login and storage settings.",
|
||||
),
|
||||
status === 403
|
||||
? t(
|
||||
"storageUpload.featureDisabled",
|
||||
"Saving to the server isn't enabled on this server.",
|
||||
)
|
||||
: t(
|
||||
"storageUpload.failure",
|
||||
"Upload failed. Please check your login and storage settings.",
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
|
||||
@@ -284,6 +284,52 @@ test.describe("Files page", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Save to server gating (storage disabled)", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// storageEnabled:false -> Save-to-server stays visible for local-only
|
||||
// files but is disabled (with an explanatory tooltip), not hidden, so
|
||||
// users discover the feature and know to ask their admin.
|
||||
await stubStorageApis(page, { storageEnabled: false });
|
||||
await seedFiles(page, [
|
||||
{ id: "local-a", name: "local-a.pdf", remoteStorageId: null },
|
||||
]);
|
||||
});
|
||||
test.use({ autoGoto: false });
|
||||
|
||||
test("bulk Save to server is disabled (not hidden) when storage off", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoFilesPage(page);
|
||||
await page
|
||||
.locator(".files-page-card:not(.is-folder)")
|
||||
.filter({ hasText: "local-a.pdf" })
|
||||
.click();
|
||||
const saveButtons = page.getByRole("button", {
|
||||
name: /^Save to server$/i,
|
||||
});
|
||||
// Present (toolbar + details panel) and every instance disabled.
|
||||
const count = await saveButtons.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
await expect(saveButtons.nth(i)).toBeVisible();
|
||||
await expect(saveButtons.nth(i)).toBeDisabled();
|
||||
}
|
||||
});
|
||||
|
||||
test("per-file kebab Save to server is disabled (not hidden) when storage off", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoFilesPage(page);
|
||||
const localCard = page
|
||||
.locator(".files-page-card:not(.is-folder)")
|
||||
.filter({ hasText: "local-a.pdf" });
|
||||
await localCard.getByRole("button", { name: /File actions/i }).click();
|
||||
const item = page.getByRole("menuitem", { name: /^Save to server$/i });
|
||||
await expect(item).toBeVisible();
|
||||
await expect(item).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Upload behaviour", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await stubStorageApis(page);
|
||||
|
||||
Reference in New Issue
Block a user