Add custom CSS (#264)

This commit is contained in:
Riccardo Graziosi
2024-01-23 18:50:42 +01:00
committed by GitHub
parent 653e139a9e
commit d7e7db9f72
40 changed files with 421 additions and 54 deletions

View File

@@ -28,3 +28,4 @@
@import 'components/SiteSettings/Roadmap';
@import 'components/SiteSettings/Users';
@import 'components/SiteSettings/Authentication';
@import 'components/SiteSettings/Appearance/';

View File

@@ -24,7 +24,7 @@
}
.mutedText {
color: $astuto-grey;
color: var(--astuto-grey);
}
.smallMutedText {

View File

@@ -9,8 +9,8 @@
}
.form-control:focus {
border-color: $astuto-light-grey;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 4px rgba(0, 0, 0, 0.6);
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem var(--primary-color-light);
}
.new_user, .edit_user {
@@ -33,7 +33,7 @@
margin-top: 24px;
a { color: $primary-color; }
a { color: var(--astuto-black); }
}
.formRow {

View File

@@ -54,11 +54,16 @@
font-size: 14px;
text-align: left;
a:hover { color: var(--primary-color); }
&.active {
@extend .badgeLight;
a { color: var(--primary-color) !important };
}
}
.nav-item.active {
@extend .badgeLight;
}
}
.profileNav {
@@ -75,12 +80,12 @@
}
.fullname {
color: $astuto-black;
color: var(--astuto-black);
vertical-align: middle;
}
}
.dropdown-item:active {
background-color: $primary-color;
background-color: var(--primary-color);
}
}

View File

@@ -1,17 +1,50 @@
::selection {
background-color: var(--primary-color-light);
}
body {
background-color: var(--background-color);
}
.btnPrimary {
@extend .btn;
color: white;
background-color: var(--primary-color);
border-color: var(--primary-color-dark);
&:hover { color: white; }
&:focus { box-shadow: 0 0 0 0.25rem var(--primary-color-light); }
}
.btnOutlinePrimary {
@extend .btn;
background-color: transparent;
color: var(--primary-color);
border-color: var(--primary-color);
&:hover {
color: white;
background-color: var(--primary-color);
}
&:focus { box-shadow: 0 0 0 0.25rem var(--primary-color-light); }
}
.card {
@extend .card;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.04),0 2px 4px -1px rgba(0, 0, 0, 0.03);
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
color: $primary-color;
color: var(--astuto-black);
padding: 8px;
}
.card3D {
@extend
.card;
@extend .card;
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
@@ -89,17 +122,17 @@
.align-self-stretch;
a {
color: $astuto-grey;
color: var(--astuto-grey);
font-weight: 500;
&:hover {
color: $astuto-black;
color: var(--primary-color);
}
}
.nav-link.active {
color: $astuto-black;
background-color: $astuto-light-grey;
color: var(--primary-color);
background-color: var(--astuto-grey-light);
}
}
@@ -134,7 +167,7 @@
.badgeLight {
@extend .badge-light;
background-color: $astuto-light-grey;
background-color: var(--astuto-grey-light);
}
.container {
@@ -147,7 +180,7 @@
}
.turbolinks-progress-bar {
background-color: $primary-color;
background-color: var(--primary-color);
height: 2px;
}
@@ -162,6 +195,14 @@
& > input[type="checkbox"] {
@extend .custom-control-input;
&:focus ~ label::before {
box-shadow: 0 0 0 0.25rem var(--primary-color-light);
}
&:active ~ label::before {
background-color: var(--primary-color);
}
}
& > label {
@@ -173,14 +214,23 @@
}
& > input:checked ~ label::before {
background-color: $primary-color !important;
border-color: $primary-color !important;
background-color: var(--primary-color) !important;
border-color: var(--primary-color-dark) !important;
}
& > input:focus ~ label::before {
border-color: var(--primary-color-dark) !important;
}
}
.selectPicker {
@extend
.custom-select;
&:focus {
box-shadow: 0 0 0 0.25rem var(--primary-color-light);
border-color: var(--primary-color-dark);
}
}
.link {
@@ -189,13 +239,16 @@
}
.actionLink {
color: $astuto-black;
color: var(--astuto-black);
display: flex;
cursor: pointer;
align-self: center;
margin-right: 12px;
&:hover { text-decoration: underline !important; }
&:hover {
color: var(--primary-color);
text-decoration: underline !important;
}
svg {
margin-right: 4px;
@@ -203,7 +256,7 @@
}
&.actionLinkDisabled {
color: $astuto-grey !important;
color: var(--astuto-grey) !important;
text-decoration: none !important;
cursor: not-allowed;

View File

@@ -11,7 +11,7 @@
}
.newPostContainer {
background-color: $astuto-light-grey;
background-color: var(--astuto-grey-light);
text-align: center;
.boardTitle {
@@ -37,6 +37,8 @@
.postStatusListItemLink {
@extend
.flex-grow-1;
&:hover { text-decoration: none; }
}
.postStatusListItem {
@@ -84,7 +86,7 @@
.p-3;
height: 140px;
color: $primary-color;
color: var(--astuto-black);
@include media-breakpoint-down(sm) {
height: auto;

View File

@@ -14,13 +14,13 @@
border-bottom: $like_button_size solid rgba(35,35,35,.2);
&:hover {
border-bottom-color: $primary-color;
border-bottom-color: var(--primary-color);
cursor: pointer;
}
}
.likeButton.liked {
border-bottom-color: $primary-color;
border-bottom-color: var(--primary-color);
}
.likeCountLabel {

View File

@@ -125,7 +125,7 @@
@extend
.my-4;
color: $primary-color;
color: var(--astuto-black);
p:last-child {
@extend .mb-0;

View File

@@ -11,7 +11,7 @@
.p-0;
width: 32%;
background-color: $astuto-light-grey;
background-color: var(--astuto-grey-light);
@include media-breakpoint-down(sm) {
width: 100%;
@@ -35,8 +35,7 @@
}
.postLink {
@extend
.my-2;
@extend .my-2;
&:hover { text-decoration: none; }
}

View File

@@ -0,0 +1,4 @@
#customCss {
width: 100%;
height: 300px;
}

View File

@@ -29,7 +29,7 @@
box-sizing: border-box;
flex: 0 0 28%;
background-color: $astuto-light-grey;
background-color: var(--astuto-grey-light);
overflow: hidden;
height: 150px;
@@ -47,7 +47,9 @@
padding: 8px 4px;
text-transform: uppercase;
.titleText { @extend .align-self-center; }
.titleText {
@extend .align-self-center;
}
}
}
}

View File

@@ -1,5 +1,14 @@
$primary-color: #333;
:root {
// Theme palette (supposed to be customized)
--primary-color: rgb(51, 51, 51);
--background-color: rgb(255, 255, 255);
$astuto-black: #333;
$astuto-grey: rgba(0, 0, 0, 0.5);
$astuto-light-grey: rgba(178, 178, 178, 0.2);
// Theme palette shades (supposed to be computed from theme palette)
--primary-color-light: color-mix(in srgb,var(--primary-color), #fff 85%);
--primary-color-dark: color-mix(in srgb,var(--primary-color), #000 20%);
// Astuto colors (supposed to be fixed)
--astuto-black: rgb(51, 51, 51);
--astuto-grey: rgba(0, 0, 0, 0.5);
--astuto-grey-light: rgba(178, 178, 178, 0.2);
}

View File

@@ -2,7 +2,7 @@ class SiteSettingsController < ApplicationController
include ApplicationHelper
before_action :authenticate_admin,
only: [:general, :boards, :post_statuses, :roadmap, :authentication]
only: [:general, :authentication, :boards, :post_statuses, :roadmap, :appearance]
before_action :authenticate_moderator,
only: [:users]
@@ -10,6 +10,9 @@ class SiteSettingsController < ApplicationController
def general
end
def authentication
end
def boards
end
@@ -19,9 +22,9 @@ class SiteSettingsController < ApplicationController
def roadmap
end
def users
def appearance
end
def authentication
def users
end
end

View File

@@ -168,7 +168,7 @@ class NewPost extends React.Component<Props, State> {
}
</Button>
:
<a href="/users/sign_in" className="btn btn-dark">
<a href="/users/sign_in" className="btn btnPrimary">
{I18n.t('board.new_post.login_button')}
</a>
}

View File

@@ -0,0 +1,118 @@
import * as React from 'react';
import { useEffect } from 'react';
import I18n from 'i18n-js';
import Box from '../../common/Box';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import Button from '../../common/Button';
import HttpStatus from '../../../constants/http_status';
import { getLabel } from '../../../helpers/formUtils';
import { SubmitHandler, useForm } from 'react-hook-form';
import ActionLink from '../../common/ActionLink';
import { LearnMoreIcon } from '../../common/Icons';
export interface ISiteSettingsAppearanceForm {
customCss: string;
}
interface Props {
originForm: ISiteSettingsAppearanceForm;
authenticityToken: string;
areUpdating: boolean;
error: string;
updateTenant(
customCss: string,
authenticityToken: string
): Promise<any>;
}
const AppearanceSiteSettingsP = ({
originForm,
authenticityToken,
areUpdating,
error,
updateTenant,
}: Props) => {
const {
register,
handleSubmit,
formState: { isDirty, isSubmitSuccessful },
watch,
} = useForm<ISiteSettingsAppearanceForm>({
defaultValues: {
customCss: originForm.customCss,
},
});
const customCss = watch('customCss');
const onSubmit: SubmitHandler<ISiteSettingsAppearanceForm> = data => {
updateTenant(
data.customCss,
authenticityToken
).then(res => {
if (res?.status !== HttpStatus.OK) return;
window.location.reload();
});
};
useEffect(() => {
const style = document.createElement('style');
style.innerHTML = customCss;
document.body.appendChild(style);
// Clean up function
return () => {
document.body.removeChild(style);
};
}, [customCss]);
return (
<>
<Box>
<h2>{ I18n.t('site_settings.appearance.title') }</h2>
<p style={{textAlign: 'left'}}>
<ActionLink
onClick={() => window.open('https://docs.astuto.io/category/appearance-customization/', '_blank')}
icon={<LearnMoreIcon />}
>
{I18n.t('site_settings.appearance.learn_more')}
</ActionLink>
</p>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="formRow">
<div className="formGroup customCssForm col-12">
<h4>{ getLabel('tenant_setting', 'custom_css') }</h4>
<textarea
{...register('customCss')}
maxLength={32000}
id="customCss"
className="formControl"
onKeyDown={e => e.key === 'Tab' && e.preventDefault()}
/>
</div>
</div>
<Button onClick={() => null} disabled={!isDirty}>
{I18n.t('common.buttons.update')}
</Button>
</form>
</Box>
<SiteSettingsInfoBox
areUpdating={areUpdating}
error={error}
areDirty={isDirty && !isSubmitSuccessful}
/>
</>
);
}
export default AppearanceSiteSettingsP;

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import AppearanceSiteSettings from '../../../containers/AppearanceSiteSettings';
import createStoreHelper from '../../../helpers/createStore';
import { State } from '../../../reducers/rootReducer';
import { ISiteSettingsAppearanceForm } from './AppearanceSiteSettingsP';
interface Props {
originForm: ISiteSettingsAppearanceForm;
authenticityToken: string;
}
class AppearanceSiteSettingsRoot extends React.Component<Props> {
store: Store<State, any>;
constructor(props: Props) {
super(props);
this.store = createStoreHelper();
}
render() {
return (
<Provider store={this.store}>
<AppearanceSiteSettings
originForm={this.props.originForm}
authenticityToken={this.props.authenticityToken}
/>
</Provider>
);
}
}
export default AppearanceSiteSettingsRoot;

View File

@@ -6,6 +6,8 @@ import OAuthProvidersList from './OAuthProvidersList';
import { AuthenticationPages } from './AuthenticationSiteSettingsP';
import { OAuthsState } from '../../../reducers/oAuthsReducer';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import ActionLink from '../../common/ActionLink';
import { LearnMoreIcon } from '../../common/Icons';
interface Props {
oAuths: OAuthsState;
@@ -34,6 +36,15 @@ const AuthenticationIndexPage = ({
<Box customClass="authenticationIndexPage">
<h2>{ I18n.t('site_settings.authentication.title') }</h2>
<p style={{textAlign: 'left'}}>
<ActionLink
onClick={() => window.open('https://docs.astuto.io/category/oauth-configuration/', '_blank')}
icon={<LearnMoreIcon />}
>
{I18n.t('site_settings.authentication.learn_more')}
</ActionLink>
</p>
<OAuthProvidersList
oAuths={oAuths.items}
handleToggleEnabledOAuth={handleToggleEnabledOAuth}

View File

@@ -11,7 +11,7 @@ interface Props {
const Button = ({ children, onClick, className = '', outline = false, disabled = false}: Props) => (
<button
onClick={onClick}
className={`${className} btn btn-${outline ? 'outline-' : ''}dark`}
className={`${className} btn${outline ? 'Outline' : ''}Primary`}
disabled={disabled}
>
{children}

View File

@@ -6,6 +6,7 @@ import { ImCancelCircle } from 'react-icons/im';
import { TbLock, TbLockOpen } from 'react-icons/tb';
import { MdContentCopy, MdDone, MdOutlineArrowBack } from 'react-icons/md';
import { GrTest } from 'react-icons/gr';
import { MdOutlineLibraryBooks } from "react-icons/md";
export const EditIcon = () => <FiEdit />;
@@ -26,3 +27,5 @@ export const DoneIcon = () => <MdDone />;
export const BackIcon = () => <MdOutlineArrowBack />;
export const ReplyIcon = () => <BsReply />;
export const LearnMoreIcon = () => <MdOutlineLibraryBooks />;

View File

@@ -0,0 +1,29 @@
import { connect } from "react-redux";
import AppearanceSiteSettingsP from "../components/SiteSettings/Appearance/AppearanceSiteSettingsP";
import { updateTenant } from "../actions/Tenant/updateTenant";
import { State } from "../reducers/rootReducer";
const mapStateToProps = (state: State) => ({
areUpdating: state.siteSettings.appearance.areUpdating,
error: state.siteSettings.appearance.error,
});
const mapDispatchToProps = (dispatch: any) => ({
updateTenant(
customCss: string,
authenticityToken: string,
): Promise<any> {
return dispatch(updateTenant({
tenantSetting: {
custom_css: customCss,
},
authenticityToken,
}));
},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(AppearanceSiteSettingsP);

View File

@@ -26,6 +26,7 @@ interface ITenantSetting {
show_vote_button_in_board?: boolean;
show_roadmap_in_header?: boolean;
collapse_boards_in_header?: TenantSettingCollapseBoardsInHeader;
custom_css?: string;
}
export default ITenantSetting;

View File

@@ -0,0 +1,48 @@
import {
TenantUpdateActionTypes,
TENANT_UPDATE_START,
TENANT_UPDATE_SUCCESS,
TENANT_UPDATE_FAILURE,
} from '../../actions/Tenant/updateTenant';
export interface SiteSettingsAppearanceState {
areUpdating: boolean;
error: string;
}
const initialState: SiteSettingsAppearanceState = {
areUpdating: false,
error: '',
};
const siteSettingsAppearanceReducer = (
state = initialState,
action: TenantUpdateActionTypes,
) => {
switch (action.type) {
case TENANT_UPDATE_START:
return {
...state,
areUpdating: true,
};
case TENANT_UPDATE_SUCCESS:
return {
...state,
areUpdating: false,
error: '',
};
case TENANT_UPDATE_FAILURE:
return {
...state,
areUpdating: false,
error: action.error,
};
default:
return state;
}
}
export default siteSettingsAppearanceReducer;

View File

@@ -95,6 +95,7 @@ import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from '
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
import siteSettingsAuthenticationReducer, { SiteSettingsAuthenticationState } from './SiteSettings/authenticationReducer';
import siteSettingsAppearanceReducer, { SiteSettingsAppearanceState } from './SiteSettings/appearanceReducer';
interface SiteSettingsState {
general: SiteSettingsGeneralState;
@@ -102,6 +103,7 @@ interface SiteSettingsState {
boards: SiteSettingsBoardsState;
postStatuses: SiteSettingsPostStatusesState;
roadmap: SiteSettingsRoadmapState;
appearance: SiteSettingsAppearanceState;
users: SiteSettingsUsersState;
}
@@ -111,6 +113,7 @@ const initialState: SiteSettingsState = {
boards: siteSettingsBoardsReducer(undefined, {} as any),
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
appearance: siteSettingsAppearanceReducer(undefined, {} as any),
users: siteSettingsUsersReducer(undefined, {} as any),
};
@@ -138,6 +141,7 @@ const siteSettingsReducer = (
return {
...state,
general: siteSettingsGeneralReducer(state.general, action),
appearance: siteSettingsAppearanceReducer(state.general, action),
};
case OAUTH_SUBMIT_START:

View File

@@ -3,6 +3,8 @@ class TenantSetting < ApplicationRecord
belongs_to :tenant
validates :custom_css, length: { maximum: 32000 }
enum brand_display: [
:name_and_logo,
:name_only,

View File

@@ -7,7 +7,8 @@ class TenantSettingPolicy < ApplicationPolicy
:show_vote_count,
:show_vote_button_in_board,
:show_roadmap_in_header,
:collapse_boards_in_header
:collapse_boards_in_header,
:custom_css
]
else
[]

View File

@@ -15,7 +15,7 @@
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.resend_confirmation_instructions'), class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.resend_confirmation_instructions'), class: "btn btnPrimary btn-block" %>
</div>
<% end %>

View File

@@ -29,7 +29,7 @@
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.change_password'), class: "btn btn-dark btn-primary" %>
<%= f.submit t('common.forms.auth.change_password'), class: "btnPrimary" %>
</div>
<% end %>

View File

@@ -14,7 +14,7 @@
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.send_reset_password_instructions'), class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.send_reset_password_instructions'), class: "btnPrimary btn-block" %>
</div>
<% end %>

View File

@@ -48,7 +48,7 @@
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.update_profile'), class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.update_profile'), class: "btnPrimary btn-block" %>
</div>
<% end %>

View File

@@ -39,7 +39,7 @@
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.sign_up'), class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.sign_up'), class: "btnPrimary btn-block" %>
</div>
<% end %>

View File

@@ -28,7 +28,7 @@
<% end %>
<div class="actions">
<%= f.submit t('common.forms.auth.log_in'), class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.log_in'), class: "btnPrimary btn-block" %>
</div>
<% end %>

View File

@@ -12,7 +12,7 @@
</div>
<div class="actions">
<%= f.submit t('common.forms.auth.resend_unlock_instructions'), class: "btn btn-dark btn-block" %>
<%= f.submit t('common.forms.auth.resend_unlock_instructions'), class: "btnPrimary btn-block" %>
</div>
<% end %>

View File

@@ -25,5 +25,11 @@
<div class="container">
<%= yield %>
</div>
<% if @tenant and not @tenant.tenant_setting.custom_css.blank? %>
<style type="text/css">
<%= @tenant.tenant_setting.custom_css %>
</style>
<% end %>
</body>
</html>

View File

@@ -9,6 +9,7 @@
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
<%= render 'menu_link', label: t('site_settings.menu.appearance'), path: site_settings_appearance_path %>
<% end %>
<%= render 'menu_link', label: t('site_settings.menu.users'), path: site_settings_users_path %>

View File

@@ -0,0 +1,16 @@
<div class="twoColumnsContainer">
<%= render 'menu' %>
<div>
<%=
react_component(
'SiteSettings/Appearance',
{
originForm: {
customCss: @tenant_setting.custom_css,
},
authenticityToken: form_authenticity_token
}
)
%>
</div>
</div>

View File

@@ -121,6 +121,7 @@ en:
root_board_id: 'Root page'
show_roadmap_in_header: 'Show roadmap link in header'
collapse_boards_in_header: 'Collapse boards in header'
custom_css: 'Custom CSS'
user:
email: 'Email'
full_name: 'Full name'

View File

@@ -144,6 +144,7 @@ en:
roadmap: 'Roadmap'
users: 'Users'
authentication: 'Authentication'
appearance: 'Appearance'
info_box:
up_to_date: 'All changes saved'
error: 'An error occurred: %{message}'
@@ -178,6 +179,9 @@ en:
title2: 'Not in roadmap'
empty: 'The roadmap is empty.'
help: 'You can add statuses to the roadmap by dragging them from the section below. If you instead want to create a new status or change their order, go to Site settings > Statuses.'
appearance:
title: 'Appearance'
learn_more: 'Learn how to customize appearance'
users:
title: 'Users'
block: 'Block'
@@ -195,6 +199,7 @@ en:
status_deleted: 'Deleted'
authentication:
title: 'Authentication'
learn_more: 'Learn how to configure custom OAuth providers'
oauth_subtitle: 'OAuth providers'
default_oauth: 'Default OAuth provider'
copy_url: 'Copy URL'

View File

@@ -50,11 +50,12 @@ Rails.application.routes.draw do
namespace :site_settings do
get 'general'
get 'authentication'
get 'boards'
get 'post_statuses'
get 'roadmap'
get 'appearance'
get 'users'
get 'authentication'
end
end

View File

@@ -0,0 +1,5 @@
class AddCustomCssToTenantSettings < ActiveRecord::Migration[6.1]
def change
add_column :tenant_settings, :custom_css, :text, null: true
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2024_01_17_112502) do
ActiveRecord::Schema.define(version: 2024_01_23_125448) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -134,6 +134,7 @@ ActiveRecord::Schema.define(version: 2024_01_17_112502) do
t.integer "root_board_id", default: 0, null: false
t.boolean "show_roadmap_in_header", default: true, null: false
t.integer "collapse_boards_in_header", default: 0, null: false
t.text "custom_css"
t.index ["tenant_id"], name: "index_tenant_settings_on_tenant_id"
end