mirror of https://github.com/docusealco/docuseal
feat: v1.1.0 — Custom Logo Support (#10)
* Fix ESLint quote-props errors in signature_step.vue * feat: add per-account custom logo support with env var fallback - Add has_one_attached :logo to Account model - Add Docuseal.custom_logo_url helper reading CUSTOM_LOGO_URL env var - Create AccountLogoController with upload/delete actions - Update shared/_logo.html.erb to render: account.logo > CUSTOM_LOGO_URL > default SVG - Replace logo_form placeholder with actual upload form + preview + delete - Add route for account_logo under /settings - Add i18n translations for logo flash messages * test: add Playwright tests for custom logo upload, delete, and env fallback --------- Co-authored-by: mario.pander <developbob50@gmail.com> Co-authored-by: David Pierre-Francois <david.pierre-francois@videotron.com>pull/639/head
parent
4aef3a0d8f
commit
db47f795ef
@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountLogoController < ApplicationController
|
||||||
|
before_action :load_account
|
||||||
|
authorize_resource :account
|
||||||
|
|
||||||
|
def create
|
||||||
|
file = params[:logo]
|
||||||
|
|
||||||
|
return redirect_to settings_personalization_path, alert: I18n.t('unable_to_upload_logo') if file.blank?
|
||||||
|
|
||||||
|
current_account.logo.attach(file)
|
||||||
|
|
||||||
|
redirect_to settings_personalization_path, notice: I18n.t('logo_has_been_saved')
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
current_account.logo.purge
|
||||||
|
|
||||||
|
redirect_to settings_personalization_path, notice: I18n.t('logo_has_been_removed')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account
|
||||||
|
@account = current_account
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1 +1,28 @@
|
|||||||
<%= render 'logo_placeholder' %>
|
<div class="my-4">
|
||||||
|
<% if current_account.logo.attached? %>
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<img src="<%= url_for(current_account.logo) %>" alt="<%= t('company_logo') %>" class="h-16 max-w-xs object-contain" data-logo-preview="true" />
|
||||||
|
<%= button_to t('remove'), settings_account_logo_path, method: :delete,
|
||||||
|
class: 'btn btn-sm btn-outline btn-error',
|
||||||
|
data: { turbo_confirm: t('are_you_sure_') } %>
|
||||||
|
</div>
|
||||||
|
<% elsif Docuseal.custom_logo_url.present? %>
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<img src="<%= Docuseal.custom_logo_url %>" alt="<%= t('company_logo') %>" class="h-16 max-w-xs object-contain" data-logo-preview="true" />
|
||||||
|
<span class="badge badge-info"><%= t('default') %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= form_with url: settings_account_logo_path, method: :post, multipart: true, class: 'flex items-end space-x-4' do |f| %>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium"><%= t('upload_logo') %></span>
|
||||||
|
</label>
|
||||||
|
<%= f.file_field :logo, accept: 'image/png,image/svg+xml,image/jpeg,image/gif',
|
||||||
|
class: 'file-input file-input-bordered w-full max-w-xs',
|
||||||
|
id: 'account_logo_input' %>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<%= t('upload_logo') %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 74 B |
@ -0,0 +1,116 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { loginAsAdmin } from './helpers/auth';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Phase 1.1 — Custom Logo Support.
|
||||||
|
// Upload a PNG logo, verify it appears in the navbar,
|
||||||
|
// delete it and verify the default SVG returns,
|
||||||
|
// then check CUSTOM_LOGO_URL env override behaviour.
|
||||||
|
|
||||||
|
const FIXTURE_LOGO = path.resolve(__dirname, 'fixtures', 'test-logo.png');
|
||||||
|
|
||||||
|
test.describe('Custom logo', () => {
|
||||||
|
test('upload a PNG logo and verify it appears in the navbar', async ({ page }) => {
|
||||||
|
await loginAsAdmin(page);
|
||||||
|
await page.goto('/settings/personalization');
|
||||||
|
|
||||||
|
const fileInput = page.locator('#account_logo_input');
|
||||||
|
await expect(fileInput).toBeVisible();
|
||||||
|
|
||||||
|
await fileInput.setInputFiles(FIXTURE_LOGO);
|
||||||
|
await page.getByRole('button', { name: /upload logo/i }).click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Verify success flash
|
||||||
|
await expect(page.locator('body')).toContainText(/logo has been saved/i);
|
||||||
|
|
||||||
|
// Verify the logo preview appears on the personalization page
|
||||||
|
const preview = page.locator('img[data-logo-preview="true"]');
|
||||||
|
await expect(preview).toBeVisible();
|
||||||
|
|
||||||
|
// Navigate to home and verify the custom logo is in the navbar
|
||||||
|
await page.goto('/');
|
||||||
|
const navbarLogo = page.locator('img[data-custom-logo="true"]');
|
||||||
|
await expect(navbarLogo).toBeVisible();
|
||||||
|
|
||||||
|
// Ensure the default SVG is NOT rendered
|
||||||
|
const defaultSvg = page.locator('a[href="/"] svg');
|
||||||
|
await expect(defaultSvg).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete the logo and verify the default DocuSeal logo returns', async ({ page }) => {
|
||||||
|
await loginAsAdmin(page);
|
||||||
|
|
||||||
|
// First upload a logo so we can delete it
|
||||||
|
await page.goto('/settings/personalization');
|
||||||
|
const fileInput = page.locator('#account_logo_input');
|
||||||
|
await fileInput.setInputFiles(FIXTURE_LOGO);
|
||||||
|
await page.getByRole('button', { name: /upload logo/i }).click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Confirm it was uploaded
|
||||||
|
await expect(page.locator('img[data-logo-preview="true"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Click Remove and accept the confirmation dialog
|
||||||
|
page.on('dialog', (dialog) => dialog.accept());
|
||||||
|
await page.getByRole('button', { name: /remove/i }).click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Verify success flash
|
||||||
|
await expect(page.locator('body')).toContainText(/logo has been removed/i);
|
||||||
|
|
||||||
|
// Navigate to home and verify the default SVG logo is back
|
||||||
|
await page.goto('/');
|
||||||
|
const defaultSvg = page.locator('a[href="/"] svg');
|
||||||
|
await expect(defaultSvg.first()).toBeVisible();
|
||||||
|
|
||||||
|
// Ensure no custom logo img tag is present
|
||||||
|
const customLogo = page.locator('img[data-custom-logo="true"]');
|
||||||
|
await expect(customLogo).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CUSTOM_LOGO_URL env wins over default but per-account upload wins over env', async ({ page }) => {
|
||||||
|
// This test validates the fallback chain conceptually.
|
||||||
|
// Since we cannot easily set env vars on the running server, we verify:
|
||||||
|
// 1. When no logo is uploaded and no env var is set, the default SVG renders.
|
||||||
|
// 2. When a logo is uploaded, the custom img renders (already tested above).
|
||||||
|
//
|
||||||
|
// The env var behaviour is verified by checking the shared/_logo.html.erb
|
||||||
|
// template logic, but we can at least verify the default state.
|
||||||
|
|
||||||
|
await loginAsAdmin(page);
|
||||||
|
|
||||||
|
// Ensure no logo is attached (delete if present)
|
||||||
|
await page.goto('/settings/personalization');
|
||||||
|
const removeBtn = page.getByRole('button', { name: /remove/i });
|
||||||
|
if (await removeBtn.isVisible()) {
|
||||||
|
page.on('dialog', (dialog) => dialog.accept());
|
||||||
|
await removeBtn.click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify default SVG renders on home page
|
||||||
|
await page.goto('/');
|
||||||
|
const defaultSvg = page.locator('a[href="/"] svg');
|
||||||
|
await expect(defaultSvg.first()).toBeVisible();
|
||||||
|
|
||||||
|
// Upload a logo — it should override any default
|
||||||
|
await page.goto('/settings/personalization');
|
||||||
|
const fileInput = page.locator('#account_logo_input');
|
||||||
|
await fileInput.setInputFiles(FIXTURE_LOGO);
|
||||||
|
await page.getByRole('button', { name: /upload logo/i }).click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Verify custom logo renders in navbar (would also win over env var)
|
||||||
|
await page.goto('/');
|
||||||
|
const customLogo = page.locator('img[data-custom-logo="true"]');
|
||||||
|
await expect(customLogo).toBeVisible();
|
||||||
|
await expect(page.locator('a[href="/"] svg')).toHaveCount(0);
|
||||||
|
|
||||||
|
// Clean up: remove the uploaded logo
|
||||||
|
await page.goto('/settings/personalization');
|
||||||
|
page.on('dialog', (dialog) => dialog.accept());
|
||||||
|
await page.getByRole('button', { name: /remove/i }).click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in new issue