mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 03:37:56 +01:00
Add upload for site logo
This commit is contained in:
@@ -350,4 +350,49 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
scale: 80%;
|
scale: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
border: 2px dashed var(--astuto-grey);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-accept {
|
||||||
|
border-color: rgb(0, 189, 0);
|
||||||
|
}
|
||||||
|
.dropzone-reject {
|
||||||
|
border-color: rgb(255, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
padding: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.thumbnailInner {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.thumbnailImage {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,10 @@
|
|||||||
.generalSiteSettingsSubmit {
|
.generalSiteSettingsSubmit {
|
||||||
@extend .mb-4;
|
@extend .mb-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.siteLogoPreview {
|
||||||
|
@extend .d-block, .my-2;
|
||||||
|
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,11 @@ class TenantsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: Current.tenant_or_raise!
|
tenant = Current.tenant_or_raise!
|
||||||
|
|
||||||
|
tenant.attributes.merge(site_logo_url: tenant.site_logo.attached? ? url_for(tenant.site_logo) : nil)
|
||||||
|
|
||||||
|
render json: tenant
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -92,6 +96,12 @@ class TenantsController < ApplicationController
|
|||||||
# to avoid unique constraint violation
|
# to avoid unique constraint violation
|
||||||
params[:tenant][:custom_domain] = nil if params[:tenant][:custom_domain].blank?
|
params[:tenant][:custom_domain] = nil if params[:tenant][:custom_domain].blank?
|
||||||
|
|
||||||
|
# If site_logo is provided, remove the old one if it exists and attach the new one
|
||||||
|
if params[:tenant][:site_logo].present?
|
||||||
|
@tenant.site_logo.purge if @tenant.site_logo.attached?
|
||||||
|
@tenant.site_logo.attach(params[:tenant][:site_logo])
|
||||||
|
end
|
||||||
|
|
||||||
if @tenant.update(tenant_update_params)
|
if @tenant.update(tenant_update_params)
|
||||||
render json: @tenant
|
render json: @tenant
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ const tenantUpdateFailure = (error: string): TenantUpdateFailureAction => ({
|
|||||||
|
|
||||||
interface UpdateTenantParams {
|
interface UpdateTenantParams {
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
siteLogo?: string;
|
siteLogo?: File;
|
||||||
|
oldSiteLogo?: string;
|
||||||
tenantSetting?: ITenantSetting;
|
tenantSetting?: ITenantSetting;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
customDomain?: string;
|
customDomain?: string;
|
||||||
@@ -57,6 +58,7 @@ interface UpdateTenantParams {
|
|||||||
export const updateTenant = ({
|
export const updateTenant = ({
|
||||||
siteName = null,
|
siteName = null,
|
||||||
siteLogo = null,
|
siteLogo = null,
|
||||||
|
oldSiteLogo = null,
|
||||||
tenantSetting = null,
|
tenantSetting = null,
|
||||||
locale = null,
|
locale = null,
|
||||||
customDomain = null,
|
customDomain = null,
|
||||||
@@ -64,24 +66,34 @@ export const updateTenant = ({
|
|||||||
}: UpdateTenantParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
}: UpdateTenantParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
dispatch(tenantUpdateStart());
|
dispatch(tenantUpdateStart());
|
||||||
|
|
||||||
const tenant = Object.assign({},
|
|
||||||
siteName !== null ? { site_name: siteName } : null,
|
|
||||||
siteLogo !== null ? { site_logo: siteLogo } : null,
|
|
||||||
locale !== null ? { locale } : null,
|
|
||||||
customDomain !== null ? { custom_domain: customDomain } : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = JSON.stringify({
|
const body = new FormData();
|
||||||
tenant: {
|
|
||||||
...tenant,
|
if (siteName)
|
||||||
tenant_setting_attributes: tenantSetting,
|
body.append('tenant[site_name]', siteName);
|
||||||
},
|
if (siteLogo)
|
||||||
|
body.append('tenant[site_logo]', siteLogo);
|
||||||
|
if (oldSiteLogo)
|
||||||
|
body.append('tenant[old_site_logo]', oldSiteLogo);
|
||||||
|
if (locale)
|
||||||
|
body.append('tenant[locale]', locale);
|
||||||
|
if (customDomain)
|
||||||
|
body.append('tenant[custom_domain]', customDomain);
|
||||||
|
|
||||||
|
Object.entries(tenantSetting).forEach(([key, value]) => {
|
||||||
|
body.append(`tenant[tenant_setting_attributes][${key}]`, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// body.forEach((value, key) => {
|
||||||
|
// console.log(key, value);
|
||||||
|
// });
|
||||||
|
|
||||||
const res = await fetch(`/tenants/0`, {
|
const res = await fetch(`/tenants/0`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: buildRequestHeaders(authenticityToken),
|
headers: {
|
||||||
|
'X-CSRF-Token': authenticityToken,
|
||||||
|
// do not set Content-Type header when using FormData
|
||||||
|
},
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
|
||||||
import I18n from 'i18n-js';
|
import I18n from 'i18n-js';
|
||||||
|
|
||||||
import Box from '../../common/Box';
|
import Box from '../../common/Box';
|
||||||
@@ -21,11 +21,13 @@ import { DangerText, SmallMutedText } from '../../common/CustomTexts';
|
|||||||
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
|
||||||
import IBoardJSON from '../../../interfaces/json/IBoard';
|
import IBoardJSON from '../../../interfaces/json/IBoard';
|
||||||
import ActionLink from '../../common/ActionLink';
|
import ActionLink from '../../common/ActionLink';
|
||||||
import { LearnMoreIcon } from '../../common/Icons';
|
import { CancelIcon, EditIcon, LearnMoreIcon } from '../../common/Icons';
|
||||||
|
import Dropzone from '../../common/Dropzone';
|
||||||
|
|
||||||
export interface ISiteSettingsGeneralForm {
|
export interface ISiteSettingsGeneralForm {
|
||||||
siteName: string;
|
siteName: string;
|
||||||
siteLogo: string;
|
siteLogo: any; // TODO: Change to File type
|
||||||
|
oldSiteLogo: string;
|
||||||
brandDisplaySetting: string;
|
brandDisplaySetting: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
useBrowserLocale: boolean;
|
useBrowserLocale: boolean;
|
||||||
@@ -46,6 +48,7 @@ export interface ISiteSettingsGeneralForm {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
originForm: ISiteSettingsGeneralForm;
|
originForm: ISiteSettingsGeneralForm;
|
||||||
|
siteLogoUrl?: string;
|
||||||
boards: IBoardJSON[];
|
boards: IBoardJSON[];
|
||||||
isMultiTenant: boolean;
|
isMultiTenant: boolean;
|
||||||
authenticityToken: string;
|
authenticityToken: string;
|
||||||
@@ -55,7 +58,8 @@ interface Props {
|
|||||||
|
|
||||||
updateTenant(
|
updateTenant(
|
||||||
siteName: string,
|
siteName: string,
|
||||||
siteLogo: string,
|
siteLogo: File,
|
||||||
|
oldSiteLogo: string,
|
||||||
brandDisplaySetting: string,
|
brandDisplaySetting: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
useBrowserLocale: boolean,
|
useBrowserLocale: boolean,
|
||||||
@@ -78,6 +82,7 @@ interface Props {
|
|||||||
|
|
||||||
const GeneralSiteSettingsP = ({
|
const GeneralSiteSettingsP = ({
|
||||||
originForm,
|
originForm,
|
||||||
|
siteLogoUrl,
|
||||||
boards,
|
boards,
|
||||||
isMultiTenant,
|
isMultiTenant,
|
||||||
authenticityToken,
|
authenticityToken,
|
||||||
@@ -91,10 +96,11 @@ const GeneralSiteSettingsP = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty, isSubmitSuccessful, errors },
|
formState: { isDirty, isSubmitSuccessful, errors },
|
||||||
watch,
|
watch,
|
||||||
|
control,
|
||||||
} = useForm<ISiteSettingsGeneralForm>({
|
} = useForm<ISiteSettingsGeneralForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
siteName: originForm.siteName,
|
siteName: originForm.siteName,
|
||||||
siteLogo: originForm.siteLogo,
|
oldSiteLogo: originForm.oldSiteLogo,
|
||||||
brandDisplaySetting: originForm.brandDisplaySetting,
|
brandDisplaySetting: originForm.brandDisplaySetting,
|
||||||
locale: originForm.locale,
|
locale: originForm.locale,
|
||||||
useBrowserLocale: originForm.useBrowserLocale,
|
useBrowserLocale: originForm.useBrowserLocale,
|
||||||
@@ -115,9 +121,12 @@ const GeneralSiteSettingsP = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ISiteSettingsGeneralForm> = data => {
|
const onSubmit: SubmitHandler<ISiteSettingsGeneralForm> = data => {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
updateTenant(
|
updateTenant(
|
||||||
data.siteName,
|
data.siteName,
|
||||||
data.siteLogo,
|
data.siteLogo[0],
|
||||||
|
data.oldSiteLogo,
|
||||||
data.brandDisplaySetting,
|
data.brandDisplaySetting,
|
||||||
data.locale,
|
data.locale,
|
||||||
data.useBrowserLocale,
|
data.useBrowserLocale,
|
||||||
@@ -163,6 +172,10 @@ const GeneralSiteSettingsP = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Site logo
|
||||||
|
const [siteLogoFile, setSiteLogoFile] = React.useState<any>([]);
|
||||||
|
const [showSiteLogoDropzone, setShowSiteLogoDropzone] = React.useState([null, undefined, ''].includes(siteLogoUrl));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box customClass="generalSiteSettingsContainer">
|
<Box customClass="generalSiteSettingsContainer">
|
||||||
@@ -170,7 +183,7 @@ const GeneralSiteSettingsP = ({
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="formRow">
|
<div className="formRow">
|
||||||
<div className="formGroup col-4">
|
<div className="formGroup col-6">
|
||||||
<label htmlFor="siteName">{ getLabel('tenant', 'site_name') }</label>
|
<label htmlFor="siteName">{ getLabel('tenant', 'site_name') }</label>
|
||||||
<input
|
<input
|
||||||
{...register('siteName', { required: true })}
|
{...register('siteName', { required: true })}
|
||||||
@@ -180,17 +193,7 @@ const GeneralSiteSettingsP = ({
|
|||||||
<DangerText>{errors.siteName && getValidationMessage(errors.siteName.type, 'tenant', 'site_name')}</DangerText>
|
<DangerText>{errors.siteName && getValidationMessage(errors.siteName.type, 'tenant', 'site_name')}</DangerText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formGroup col-4">
|
<div className="formGroup col-6">
|
||||||
<label htmlFor="siteLogo">{ getLabel('tenant', 'site_logo') }</label>
|
|
||||||
<input
|
|
||||||
{...register('siteLogo')}
|
|
||||||
placeholder='https://example.com/logo.png'
|
|
||||||
id="siteLogo"
|
|
||||||
className="formControl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="formGroup col-4">
|
|
||||||
<label htmlFor="brandSetting">{ getLabel('tenant_setting', 'brand_display') }</label>
|
<label htmlFor="brandSetting">{ getLabel('tenant_setting', 'brand_display') }</label>
|
||||||
<select
|
<select
|
||||||
{...register('brandDisplaySetting')}
|
{...register('brandDisplaySetting')}
|
||||||
@@ -211,6 +214,66 @@ const GeneralSiteSettingsP = ({
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden oldSiteLogo field for backwards compatibility */}
|
||||||
|
<div className="formGroup d-none">
|
||||||
|
<label htmlFor="oldSiteLogo">{ getLabel('tenant', 'site_logo') }</label>
|
||||||
|
<input
|
||||||
|
{...register('oldSiteLogo')}
|
||||||
|
placeholder='https://example.com/logo.png'
|
||||||
|
id="oldSiteLogo"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formRow">
|
||||||
|
<div className="formGroup col-6">
|
||||||
|
<label htmlFor="siteLogo">{ getLabel('tenant', 'site_logo') }</label>
|
||||||
|
|
||||||
|
{ siteLogoUrl && <img src={siteLogoUrl} alt={`${originForm.siteName} logo`} className="siteLogoPreview" /> }
|
||||||
|
|
||||||
|
{
|
||||||
|
siteLogoUrl &&
|
||||||
|
(showSiteLogoDropzone ?
|
||||||
|
<ActionLink
|
||||||
|
onClick={() => setShowSiteLogoDropzone(false)}
|
||||||
|
icon={<CancelIcon />}
|
||||||
|
>
|
||||||
|
{I18n.t('common.buttons.cancel')}
|
||||||
|
</ActionLink>
|
||||||
|
:
|
||||||
|
<ActionLink
|
||||||
|
onClick={() => setShowSiteLogoDropzone(true)}
|
||||||
|
icon={<EditIcon />}
|
||||||
|
>
|
||||||
|
{I18n.t('common.buttons.edit')}
|
||||||
|
</ActionLink>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showSiteLogoDropzone &&
|
||||||
|
<Controller
|
||||||
|
name="siteLogo"
|
||||||
|
control={control}
|
||||||
|
defaultValue={[]}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Dropzone
|
||||||
|
files={siteLogoFile}
|
||||||
|
setFiles={setSiteLogoFile}
|
||||||
|
onDrop={field.onChange}
|
||||||
|
maxSizeKB={512}
|
||||||
|
maxFiles={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formGroup col-6">
|
||||||
|
<label htmlFor="siteFavicon">{ getLabel('tenant', 'site_logo') }</label>
|
||||||
|
{/* <Dropzone /> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formGroup">
|
<div className="formGroup">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ISiteSettingsGeneralForm } from './GeneralSiteSettingsP';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
originForm: ISiteSettingsGeneralForm;
|
originForm: ISiteSettingsGeneralForm;
|
||||||
|
siteLogoUrl?: string;
|
||||||
boards: IBoardJSON[];
|
boards: IBoardJSON[];
|
||||||
isMultiTenant: boolean;
|
isMultiTenant: boolean;
|
||||||
authenticityToken: string;
|
authenticityToken: string;
|
||||||
@@ -29,6 +30,7 @@ class GeneralSiteSettingsRoot extends React.Component<Props> {
|
|||||||
<Provider store={this.store}>
|
<Provider store={this.store}>
|
||||||
<GeneralSiteSettings
|
<GeneralSiteSettings
|
||||||
originForm={this.props.originForm}
|
originForm={this.props.originForm}
|
||||||
|
siteLogoUrl={this.props.siteLogoUrl}
|
||||||
boards={this.props.boards}
|
boards={this.props.boards}
|
||||||
isMultiTenant={this.props.isMultiTenant}
|
isMultiTenant={this.props.isMultiTenant}
|
||||||
authenticityToken={this.props.authenticityToken}
|
authenticityToken={this.props.authenticityToken}
|
||||||
|
|||||||
79
app/javascript/components/common/Dropzone.tsx
Normal file
79
app/javascript/components/common/Dropzone.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {useDropzone} from 'react-dropzone';
|
||||||
|
import I18n from 'i18n-js';
|
||||||
|
import { SmallMutedText } from './CustomTexts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
files: any[];
|
||||||
|
setFiles: React.Dispatch<React.SetStateAction<any[]>>;
|
||||||
|
onDrop?: any;
|
||||||
|
maxSizeKB?: number;
|
||||||
|
maxFiles?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dropzone = ({
|
||||||
|
files,
|
||||||
|
setFiles,
|
||||||
|
onDrop,
|
||||||
|
maxSizeKB = 256,
|
||||||
|
maxFiles = 1,
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
isDragAccept,
|
||||||
|
isDragReject
|
||||||
|
} = useDropzone({
|
||||||
|
accept: {
|
||||||
|
'image/png': [],
|
||||||
|
'image/jpeg': [],
|
||||||
|
'image/jpg': [],
|
||||||
|
},
|
||||||
|
maxSize: maxSizeKB * 1024,
|
||||||
|
maxFiles: maxFiles,
|
||||||
|
onDrop: (acceptedFiles, fileRejections) => {
|
||||||
|
if (fileRejections.length > 0) {
|
||||||
|
alert(I18n.t('common.errors.invalid_file', { errors: fileRejections.map(rejection => rejection.errors[0].message).join(', ') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiles(acceptedFiles.map(file => Object.assign(file, {
|
||||||
|
preview: URL.createObjectURL(file)
|
||||||
|
})));
|
||||||
|
if (onDrop) {
|
||||||
|
onDrop(acceptedFiles);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const thumbnails = files.map(file => (
|
||||||
|
<div className="thumbnail" key={file.name}>
|
||||||
|
<div className="thumbnailInner">
|
||||||
|
<img
|
||||||
|
src={file.preview}
|
||||||
|
className="thumbnailImage"
|
||||||
|
// Revoke data uri after image is loaded
|
||||||
|
onLoad={() => { URL.revokeObjectURL(file.preview) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Make sure to revoke the data uris to avoid memory leaks, will run on unmount
|
||||||
|
return () => files.forEach(file => URL.revokeObjectURL(file.preview));
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div {...getRootProps({className: 'dropzone' + (isDragAccept ? ' dropzone-accept' : isDragReject ? ' dropzone-reject' : '')})}>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<SmallMutedText>{I18n.t('common.drag_and_drop', { maxCount: maxFiles, maxSize: maxSizeKB })}</SmallMutedText>
|
||||||
|
</div>
|
||||||
|
<aside className="thumbnailsContainer">
|
||||||
|
{thumbnails}
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dropzone;
|
||||||
@@ -18,7 +18,8 @@ const mapStateToProps = (state: State) => ({
|
|||||||
const mapDispatchToProps = (dispatch: any) => ({
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
updateTenant(
|
updateTenant(
|
||||||
siteName: string,
|
siteName: string,
|
||||||
siteLogo: string,
|
siteLogo: File,
|
||||||
|
oldSiteLogo: string,
|
||||||
brandDisplaySetting: TenantSettingBrandDisplay,
|
brandDisplaySetting: TenantSettingBrandDisplay,
|
||||||
locale: string,
|
locale: string,
|
||||||
useBrowserLocale: boolean,
|
useBrowserLocale: boolean,
|
||||||
@@ -40,6 +41,7 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||||||
return dispatch(updateTenant({
|
return dispatch(updateTenant({
|
||||||
siteName,
|
siteName,
|
||||||
siteLogo,
|
siteLogo,
|
||||||
|
oldSiteLogo,
|
||||||
tenantSetting: {
|
tenantSetting: {
|
||||||
brand_display: brandDisplaySetting,
|
brand_display: brandDisplaySetting,
|
||||||
use_browser_locale: useBrowserLocale,
|
use_browser_locale: useBrowserLocale,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const buildRequestHeaders = (authenticityToken: string) => ({
|
const buildRequestHeaders = (authenticityToken: string, contentType: string = 'application/json') => ({
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': contentType,
|
||||||
'X-CSRF-Token': authenticityToken,
|
'X-CSRF-Token': authenticityToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
interface ITenant {
|
interface ITenant {
|
||||||
id: number;
|
id: number;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
siteLogo: string;
|
oldSiteLogo: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
customDomain?: string;
|
customDomain?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
interface ITenantJSON {
|
interface ITenantJSON {
|
||||||
id: number;
|
id: number;
|
||||||
site_name: string;
|
site_name: string;
|
||||||
site_logo: string;
|
old_site_logo: string;
|
||||||
brand_display_setting: string;
|
brand_display_setting: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
custom_domain?: string;
|
custom_domain?: string;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Tenant < ApplicationRecord
|
|||||||
# used to query all globally enabled default oauths that are also enabled by the specific tenant
|
# used to query all globally enabled default oauths that are also enabled by the specific tenant
|
||||||
has_many :default_o_auths, -> { where tenant_id: nil, is_enabled: true }, through: :tenant_default_o_auths, source: :o_auth
|
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
|
||||||
|
|
||||||
enum status: [:active, :pending, :blocked]
|
enum status: [:active, :pending, :blocked]
|
||||||
|
|
||||||
after_initialize :set_default_status, if: :new_record?
|
after_initialize :set_default_status, if: :new_record?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TenantPolicy < ApplicationPolicy
|
|||||||
|
|
||||||
def permitted_attributes_for_update
|
def permitted_attributes_for_update
|
||||||
if user.admin?
|
if user.admin?
|
||||||
[:site_name, :site_logo, :locale, :custom_domain]
|
[:site_name, :site_logo, :old_site_logo, :locale, :custom_domain]
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
<%=
|
<%=
|
||||||
link_to get_url_for_logo, class: "brand#{@tenant_setting.logo_links_to == 'nothing' ? ' brandDisabled' : ''}", tabindex: @tenant_setting.logo_links_to == 'nothing' ? -1 : 0 do
|
link_to get_url_for_logo, class: "brand#{@tenant_setting.logo_links_to == 'nothing' ? ' brandDisabled' : ''}", tabindex: @tenant_setting.logo_links_to == 'nothing' ? -1 : 0 do
|
||||||
app_name = content_tag :span, @tenant.site_name
|
app_name = content_tag :span, @tenant.site_name
|
||||||
logo = image_tag(@tenant.site_logo ? @tenant.site_logo : "", class: 'logo', skip_pipeline: true)
|
logo_url = @tenant.site_logo.attached? ? url_for(@tenant.site_logo) : @tenant.old_site_logo
|
||||||
|
|
||||||
|
logo = image_tag(logo_url || "", class: 'logo', skip_pipeline: true)
|
||||||
|
|
||||||
if @tenant_setting.brand_display == "name_and_logo"
|
if @tenant_setting.brand_display == "name_and_logo"
|
||||||
logo + app_name
|
logo + app_name
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{
|
{
|
||||||
originForm: {
|
originForm: {
|
||||||
siteName: @tenant.site_name,
|
siteName: @tenant.site_name,
|
||||||
siteLogo: @tenant.site_logo,
|
oldSiteLogo: @tenant.old_site_logo,
|
||||||
brandDisplaySetting: @tenant_setting.brand_display,
|
brandDisplaySetting: @tenant_setting.brand_display,
|
||||||
showVoteCount: @tenant_setting.show_vote_count,
|
showVoteCount: @tenant_setting.show_vote_count,
|
||||||
showVoteButtonInBoard: @tenant_setting.show_vote_button_in_board,
|
showVoteButtonInBoard: @tenant_setting.show_vote_button_in_board,
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
locale: @tenant.locale,
|
locale: @tenant.locale,
|
||||||
useBrowserLocale: @tenant_setting.use_browser_locale,
|
useBrowserLocale: @tenant_setting.use_browser_locale,
|
||||||
},
|
},
|
||||||
|
siteLogoUrl: @tenant.site_logo.attached? ? url_for(@tenant.site_logo) : nil,
|
||||||
boards: @tenant.boards.order(order: :asc),
|
boards: @tenant.boards.order(order: :asc),
|
||||||
isMultiTenant: Rails.application.multi_tenancy?,
|
isMultiTenant: Rails.application.multi_tenancy?,
|
||||||
authenticityToken: form_authenticity_token
|
authenticityToken: form_authenticity_token
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ en:
|
|||||||
or: 'or'
|
or: 'or'
|
||||||
errors:
|
errors:
|
||||||
unknown: 'An unknown error occurred, please try again'
|
unknown: 'An unknown error occurred, please try again'
|
||||||
|
invalid_file: 'The files you uploaded are invalid: %{errors}'
|
||||||
validations:
|
validations:
|
||||||
required: '%{attribute} is required'
|
required: '%{attribute} is required'
|
||||||
email: 'Invalid email'
|
email: 'Invalid email'
|
||||||
@@ -74,6 +75,7 @@ en:
|
|||||||
user_staff: 'Staff'
|
user_staff: 'Staff'
|
||||||
language_supported: '%{language} supported'
|
language_supported: '%{language} supported'
|
||||||
powered_by: 'Powered by'
|
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)'
|
||||||
buttons:
|
buttons:
|
||||||
new: 'New'
|
new: 'New'
|
||||||
edit: 'Edit'
|
edit: 'Edit'
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class RenameSiteLogoColumnTenant < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
rename_column :tenants, :site_logo, :old_site_logo
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2025_01_03_152157) do
|
ActiveRecord::Schema.define(version: 2025_01_21_093808) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -233,7 +233,7 @@ ActiveRecord::Schema.define(version: 2025_01_03_152157) do
|
|||||||
|
|
||||||
create_table "tenants", force: :cascade do |t|
|
create_table "tenants", force: :cascade do |t|
|
||||||
t.string "site_name", null: false
|
t.string "site_name", null: false
|
||||||
t.string "site_logo"
|
t.string "old_site_logo"
|
||||||
t.string "subdomain", null: false
|
t.string "subdomain", null: false
|
||||||
t.string "locale", default: "en"
|
t.string "locale", default: "en"
|
||||||
t.string "custom_domain"
|
t.string "custom_domain"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-beautiful-dnd": "13.1.0",
|
"react-beautiful-dnd": "13.1.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
|
"react-dropzone": "14.3.5",
|
||||||
"react-gravatar": "2.6.3",
|
"react-gravatar": "2.6.3",
|
||||||
"react-hook-form": "7.33.1",
|
"react-hook-form": "7.33.1",
|
||||||
"react-icons": "5.0.1",
|
"react-icons": "5.0.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :tenant do
|
factory :tenant do
|
||||||
site_name { "MySiteName" }
|
site_name { "MySiteName" }
|
||||||
site_logo { "" }
|
old_site_logo { "" }
|
||||||
sequence(:subdomain) { |n| "mysubdomain#{n}" }
|
sequence(:subdomain) { |n| "mysubdomain#{n}" }
|
||||||
locale { "en" }
|
locale { "en" }
|
||||||
custom_domain { nil }
|
custom_domain { nil }
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ feature 'site settings: general', type: :system, js: true do
|
|||||||
new_site_logo = 'https://www.example.com/logo.png'
|
new_site_logo = 'https://www.example.com/logo.png'
|
||||||
|
|
||||||
expect(page).to have_field('Site name', with: Current.tenant.site_name)
|
expect(page).to have_field('Site name', with: Current.tenant.site_name)
|
||||||
expect(page).to have_field('Site logo', with: Current.tenant.site_logo)
|
expect(page).to have_field('Site logo', with: Current.tenant.old_site_logo)
|
||||||
|
|
||||||
expect(Current.tenant.site_name).not_to eq(new_site_name)
|
expect(Current.tenant.site_name).not_to eq(new_site_name)
|
||||||
expect(Current.tenant.site_logo).not_to eq(new_site_logo)
|
expect(Current.tenant.old_site_logo).not_to eq(new_site_logo)
|
||||||
|
|
||||||
fill_in 'Site name', with: new_site_name
|
fill_in 'Site name', with: new_site_name
|
||||||
fill_in 'Site logo', with: new_site_logo
|
fill_in 'Site logo', with: new_site_logo
|
||||||
@@ -34,7 +34,7 @@ feature 'site settings: general', type: :system, js: true do
|
|||||||
|
|
||||||
t = Tenant.first
|
t = Tenant.first
|
||||||
expect(t.site_name).to eq(new_site_name)
|
expect(t.site_name).to eq(new_site_name)
|
||||||
expect(t.site_logo).to eq(new_site_logo)
|
expect(t.old_site_logo).to eq(new_site_logo)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'lets edit the site language' do
|
it 'lets edit the site language' do
|
||||||
|
|||||||
26
yarn.lock
26
yarn.lock
@@ -1723,6 +1723,11 @@ anymatch@~3.1.2:
|
|||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
picomatch "^2.0.4"
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
|
attr-accept@^2.2.4:
|
||||||
|
version "2.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e"
|
||||||
|
integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==
|
||||||
|
|
||||||
babel-loader@9.1.2:
|
babel-loader@9.1.2:
|
||||||
version "9.1.2"
|
version "9.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c"
|
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c"
|
||||||
@@ -2142,6 +2147,13 @@ fastest-levenshtein@^1.0.12:
|
|||||||
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
|
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
|
||||||
integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
|
integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
|
||||||
|
|
||||||
|
file-selector@^2.1.0:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4"
|
||||||
|
integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.7.0"
|
||||||
|
|
||||||
fill-range@^7.1.1:
|
fill-range@^7.1.1:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||||
@@ -2816,6 +2828,15 @@ react-dom@16.14.0:
|
|||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.19.1"
|
scheduler "^0.19.1"
|
||||||
|
|
||||||
|
react-dropzone@14.3.5:
|
||||||
|
version "14.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.5.tgz#1a8bd312c8a353ec78ef402842ccb3589c225add"
|
||||||
|
integrity sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==
|
||||||
|
dependencies:
|
||||||
|
attr-accept "^2.2.4"
|
||||||
|
file-selector "^2.1.0"
|
||||||
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
react-floater@^0.7.9:
|
react-floater@^0.7.9:
|
||||||
version "0.7.9"
|
version "0.7.9"
|
||||||
resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.7.9.tgz#b15a652e817f200bfa42a2023ee8d3105803b968"
|
resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.7.9.tgz#b15a652e817f200bfa42a2023ee8d3105803b968"
|
||||||
@@ -3331,6 +3352,11 @@ tslib@^1.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||||
|
|
||||||
|
tslib@^2.7.0:
|
||||||
|
version "2.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||||
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
|
|
||||||
turbolinks@5.2.0:
|
turbolinks@5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c"
|
resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c"
|
||||||
|
|||||||
Reference in New Issue
Block a user