From 130a3b0d1cbafa94e22f7f1d6f5bca25803f59e0 Mon Sep 17 00:00:00 2001 From: StarFleetCPTN Date: Wed, 16 Apr 2025 19:24:16 -0700 Subject: [PATCH] feat: Enhance storage provider form and configuration - Updated the storage provider form to categorize options into server-based, object storage, web storage, and cloud storage. - Improved input placeholders for clarity and added descriptions for fields. - Introduced new storage provider types: WebDAV, Nextcloud, Wasabi, MinIO, and B2. - Added JavaScript functionality to auto-populate default ports based on selected provider type. - Updated .gitignore to include test results and logs, and added Playwright configuration for testing. --- .gitignore | 25 ++- components/storage_provider_form.templ | 251 +++++++++++++++++-------- internal/db/storage_provider.go | 11 +- package-lock.json | 89 +++++++++ package.json | 11 +- playwright.config.js | 64 +++++++ 6 files changed, 366 insertions(+), 85 deletions(-) create mode 100644 playwright.config.js diff --git a/.gitignore b/.gitignore index 5a30c78..f73eaec 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ configs/ # Ignore the backups directory backups/ +# Ignore the tests directory +test-results/ +playwright-report/ # Ignore Dirs /source/ /destination/ @@ -81,4 +84,24 @@ docs/static/search/ docs/static/js/ docs/node_modules/ docs/.env* -docs/*.log \ No newline at end of file +docs/*.log + +# Added by Claude Task Master +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +dev-debug.log +# Environment variables +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# OS specific +# Task files +tasks.json +tasks/ \ No newline at end of file diff --git a/components/storage_provider_form.templ b/components/storage_provider_form.templ index 88293d6..83ec726 100644 --- a/components/storage_provider_form.templ +++ b/components/storage_provider_form.templ @@ -92,51 +92,95 @@ templ formFields(data StorageProviderFormData) { > Select provider type - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -148,26 +192,26 @@ templ formFields(data StorageProviderFormData) {
- +
-

Leave empty for default (SFTP: 22, FTP: 21, SMB: 445)

+

Leave empty for default (SFTP: 22, FTP: 21, SMB: 445, WebDAV: 80/443)

- +
- + if data.IsEdit {

Leave empty to keep the current password

} @@ -176,14 +220,14 @@ templ formFields(data StorageProviderFormData) { @@ -207,31 +251,31 @@ templ formFields(data StorageProviderFormData) {
- +
- +
- +
- +
- + if data.IsEdit {

Leave empty to keep the current secret key

} @@ -245,13 +289,13 @@ templ formFields(data StorageProviderFormData) {
- +
- + if data.IsEdit {

Leave empty to keep the current client secret

} @@ -260,7 +304,7 @@ templ formFields(data StorageProviderFormData) { @@ -291,7 +335,7 @@ templ formFields(data StorageProviderFormData) {
- +

Absolute path on the server's file system

@@ -318,62 +362,107 @@ templ providerFormScript() { // Set current active fields on page load document.addEventListener('DOMContentLoaded', function() { toggleProviderFields(); + + // Add event listener to track if user manually changes the port + const portField = document.getElementById('port'); + portField.addEventListener('input', function() { + // Mark the field as user-modified + this.dataset.userModified = 'true'; + }); }); function toggleProviderFields() { + const provider = document.getElementById('type').value; + // Hide all provider fields first const providerFieldsets = document.querySelectorAll('.provider-fields'); providerFieldsets.forEach(fieldset => { fieldset.classList.add('hidden'); }); - // Show the appropriate fields based on the selected provider type - const providerType = document.getElementById('type').value; - console.log("Provider type selected:", providerType); + // Auto-populate port based on provider type + const portField = document.getElementById('port'); + const isPortEmpty = portField.value === ''; + const userModified = portField.dataset.userModified === 'true'; - // SFTP/FTP/SMB fields - if (['sftp', 'ftp', 'smb'].includes(providerType)) { + // Only set default port if field is empty or hasn't been modified by user + if (isPortEmpty || !userModified) { + if (provider === 'sftp') { + portField.value = '22'; + } else if (provider === 'hetzner') { + portField.value = '23'; + } else if (provider === 'ftp') { + portField.value = '21'; + } else if (provider === 'smb') { + portField.value = '445'; + } else if (provider === 'webdav' || provider === 'nextcloud') { + portField.value = '443'; + } else { + portField.value = ''; + } + // Reset user modified flag if we're setting it programmatically + portField.dataset.userModified = 'false'; + } + + // Show the appropriate fields based on the selected provider type + console.log("Provider type selected:", provider); + + // Server-based connection fields (SFTP, FTP, Hetzner) + if (['sftp', 'ftp', 'hetzner'].includes(provider)) { document.getElementById('sftp-ftp-fields').classList.remove('hidden'); - // SFTP-specific fields - if (providerType === 'sftp') { + // SFTP/Hetzner-specific fields + if (provider === 'sftp' || provider === 'hetzner') { document.getElementById('key-file-field').classList.remove('hidden'); } - // SMB-specific fields - if (providerType === 'smb') { - document.getElementById('domain-field').classList.remove('hidden'); - } - // FTP-specific fields - if (providerType === 'ftp') { + if (provider === 'ftp') { document.getElementById('passive-mode-field').classList.remove('hidden'); } } - // S3 fields - if (providerType === 's3' || providerType === 'hetzner') { + // WebDAV-based fields + if (['webdav', 'nextcloud'].includes(provider)) { + document.getElementById('sftp-ftp-fields').classList.remove('hidden'); + // No special fields currently for WebDAV + } + + // SMB-specific fields + if (provider === 'smb') { + document.getElementById('sftp-ftp-fields').classList.remove('hidden'); + document.getElementById('domain-field').classList.remove('hidden'); + } + + // S3-compatible storage + if (['s3', 'wasabi', 'minio', 'b2'].includes(provider)) { document.getElementById('s3-fields').classList.remove('hidden'); } - // Cloud storage fields - if (['onedrive', 'gdrive', 'gphotos'].includes(providerType)) { + // Google services + if (['gdrive', 'gphotos'].includes(provider)) { document.getElementById('cloud-fields').classList.remove('hidden'); // Google Drive specific fields - if (providerType === 'gdrive') { + if (provider === 'gdrive') { document.getElementById('drive-id-field').classList.remove('hidden'); document.getElementById('team-drive-field').classList.remove('hidden'); } // Google Photos specific fields - if (providerType === 'gphotos') { + if (provider === 'gphotos') { document.getElementById('readonly-field').classList.remove('hidden'); } } + // OneDrive + if (provider === 'onedrive') { + document.getElementById('cloud-fields').classList.remove('hidden'); + // No special fields for OneDrive currently + } + // Local filesystem fields - if (providerType === 'local') { + if (provider === 'local') { document.getElementById('local-fields').classList.remove('hidden'); } } diff --git a/internal/db/storage_provider.go b/internal/db/storage_provider.go index 943feca..0419748 100644 --- a/internal/db/storage_provider.go +++ b/internal/db/storage_provider.go @@ -18,6 +18,11 @@ const ( ProviderTypeSMB StorageProviderType = "smb" ProviderTypeHetzner StorageProviderType = "hetzner" ProviderTypeLocal StorageProviderType = "local" + ProviderTypeWebDAV StorageProviderType = "webdav" + ProviderTypeNextcloud StorageProviderType = "nextcloud" + ProviderTypeB2 StorageProviderType = "b2" + ProviderTypeWasabi StorageProviderType = "wasabi" + ProviderTypeMinio StorageProviderType = "minio" ) // StorageProvider represents a connection to a storage service @@ -164,7 +169,9 @@ func (sp *StorageProvider) SetAuthenticated(value bool) { func (sp *StorageProvider) IsOAuthProvider() bool { return sp.Type == ProviderTypeOneDrive || sp.Type == ProviderTypeGoogleDrive || - sp.Type == ProviderTypeGooglePhoto + sp.Type == ProviderTypeGooglePhoto || + sp.Type == ProviderTypeWebDAV || + sp.Type == ProviderTypeNextcloud } // RequiresEncryption returns true if the provider has sensitive fields that need encryption @@ -183,7 +190,7 @@ func (sp *StorageProvider) GetSensitiveFields() map[string]string { if sp.Password != "" { sensitiveFields["Password"] = sp.Password } - case ProviderTypeS3: + case ProviderTypeS3, ProviderTypeWasabi, ProviderTypeMinio, ProviderTypeB2: if sp.SecretKey != "" { sensitiveFields["SecretKey"] = sp.SecretKey } diff --git a/package-lock.json b/package-lock.json index 373ebaf..7faf9c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "gomft", "version": "1.0.0", + "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-free": "^6.4.0", "alpinejs": "^3.13.5", @@ -14,6 +15,11 @@ "flowbite": "^2.2.1", "htmx.org": "^1.9.10", "tailwindcss": "^3.4.1" + }, + "devDependencies": { + "@playwright/test": "^1.46.0", + "dotenv": "^16.5.0", + "expect-playwright": "^0.8.0" } }, "node_modules/@alloc/quick-lru": { @@ -516,6 +522,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", + "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -748,6 +770,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -798,6 +833,13 @@ "@esbuild/win32-x64": "0.20.2" } }, + "node_modules/expect-playwright": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.8.0.tgz", + "integrity": "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1247,6 +1289,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", diff --git a/package.json b/package.json index d07a96f..8415176 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "scripts": { "build": "node build.js", "watch": "node build.js --watch", - "postinstall": "npm run build" + "postinstall": "npm run build", + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:headed": "playwright test --headed", + "test:debug": "playwright test --debug" }, "dependencies": { "@fortawesome/fontawesome-free": "^6.4.0", @@ -15,5 +19,10 @@ "flowbite": "^2.2.1", "htmx.org": "^1.9.10", "tailwindcss": "^3.4.1" + }, + "devDependencies": { + "@playwright/test": "^1.46.0", + "dotenv": "^16.5.0", + "expect-playwright": "^0.8.0" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..37adfd4 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,64 @@ +// @ts-check +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Capture screenshot on failure */ + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'go run main.go', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + }, +}); \ No newline at end of file