mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor Upload Modal to use read-only storage status
Simplified the Upload Modal by removing inline storage configuration forms. The modal now displays the active storage provider (read-only) or prompts the user to configure storage via the Profile Modal if missing. Changes: - Refactored `js/ui/components/UploadModal.js` to consume stored credentials directly. - Updated `components/upload-modal.html` to remove complex forms. - Updated `tests/ui/uploadModal-integration.test.mjs` to match new UI structure. - Removed race-prone logic for saving credentials within the upload flow.
This commit is contained in:
1 parent
b6260c1352
commit
2062ba0889
3 files changed
+240
-973
No files matched your search
+14
-186
@@ -115,7 +115,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>Storage Configuration</span>
|
||||
<span>Storage Status</span>
|
||||
</button>
|
||||
|
||||
<!-- Storage Settings Container -->
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="storage-lock-status" class="text-xs text-muted">Locked 🔒</span>
|
||||
<button type="button" id="btn-storage-unlock" class="text-xs text-accent hover:underline hidden">Unlock</button>
|
||||
<button type="button" id="btn-manage-storage" class="text-xs text-muted hover:text-text hover:underline">Manage Storage</button>
|
||||
<button type="button" id="btn-manage-storage" class="text-xs text-muted hover:text-text hover:underline">Manage</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 text-xs">
|
||||
@@ -150,190 +150,18 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Editable Form (Legacy/Fallback) -->
|
||||
<div id="storage-form-view" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-text">Storage Credentials</h4>
|
||||
<span id="storage-status" class="text-xs text-muted hidden">Stored locally</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-xs text-muted">
|
||||
Required for direct file uploads.
|
||||
<a href="#view=upload-content" id="link-r2-help" class="text-accent hover:underline font-medium ml-1">
|
||||
Read the storage setup guide.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Provider Selector -->
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="inline-flex items-center rounded-full bg-surface-alt p-1 text-xs shadow-inner"
|
||||
role="group"
|
||||
aria-label="Storage provider selector"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-storage-provider-r2"
|
||||
class="rounded-full px-4 py-1 font-medium transition-colors text-text bg-surface shadow-sm"
|
||||
aria-pressed="true"
|
||||
>
|
||||
Cloudflare
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-storage-provider-s3"
|
||||
class="rounded-full px-4 py-1 font-medium transition-colors text-muted hover:text-text"
|
||||
aria-pressed="false"
|
||||
>
|
||||
S3 Compatible
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="section-storage-r2" class="space-y-4">
|
||||
<!-- Target Bucket Display -->
|
||||
<div class="space-y-1">
|
||||
<label for="input-r2-bucket-name" class="text-xs font-medium text-text">Target Bucket Name (Required)</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="input-r2-bucket-name"
|
||||
type="text"
|
||||
class="input text-xs bg-surface-alt text-muted cursor-text select-all"
|
||||
readonly
|
||||
placeholder="Log in to see your bucket name"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-copy-bucket"
|
||||
class="btn-ghost px-2 text-xs border border-border/30"
|
||||
aria-label="Copy bucket name"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-4xs text-muted">Create a bucket with this exact name in Cloudflare R2.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Credentials -->
|
||||
<div id="step-credentials" class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<label for="input-r2-account" class="text-xs font-medium text-text">Account ID / Endpoint</label>
|
||||
<input id="input-r2-account" type="text" class="input text-xs" placeholder="xxxxxxxx... or https://..." />
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<label for="input-r2-key" class="text-xs font-medium text-text">Access Key ID</label>
|
||||
<input id="input-r2-key" type="text" class="input text-xs" placeholder="AKIA..." />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="input-r2-secret" class="text-xs font-medium text-text">Secret Access Key</label>
|
||||
<input id="input-r2-secret" type="password" class="input text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button type="button" id="btn-next-step" class="btn btn-sm">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Verification -->
|
||||
<div id="step-verification" class="hidden space-y-3 pt-2 border-t border-border/30">
|
||||
<div class="space-y-1">
|
||||
<label for="input-r2-domain" class="text-xs font-medium text-text">Public URL</label>
|
||||
<input id="input-r2-domain" type="text" class="input text-xs" placeholder="https://pub-xxx.r2.dev" />
|
||||
<p class="text-4xs text-muted leading-tight">
|
||||
The public URL where your files will be accessible.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Container for Verification Failures -->
|
||||
<div id="container-verification-error" class="hidden bg-critical/10 border border-critical/30 rounded p-3 text-xs text-critical space-y-2">
|
||||
<p id="text-verification-error" class="font-medium">Verification Failed</p>
|
||||
<a id="link-cloudflare-guide" href="#" target="_blank" class="block text-center bg-critical/10 hover:bg-critical/20 py-1.5 rounded font-medium transition-colors">
|
||||
Check Provider Settings →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button type="button" id="btn-back-step" class="btn-ghost px-2 text-xs">Back</button>
|
||||
<button type="button" id="btn-save-settings" class="btn btn-sm">Verify & Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="section-storage-s3" class="hidden space-y-4">
|
||||
<p class="text-xs text-muted">
|
||||
Configure a generic S3-compatible bucket for direct uploads.
|
||||
</p>
|
||||
<p class="text-xs text-status-warning bg-status-warning/10 p-2 rounded">
|
||||
Warning: Browser-held keys are only safe for trusted operators.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="quickS3ManifestToggle" class="checkbox" />
|
||||
<label for="quickS3ManifestToggle" class="text-xs font-medium text-text select-none">
|
||||
Use presigned manifest
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="quickS3CredentialsContainer" class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<label for="input-s3-endpoint" class="text-xs font-medium text-text">Endpoint</label>
|
||||
<input id="input-s3-endpoint" type="text" class="input text-xs" placeholder="https://host[:port]" />
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<label for="input-s3-region" class="text-xs font-medium text-text">Region (optional)</label>
|
||||
<input id="input-s3-region" type="text" class="input text-xs" placeholder="auto" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="input-s3-bucket" class="text-xs font-medium text-text">Bucket (explicit override if needed)</label>
|
||||
<input id="input-s3-bucket" type="text" class="input text-xs" placeholder="my-bucket" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<label for="input-s3-key" class="text-xs font-medium text-text">Access Key ID</label>
|
||||
<input id="input-s3-key" type="text" class="input text-xs" placeholder="AKIA..." />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="input-s3-secret" class="text-xs font-medium text-text">Secret Access Key</label>
|
||||
<input id="input-s3-secret" type="password" class="input text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="input-s3-public-base" class="text-xs font-medium text-text">Public Base URL Override</label>
|
||||
<input id="input-s3-public-base" type="text" class="input text-xs" placeholder="https://cdn.example.com/bucket" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4 text-xs text-muted">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" id="check-s3-path-style" class="checkbox" />
|
||||
<span>Force path-style URLs</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" id="check-s3-create-bucket" class="checkbox" />
|
||||
<span>Create bucket if missing</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="quickS3ManifestContainer" class="hidden space-y-2">
|
||||
<label for="quickS3ManifestJson" class="text-xs font-medium text-text">Manifest JSON</label>
|
||||
<p class="text-4xs text-muted leading-tight">
|
||||
The manifest must be produced externally. This app does not create or sign presigned URLs.
|
||||
</p>
|
||||
<textarea
|
||||
id="quickS3ManifestJson"
|
||||
rows="6"
|
||||
class="textarea text-xs font-mono"
|
||||
placeholder='{"bucket": "...", "key": "...", "uploadId": "...", "parts": [{"partNumber": 1, "url": "..."}]}'
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button type="button" id="btn-save-s3-settings" class="btn btn-sm">Verify & Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Empty State / Not Configured -->
|
||||
<div id="storage-empty-view" class="space-y-4 text-center py-4">
|
||||
<p class="text-sm text-muted">
|
||||
Storage is not configured.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
id="btn-configure-storage"
|
||||
class="btn btn-sm btn-secondary"
|
||||
>
|
||||
Configure Storage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+158
-763
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,7 @@ describe("UploadModal Integration", () => {
|
||||
global.window = dom.window;
|
||||
global.document = dom.document;
|
||||
global.EventTarget = dom.window.EventTarget;
|
||||
global.alert = (msg) => {}; // Polyfill alert for tests
|
||||
|
||||
// UploadModal uses HTML elements
|
||||
global.HTMLElement = dom.window.HTMLElement;
|
||||
@@ -37,13 +38,13 @@ describe("UploadModal Integration", () => {
|
||||
if (dom) dom.cleanup();
|
||||
});
|
||||
|
||||
it("should auto-fill default R2 connection when loaded and unlocked", async () => {
|
||||
it("should detect default R2 connection and show summary when loaded and unlocked", async () => {
|
||||
// Setup Mock Data
|
||||
const pubkey = "pk1";
|
||||
const r2Conn = {
|
||||
id: "r2-def",
|
||||
provider: "cloudflare_r2",
|
||||
meta: { label: "My R2", defaultForUploads: true, baseDomain: "https://r2.pub" },
|
||||
meta: { label: "My R2", defaultForUploads: true, baseDomain: "https://r2.pub", bucket: "test-bucket" },
|
||||
accountId: "acc-id",
|
||||
accessKeyId: "acc-key",
|
||||
secretAccessKey: "acc-secret"
|
||||
@@ -66,9 +67,7 @@ describe("UploadModal Integration", () => {
|
||||
setGlobalModalState: () => {},
|
||||
});
|
||||
|
||||
// Override load to use our container directly or mocked fetch
|
||||
// We must mock global.fetch to return the HTML content since JSDOM doesn't access file system.
|
||||
|
||||
// Updated Mock HTML matching refactored component
|
||||
const mockHtml = `
|
||||
<div id="uploadModal" class="hidden">
|
||||
<form id="unifiedUploadForm">
|
||||
@@ -76,21 +75,54 @@ describe("UploadModal Integration", () => {
|
||||
<button id="btn-mode-external"></button>
|
||||
<div id="section-source-upload">
|
||||
<button id="btn-storage-settings"></button>
|
||||
|
||||
<!-- Storage Settings Container -->
|
||||
<div id="section-storage-settings" class="hidden">
|
||||
<input id="input-r2-account" />
|
||||
<input id="input-r2-key" />
|
||||
<input id="input-r2-secret" />
|
||||
<input id="input-r2-domain" />
|
||||
<button id="btn-save-settings"></button>
|
||||
<button id="btn-storage-unlock" class="hidden"></button>
|
||||
<!-- Summary View -->
|
||||
<div id="storage-summary-view" class="hidden">
|
||||
<span id="storage-lock-status">Locked 🔒</span>
|
||||
<button id="btn-storage-unlock" class="hidden">Unlock</button>
|
||||
<button id="btn-manage-storage">Manage</button>
|
||||
<span id="summary-provider">--</span>
|
||||
<span id="summary-bucket">--</span>
|
||||
<span id="summary-url-style">--</span>
|
||||
<span id="summary-copy"></span>
|
||||
</div>
|
||||
|
||||
<!-- Empty View -->
|
||||
<div id="storage-empty-view" class="hidden">
|
||||
<button id="btn-configure-storage">Configure Storage</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="storage-status"></div>
|
||||
<div id="storage-lock-status"></div>
|
||||
|
||||
<div id="upload-status-text"></div>
|
||||
<div id="upload-percent-text"></div>
|
||||
<progress id="input-progress"></progress>
|
||||
</div>
|
||||
<div id="section-source-external"></div>
|
||||
|
||||
<!-- Inputs -->
|
||||
<input id="input-title" />
|
||||
<textarea id="input-description"></textarea>
|
||||
<input id="input-thumbnail" />
|
||||
<input id="input-thumbnail-file" />
|
||||
<input id="input-file" />
|
||||
<input id="input-url" />
|
||||
<input id="input-magnet" />
|
||||
|
||||
<input id="check-nsfw" type="checkbox" />
|
||||
<input id="check-kids" type="checkbox" />
|
||||
<input id="check-comments" type="checkbox" />
|
||||
<input id="check-summary-unlock" type="checkbox" />
|
||||
|
||||
<button id="btn-advanced-toggle"></button>
|
||||
<button id="btn-thumbnail-file"></button>
|
||||
|
||||
<div id="section-advanced"></div>
|
||||
|
||||
<button id="btn-submit"></button>
|
||||
<div id="submit-status"></div>
|
||||
</form>
|
||||
<button id="btn-submit"></button>
|
||||
<div id="submit-status"></div>
|
||||
<button id="closeUploadModal"></button>
|
||||
</div>
|
||||
`;
|
||||
@@ -107,14 +139,21 @@ describe("UploadModal Integration", () => {
|
||||
|
||||
await modal.load({ container });
|
||||
|
||||
// Verify Inputs populated
|
||||
const accInput = document.getElementById("input-r2-account");
|
||||
const keyInput = document.getElementById("input-r2-key");
|
||||
const secretInput = document.getElementById("input-r2-secret");
|
||||
// Verify State
|
||||
assert.strictEqual(modal.storageConfigured, true, "Should be configured");
|
||||
assert.notStrictEqual(modal.activeCredentials, null, "Credentials should be loaded");
|
||||
assert.strictEqual(modal.activeCredentials.accountId, "acc-id", "Credentials should match mock");
|
||||
|
||||
assert.strictEqual(accInput.value, "acc-id", "Account ID should be populated");
|
||||
assert.strictEqual(keyInput.value, "acc-key", "Access Key should be populated");
|
||||
assert.strictEqual(secretInput.value, "acc-secret", "Secret should be populated");
|
||||
// Verify UI
|
||||
const summaryView = document.getElementById("storage-summary-view");
|
||||
const emptyView = document.getElementById("storage-empty-view");
|
||||
const providerLabel = document.getElementById("summary-provider");
|
||||
const bucketLabel = document.getElementById("summary-bucket");
|
||||
|
||||
assert.strictEqual(summaryView.classList.contains("hidden"), false, "Summary view should be visible");
|
||||
assert.strictEqual(emptyView.classList.contains("hidden"), true, "Empty view should be hidden");
|
||||
assert.strictEqual(providerLabel.textContent, "Cloudflare R2", "Provider label should match");
|
||||
assert.strictEqual(bucketLabel.textContent, "test-bucket", "Bucket label should match");
|
||||
});
|
||||
|
||||
it("should handle locked state and unlock flow", async () => {
|
||||
@@ -124,7 +163,12 @@ describe("UploadModal Integration", () => {
|
||||
// Mock StorageService Behavior
|
||||
mockStorageService.isUnlocked = (pk) => unlocked;
|
||||
mockStorageService.unlock = async (pk) => { unlocked = true; };
|
||||
mockStorageService.listConnections = async () => []; // Return empty first to verify logic runs without crashing
|
||||
// Return a connection so it's "configured" but locked
|
||||
mockStorageService.listConnections = async () => [{
|
||||
id: "conn1",
|
||||
provider: "cloudflare_r2",
|
||||
meta: { bucket: "locked-bucket" }
|
||||
}];
|
||||
|
||||
// Setup DOM
|
||||
const container = document.createElement("div");
|
||||
@@ -135,7 +179,7 @@ describe("UploadModal Integration", () => {
|
||||
const modal = new UploadModal({
|
||||
storageService: mockStorageService,
|
||||
getCurrentPubkey: () => pubkey,
|
||||
authService: { signer: {} }, // Mock signer presence
|
||||
authService: { signer: { signEvent: async () => {} } }, // Mock signer presence
|
||||
setGlobalModalState: () => {},
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user