From 6e3562b64bfb2ecabd0a9d5555e58e3649816687 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 26 Jan 2026 16:50:48 +0000
Subject: [PATCH] feat(ui): enhance custom S3 storage configuration with helper
text and force path style option
- Add helper text to the S3 storage configuration form to guide users on Endpoint URL format.
- Add 'Force Path Style' checkbox to S3 storage configuration to support MinIO and other S3-compatible providers that require it.
- Update `ProfileModalController` to toggle visibility of these new fields based on the selected provider.
- Update `ProfileModalController` to save and load the `forcePathStyle` setting.
- Update `tests/ui/storageService.test.mjs` to verify persistence of `forcePathStyle`.
---
components/profile-modal.html | 10 +++++++
js/ui/profileModalController.js | 48 +++++++++++++++++++++++++++++---
tests/ui/storageService.test.mjs | 4 ++-
3 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/components/profile-modal.html b/components/profile-modal.html
index f94bd2d1..9fb70db7 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -835,11 +835,21 @@
Enter your Cloudflare R2 credentials. Your Account ID (from dashboard) is used to connect. For Public Access URL, use your custom domain or R2.dev subdomain. Ensure the token has "Object Read & Write" permissions.
+
+
+ Enter your S3-compatible storage details. The Endpoint URL is your provider's API address (e.g., https://s3.amazonaws.com or your MinIO URL).
+
+
Endpoint
+
+
+ Force Path Style
+
+
Region
diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js
index 8855f899..040bab22 100644
--- a/js/ui/profileModalController.js
+++ b/js/ui/profileModalController.js
@@ -1776,6 +1776,9 @@ export class ProfileModalController {
this.storagePrefixWarning = document.getElementById("storagePrefixWarning") || null;
this.storageDefaultInput = document.getElementById("storageDefault") || null;
this.storageR2Helper = document.getElementById("storageR2Helper") || null;
+ this.storageS3Helper = document.getElementById("storageS3Helper") || null;
+ this.storageForcePathStyleInput = document.getElementById("storageForcePathStyle") || null;
+ this.storageForcePathStyleLabel = document.getElementById("storageForcePathStyleLabel") || null;
// Backwards-compatible aliases retained for application code that still
// mirrors DOM references from the controller. These should be removed once
@@ -9042,8 +9045,8 @@ export class ProfileModalController {
fillStorageForm(conn) {
if (!conn) return;
- const { provider, accessKeyId, secretAccessKey, accountId: payloadAccountId, endpoint: payloadEndpoint } = conn;
- const { endpoint, region, bucket, prefix, defaultForUploads, accountId } = conn.meta || {};
+ const { provider, accessKeyId, secretAccessKey, accountId: payloadAccountId, endpoint: payloadEndpoint, forcePathStyle: payloadForcePathStyle } = conn;
+ const { endpoint, region, bucket, prefix, defaultForUploads, accountId, forcePathStyle: metaForcePathStyle } = conn.meta || {};
if (this.storageProviderInput) this.storageProviderInput.value = provider || "cloudflare_r2";
@@ -9058,6 +9061,17 @@ export class ProfileModalController {
if (this.storagePrefixInput) this.storagePrefixInput.value = prefix || "";
if (this.storageDefaultInput) this.storageDefaultInput.checked = !!defaultForUploads;
+ if (this.storageForcePathStyleInput) {
+ if (typeof payloadForcePathStyle === "boolean") {
+ this.storageForcePathStyleInput.checked = payloadForcePathStyle;
+ } else if (typeof metaForcePathStyle === "boolean") {
+ this.storageForcePathStyleInput.checked = metaForcePathStyle;
+ } else {
+ // Default to true for S3 if unspecified
+ this.storageForcePathStyleInput.checked = true;
+ }
+ }
+
this.updateStorageFormVisibility();
this.handlePublicUrlInput();
}
@@ -9098,6 +9112,20 @@ export class ProfileModalController {
if (isR2) this.storageR2Helper.classList.remove("hidden");
else this.storageR2Helper.classList.add("hidden");
}
+
+ if (this.storageS3Helper) {
+ if (!isR2) this.storageS3Helper.classList.remove("hidden");
+ else this.storageS3Helper.classList.add("hidden");
+ }
+
+ if (this.storageForcePathStyleLabel) {
+ if (!isR2) this.storageForcePathStyleLabel.classList.remove("hidden", "flex");
+ else this.storageForcePathStyleLabel.classList.add("hidden");
+
+ if (!isR2) {
+ this.storageForcePathStyleLabel.classList.add("flex");
+ }
+ }
}
async handleUnlockStorage() {
@@ -9195,7 +9223,11 @@ export class ProfileModalController {
}
let publicBaseUrl = "";
- let forcePathStyle = provider === PROVIDERS.GENERIC;
+ // For Generic S3, respect the user's checkbox selection.
+ let forcePathStyle = false;
+ if (provider === PROVIDERS.GENERIC) {
+ forcePathStyle = this.storageForcePathStyleInput?.checked ?? true;
+ }
const payload = {
provider,
@@ -9287,7 +9319,12 @@ export class ProfileModalController {
const accessKeyId = this.storageAccessKeyInput?.value?.trim() || "";
const secretAccessKey = this.storageSecretKeyInput?.value?.trim() || "";
const bucket = this.storageBucketInput?.value?.trim() || "";
- const forcePathStyle = provider === PROVIDERS.GENERIC;
+
+ // Checkbox is only relevant for Generic S3
+ const forcePathStyle = provider === PROVIDERS.GENERIC
+ ? (this.storageForcePathStyleInput?.checked ?? true)
+ : false;
+
const publicBaseUrl = this.storagePrefixInput?.value?.trim() || "";
if (!accessKeyId || !secretAccessKey || !endpointOrAccount) {
@@ -9348,6 +9385,9 @@ export class ProfileModalController {
this.storagePrefixInput.value = "";
this.storageDefaultInput.checked = false;
this.storageProviderInput.value = "cloudflare_r2";
+ if (this.storageForcePathStyleInput) {
+ this.storageForcePathStyleInput.checked = true;
+ }
this.updateStorageFormVisibility();
this.setStorageFormStatus("", "info");
}
diff --git a/tests/ui/storageService.test.mjs b/tests/ui/storageService.test.mjs
index 9ecb00b0..1fbd2a42 100644
--- a/tests/ui/storageService.test.mjs
+++ b/tests/ui/storageService.test.mjs
@@ -134,7 +134,8 @@ describe("StorageService", () => {
region: "us-east-1",
bucket: "my-bucket",
accessKeyId: "minio-key",
- secretAccessKey: "minio-secret"
+ secretAccessKey: "minio-secret",
+ forcePathStyle: true
};
const meta = {
label: "Local Minio",
@@ -151,6 +152,7 @@ describe("StorageService", () => {
assert.strictEqual(retrieved.region, "us-east-1", "Region should be preserved");
assert.strictEqual(retrieved.bucket, "my-bucket", "Bucket should be preserved");
assert.strictEqual(retrieved.accessKeyId, "minio-key");
+ assert.strictEqual(retrieved.forcePathStyle, true, "ForcePathStyle should be preserved");
assert.strictEqual(retrieved.meta.defaultForUploads, true);
});