mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 03:37:56 +01:00
Add site favicon attachment
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
@extend .mb-4;
|
||||
}
|
||||
|
||||
.siteLogoPreview {
|
||||
.siteLogoPreview, .siteFaviconPreview {
|
||||
@extend .d-block, .my-2;
|
||||
|
||||
position: relative;
|
||||
@@ -18,21 +18,19 @@
|
||||
height: fit-content; /* Adjusts height to match the image */
|
||||
}
|
||||
|
||||
.siteLogoPreview .siteLogoPreviewImg {
|
||||
.siteLogoPreview .siteLogoPreviewImg, .siteFaviconPreview .siteFaviconPreviewImg {
|
||||
display: block;
|
||||
height: 50px; /* Fixed height for the image */
|
||||
width: auto; /* Maintain the aspect ratio */
|
||||
}
|
||||
|
||||
.siteLogoPreview.siteLogoPreviewShouldDelete {
|
||||
.siteLogoPreview.siteLogoPreviewShouldDelete, .siteFaviconPreview.siteFaviconPreviewShouldDelete {
|
||||
border: 2px solid red;
|
||||
|
||||
.siteLogoPreviewImg {
|
||||
.siteFaviconPreviewImg {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.siteLogoActions {
|
||||
@extend .d-flex;
|
||||
}
|
||||
.siteLogoActions, .siteFaviconActions { @extend .d-flex; }
|
||||
}
|
||||
@@ -96,14 +96,22 @@ class TenantsController < ApplicationController
|
||||
# to avoid unique constraint violation
|
||||
params[:tenant][:custom_domain] = nil if params[:tenant][:custom_domain].blank?
|
||||
|
||||
# Handle site logo attachment
|
||||
if params[:tenant][:should_delete_site_logo] == "true"
|
||||
@tenant.site_logo.purge if @tenant.site_logo.attached?
|
||||
elsif params[:tenant][:site_logo].present?
|
||||
# If site_logo is provided, remove the old one if it exists and attach the new one
|
||||
@tenant.site_logo.purge if @tenant.site_logo.attached?
|
||||
@tenant.site_logo.attach(params[:tenant][:site_logo])
|
||||
end
|
||||
|
||||
# Handle site favicon attachment
|
||||
if params[:tenant][:should_delete_site_favicon] == "true"
|
||||
@tenant.site_favicon.purge if @tenant.site_favicon.attached?
|
||||
elsif params[:tenant][:site_favicon].present?
|
||||
@tenant.site_favicon.purge if @tenant.site_favicon.attached?
|
||||
@tenant.site_favicon.attach(params[:tenant][:site_favicon])
|
||||
end
|
||||
|
||||
if @tenant.update(tenant_update_params)
|
||||
render json: @tenant
|
||||
else
|
||||
@@ -141,7 +149,7 @@ class TenantsController < ApplicationController
|
||||
.permitted_attributes_for_update
|
||||
.concat([{
|
||||
tenant_setting_attributes: policy(@tenant.tenant_setting).permitted_attributes_for_update,
|
||||
additional_params: [:should_delete_site_logo]
|
||||
additional_params: [:should_delete_site_logo, :should_delete_site_favicon]
|
||||
}]) # in order to permit nested attributes for tenant_setting
|
||||
)
|
||||
end
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Action } from "redux";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
|
||||
import HttpStatus from "../../constants/http_status";
|
||||
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||
import ITenantSetting from "../../interfaces/ITenantSetting";
|
||||
import ITenantJSON from "../../interfaces/json/ITenant";
|
||||
import { State } from "../../reducers/rootReducer";
|
||||
@@ -50,6 +49,8 @@ interface UpdateTenantParams {
|
||||
siteLogo?: File;
|
||||
shouldDeleteSiteLogo?: boolean;
|
||||
oldSiteLogo?: string;
|
||||
siteFavicon?: File;
|
||||
shouldDeleteSiteFavicon?: boolean;
|
||||
tenantSetting?: ITenantSetting;
|
||||
locale?: string;
|
||||
customDomain?: string;
|
||||
@@ -61,6 +62,8 @@ export const updateTenant = ({
|
||||
siteLogo = null,
|
||||
shouldDeleteSiteLogo = null,
|
||||
oldSiteLogo = null,
|
||||
siteFavicon = null,
|
||||
shouldDeleteSiteFavicon = null,
|
||||
tenantSetting = null,
|
||||
locale = null,
|
||||
customDomain = null,
|
||||
@@ -79,6 +82,10 @@ export const updateTenant = ({
|
||||
body.append('tenant[should_delete_site_logo]', shouldDeleteSiteLogo.toString());
|
||||
if (oldSiteLogo)
|
||||
body.append('tenant[old_site_logo]', oldSiteLogo);
|
||||
if (siteFavicon)
|
||||
body.append('tenant[site_favicon]', siteFavicon);
|
||||
if (shouldDeleteSiteFavicon)
|
||||
body.append('tenant[should_delete_site_favicon]', shouldDeleteSiteFavicon.toString());
|
||||
if (locale)
|
||||
body.append('tenant[locale]', locale);
|
||||
if (customDomain)
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ISiteSettingsGeneralForm {
|
||||
siteName: string;
|
||||
siteLogo: File;
|
||||
oldSiteLogo: string;
|
||||
siteFavicon: File;
|
||||
brandDisplaySetting: string;
|
||||
locale: string;
|
||||
useBrowserLocale: boolean;
|
||||
@@ -49,6 +50,7 @@ export interface ISiteSettingsGeneralForm {
|
||||
interface Props {
|
||||
originForm: ISiteSettingsGeneralForm;
|
||||
siteLogoUrl?: string;
|
||||
siteFaviconUrl?: string;
|
||||
boards: IBoardJSON[];
|
||||
isMultiTenant: boolean;
|
||||
authenticityToken: string;
|
||||
@@ -61,6 +63,8 @@ interface Props {
|
||||
siteLogo: File,
|
||||
shouldDeleteSiteLogo: boolean,
|
||||
oldSiteLogo: string,
|
||||
siteFavicon: File,
|
||||
shouldDeleteSiteFavicon: boolean,
|
||||
brandDisplaySetting: string,
|
||||
locale: string,
|
||||
useBrowserLocale: boolean,
|
||||
@@ -84,6 +88,7 @@ interface Props {
|
||||
const GeneralSiteSettingsP = ({
|
||||
originForm,
|
||||
siteLogoUrl,
|
||||
siteFaviconUrl,
|
||||
boards,
|
||||
isMultiTenant,
|
||||
authenticityToken,
|
||||
@@ -132,6 +137,8 @@ const GeneralSiteSettingsP = ({
|
||||
'siteLogo' in data && data.siteLogo ? data.siteLogo[0] : null,
|
||||
shouldDeleteSiteLogo,
|
||||
data.oldSiteLogo,
|
||||
'siteFavicon' in data && data.siteFavicon ? data.siteFavicon[0] : null,
|
||||
shouldDeleteSiteFavicon,
|
||||
data.brandDisplaySetting,
|
||||
data.locale,
|
||||
data.useBrowserLocale,
|
||||
@@ -182,6 +189,11 @@ const GeneralSiteSettingsP = ({
|
||||
const [showSiteLogoDropzone, setShowSiteLogoDropzone] = React.useState([null, undefined, ''].includes(siteLogoUrl));
|
||||
const [shouldDeleteSiteLogo, setShouldDeleteSiteLogo] = React.useState(false);
|
||||
|
||||
// Site favicon
|
||||
const [siteFaviconFile, setSiteFaviconFile] = React.useState<any>([]);
|
||||
const [showSiteFaviconDropzone, setShowSiteFaviconDropzone] = React.useState([null, undefined, ''].includes(siteFaviconUrl));
|
||||
const [shouldDeleteSiteFavicon, setShouldDeleteSiteFavicon] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box customClass="generalSiteSettingsContainer">
|
||||
@@ -293,7 +305,7 @@ const GeneralSiteSettingsP = ({
|
||||
files={siteLogoFile}
|
||||
setFiles={setSiteLogoFile}
|
||||
onDrop={field.onChange}
|
||||
maxSizeKB={512}
|
||||
maxSizeKB={256}
|
||||
maxFiles={1}
|
||||
/>
|
||||
)}
|
||||
@@ -302,8 +314,72 @@ const GeneralSiteSettingsP = ({
|
||||
</div>
|
||||
|
||||
<div className="formGroup col-6">
|
||||
<label htmlFor="siteFavicon">{ getLabel('tenant', 'site_logo') }</label>
|
||||
{/* <Dropzone /> */}
|
||||
<label htmlFor="siteFavicon">{ getLabel('tenant', 'site_favicon') }</label>
|
||||
|
||||
{
|
||||
siteFaviconUrl &&
|
||||
<div className={`siteFaviconPreview${shouldDeleteSiteFavicon ? ' siteFaviconPreviewShouldDelete' : ''}`}>
|
||||
<img src={siteFaviconUrl} alt={`${originForm.siteName} favicon`} className="siteFaviconPreviewImg" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="siteFaviconActions">
|
||||
{
|
||||
(siteFaviconUrl && !shouldDeleteSiteFavicon) &&
|
||||
(showSiteFaviconDropzone ?
|
||||
<ActionLink
|
||||
onClick={() => setShowSiteFaviconDropzone(false)}
|
||||
icon={<CancelIcon />}
|
||||
>
|
||||
{I18n.t('common.buttons.cancel')}
|
||||
</ActionLink>
|
||||
:
|
||||
<ActionLink
|
||||
onClick={() => setShowSiteFaviconDropzone(true)}
|
||||
icon={<EditIcon />}
|
||||
>
|
||||
{I18n.t('common.buttons.edit')}
|
||||
</ActionLink>)
|
||||
}
|
||||
|
||||
{
|
||||
(siteFaviconUrl && !showSiteFaviconDropzone) &&
|
||||
(shouldDeleteSiteFavicon ?
|
||||
<ActionLink
|
||||
onClick={() => setShouldDeleteSiteFavicon(false)}
|
||||
icon={<CancelIcon />}
|
||||
>
|
||||
{I18n.t('common.buttons.cancel')}
|
||||
</ActionLink>
|
||||
:
|
||||
<ActionLink
|
||||
onClick={() => { setShouldDeleteSiteFavicon(true); setValue('siteFavicon', getValues('siteFavicon'), { shouldDirty: true }) }}
|
||||
icon={<DeleteIcon />}
|
||||
>
|
||||
{I18n.t('common.buttons.delete')}
|
||||
</ActionLink>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
showSiteFaviconDropzone &&
|
||||
<Controller
|
||||
name="siteFavicon"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Dropzone
|
||||
files={siteFaviconFile}
|
||||
setFiles={setSiteFaviconFile}
|
||||
onDrop={field.onChange}
|
||||
maxSizeKB={64}
|
||||
maxFiles={1}
|
||||
accept={['image/x-icon', 'image/icon', 'image/png', 'image/jpeg', 'image/jpg']}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ISiteSettingsGeneralForm } from './GeneralSiteSettingsP';
|
||||
interface Props {
|
||||
originForm: ISiteSettingsGeneralForm;
|
||||
siteLogoUrl?: string;
|
||||
siteFaviconUrl?: string;
|
||||
boards: IBoardJSON[];
|
||||
isMultiTenant: boolean;
|
||||
authenticityToken: string;
|
||||
@@ -31,6 +32,7 @@ class GeneralSiteSettingsRoot extends React.Component<Props> {
|
||||
<GeneralSiteSettings
|
||||
originForm={this.props.originForm}
|
||||
siteLogoUrl={this.props.siteLogoUrl}
|
||||
siteFaviconUrl={this.props.siteFaviconUrl}
|
||||
boards={this.props.boards}
|
||||
isMultiTenant={this.props.isMultiTenant}
|
||||
authenticityToken={this.props.authenticityToken}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Props {
|
||||
onDrop?: any;
|
||||
maxSizeKB?: number;
|
||||
maxFiles?: number;
|
||||
accept?: string[];
|
||||
}
|
||||
|
||||
const Dropzone = ({
|
||||
@@ -17,18 +18,20 @@ const Dropzone = ({
|
||||
onDrop,
|
||||
maxSizeKB = 256,
|
||||
maxFiles = 1,
|
||||
accept = ['image/png', 'image/jpeg', 'image/jpg', 'image/x-icon', 'image/icon', 'image/svg+xml', 'image/svg', 'image/webp'],
|
||||
}: Props) => {
|
||||
const acceptDict = accept.reduce((acc, type) => {
|
||||
acc[type] = [];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragAccept,
|
||||
isDragReject
|
||||
} = useDropzone({
|
||||
accept: {
|
||||
'image/png': [],
|
||||
'image/jpeg': [],
|
||||
'image/jpg': [],
|
||||
},
|
||||
accept: acceptDict,
|
||||
maxSize: maxSizeKB * 1024,
|
||||
maxFiles: maxFiles,
|
||||
onDrop: (acceptedFiles, fileRejections) => {
|
||||
|
||||
@@ -21,6 +21,8 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
siteLogo: File,
|
||||
shouldDeleteSiteLogo: boolean,
|
||||
oldSiteLogo: string,
|
||||
siteFavicon: File,
|
||||
shouldDeleteSiteFavicon: boolean,
|
||||
brandDisplaySetting: TenantSettingBrandDisplay,
|
||||
locale: string,
|
||||
useBrowserLocale: boolean,
|
||||
@@ -44,6 +46,8 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
siteLogo,
|
||||
shouldDeleteSiteLogo,
|
||||
oldSiteLogo,
|
||||
siteFavicon,
|
||||
shouldDeleteSiteFavicon,
|
||||
tenantSetting: {
|
||||
brand_display: brandDisplaySetting,
|
||||
use_browser_locale: useBrowserLocale,
|
||||
|
||||
@@ -13,6 +13,7 @@ class Tenant < ApplicationRecord
|
||||
has_many :default_o_auths, -> { where tenant_id: nil, is_enabled: true }, through: :tenant_default_o_auths, source: :o_auth
|
||||
|
||||
has_one_attached :site_logo, service: ENV.fetch('ACTIVE_STORAGE_PUBLIC_SERVICE', :local).to_sym
|
||||
has_one_attached :site_favicon, service: ENV.fetch('ACTIVE_STORAGE_PUBLIC_SERVICE', :local).to_sym
|
||||
|
||||
enum status: [:active, :pending, :blocked]
|
||||
|
||||
@@ -23,6 +24,12 @@ class Tenant < ApplicationRecord
|
||||
validates :subdomain, presence: true, uniqueness: true
|
||||
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
|
||||
validates :custom_domain, format: { with: /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/ }, uniqueness: true, allow_blank: true, allow_nil: true
|
||||
validates :site_logo,
|
||||
content_type: Rails.application.accepted_image_types,
|
||||
size: { less_than: 256.kilobytes }
|
||||
validates :site_favicon,
|
||||
content_type: Rails.application.accepted_image_types,
|
||||
size: { less_than: 64.kilobytes }
|
||||
|
||||
accepts_nested_attributes_for :tenant_setting, update_only: true
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ class User < ApplicationRecord
|
||||
validates :password, allow_blank: true, length: { in: 6..128 }
|
||||
validates :password, presence: true, on: :create
|
||||
validates :avatar,
|
||||
content_type: ['image/png', 'image/jpg', 'image/jpeg'],
|
||||
size: { less_than: 100.kilobytes }
|
||||
content_type: Rails.application.accepted_image_types,
|
||||
size: { less_than: 128.kilobytes }
|
||||
|
||||
def set_default_role
|
||||
self.role ||= :user
|
||||
|
||||
@@ -5,7 +5,7 @@ class TenantPolicy < ApplicationPolicy
|
||||
|
||||
def permitted_attributes_for_update
|
||||
if user.admin?
|
||||
[:site_name, :site_logo, :old_site_logo, :locale, :custom_domain]
|
||||
[:site_name, :site_logo, :old_site_logo, :site_favicon, :locale, :custom_domain]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
||||
<%= favicon_link_tag asset_path('favicon.png') %>
|
||||
<%= favicon_link_tag @tenant.site_favicon.attached? ? url_for(@tenant.site_favicon) : asset_path('favicon.png') %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
useBrowserLocale: @tenant_setting.use_browser_locale,
|
||||
},
|
||||
siteLogoUrl: @tenant.site_logo.attached? ? url_for(@tenant.site_logo) : nil,
|
||||
siteFaviconUrl: @tenant.site_favicon.attached? ? url_for(@tenant.site_favicon) : nil,
|
||||
boards: @tenant.boards.order(order: :asc),
|
||||
isMultiTenant: Rails.application.multi_tenancy?,
|
||||
authenticityToken: form_authenticity_token
|
||||
|
||||
@@ -40,6 +40,10 @@ module App
|
||||
15
|
||||
end
|
||||
|
||||
def accepted_image_types
|
||||
%w[image/png image/jpg image/jpeg image/x-icon image/icon image/svg+xml image/svg image/webp]
|
||||
end
|
||||
|
||||
def trial_period_days
|
||||
ENV.key?("TRIAL_PERIOD_DAYS") ? ENV["TRIAL_PERIOD_DAYS"].to_i.days : 7.days
|
||||
end
|
||||
|
||||
@@ -128,6 +128,7 @@ en:
|
||||
tenant:
|
||||
site_name: 'Site name'
|
||||
site_logo: 'Site logo'
|
||||
site_favicon: 'Site favicon'
|
||||
subdomain: 'Subdomain'
|
||||
locale: 'Language'
|
||||
custom_domain: 'Custom domain'
|
||||
|
||||
@@ -75,7 +75,7 @@ en:
|
||||
user_staff: 'Staff'
|
||||
language_supported: '%{language} supported'
|
||||
powered_by: 'Powered by'
|
||||
drag_and_drop: 'Drag and drop or click to upload images (only .png or .jpg, max %{maxCount} files, max %{maxSize}KB each)'
|
||||
drag_and_drop: 'Drag and drop or click to upload images (max %{maxCount} files, max %{maxSize}KB each)'
|
||||
buttons:
|
||||
new: 'New'
|
||||
edit: 'Edit'
|
||||
|
||||
Reference in New Issue
Block a user