Add setting to manage visibility of vote count, vote button and decide root page (#197)

This commit is contained in:
Riccardo Graziosi
2023-02-05 11:55:38 +01:00
committed by GitHub
parent d4242dd78e
commit e7335f5622
35 changed files with 246 additions and 48 deletions

View File

@@ -1,14 +1,19 @@
class StaticPagesController < ApplicationController class StaticPagesController < ApplicationController
skip_before_action :load_tenant_data, only: [:showcase, :pending_tenant, :blocked_tenant] skip_before_action :load_tenant_data, only: [:showcase, :pending_tenant, :blocked_tenant]
def roadmap def root
@post_statuses = PostStatus @board = Board.find_by(id: Current.tenant.tenant_setting.root_board_id)
.find_roadmap
.select(:id, :name, :color)
@posts = Post if @board
.find_with_post_status_in(@post_statuses) render 'boards/show'
.select(:id, :title, :board_id, :post_status_id, :user_id, :created_at) else
get_roadmap_data
render 'static_pages/roadmap'
end
end
def roadmap
get_roadmap_data
end end
def showcase def showcase
@@ -20,4 +25,16 @@ class StaticPagesController < ApplicationController
def blocked_tenant def blocked_tenant
end end
private
def get_roadmap_data
@post_statuses = PostStatus
.find_roadmap
.select(:id, :name, :color)
@posts = Post
.find_with_post_status_in(@post_statuses)
.select(:id, :title, :board_id, :post_status_id, :user_id, :created_at)
end
end end

View File

@@ -76,8 +76,6 @@ export const updateTenant = ({
}, },
}); });
console.log(body)
const res = await fetch(`/tenants/0`, { const res = await fetch(`/tenants/0`, {
method: 'PATCH', method: 'PATCH',
headers: buildRequestHeaders(authenticityToken), headers: buildRequestHeaders(authenticityToken),

View File

@@ -7,6 +7,7 @@ import PostList from './PostList';
import Sidebar from '../common/Sidebar'; import Sidebar from '../common/Sidebar';
import IBoard from '../../interfaces/IBoard'; import IBoard from '../../interfaces/IBoard';
import ITenantSetting from '../../interfaces/ITenantSetting';
import { PostsState } from '../../reducers/postsReducer'; import { PostsState } from '../../reducers/postsReducer';
import { PostStatusesState } from '../../reducers/postStatusesReducer'; import { PostStatusesState } from '../../reducers/postStatusesReducer';
@@ -14,6 +15,8 @@ import { PostStatusesState } from '../../reducers/postStatusesReducer';
interface Props { interface Props {
board: IBoard; board: IBoard;
isLoggedIn: boolean; isLoggedIn: boolean;
isPowerUser: boolean;
tenantSetting: ITenantSetting;
authenticityToken: string; authenticityToken: string;
posts: PostsState; posts: PostsState;
postStatuses: PostStatusesState; postStatuses: PostStatusesState;
@@ -63,6 +66,8 @@ class BoardP extends React.Component<Props> {
const { const {
board, board,
isLoggedIn, isLoggedIn,
isPowerUser,
tenantSetting,
authenticityToken, authenticityToken,
posts, posts,
postStatuses, postStatuses,
@@ -97,6 +102,8 @@ class BoardP extends React.Component<Props> {
<PostList <PostList
posts={posts.items} posts={posts.items}
showLikeCount={isPowerUser || tenantSetting.show_vote_count}
showLikeButtons={tenantSetting.show_vote_button_in_board}
postStatuses={postStatuses.items} postStatuses={postStatuses.items}
areLoading={posts.areLoading} areLoading={posts.areLoading}
error={posts.error} error={posts.error}

View File

@@ -14,6 +14,8 @@ import IPostStatus from '../../interfaces/IPostStatus';
interface Props { interface Props {
posts: Array<IPost>; posts: Array<IPost>;
showLikeCount: boolean;
showLikeButtons: boolean;
postStatuses: Array<IPostStatus>; postStatuses: Array<IPostStatus>;
areLoading: boolean; areLoading: boolean;
error: string; error: string;
@@ -27,6 +29,8 @@ interface Props {
const PostList = ({ const PostList = ({
posts, posts,
showLikeCount,
showLikeButtons,
postStatuses, postStatuses,
areLoading, areLoading,
error, error,
@@ -53,7 +57,9 @@ const PostList = ({
title={post.title} title={post.title}
description={post.description} description={post.description}
postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)} postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)}
likesCount={post.likesCount} likeCount={post.likeCount}
showLikeCount={showLikeCount}
showLikeButtons={showLikeButtons}
liked={post.liked} liked={post.liked}
commentsCount={post.commentsCount} commentsCount={post.commentsCount}

View File

@@ -12,7 +12,9 @@ interface Props {
title: string; title: string;
description?: string; description?: string;
postStatus: IPostStatus; postStatus: IPostStatus;
likesCount: number; likeCount: number;
showLikeCount: boolean;
showLikeButtons: boolean;
liked: number; liked: number;
commentsCount: number; commentsCount: number;
@@ -25,7 +27,9 @@ const PostListItem = ({
title, title,
description, description,
postStatus, postStatus,
likesCount, likeCount,
showLikeCount,
showLikeButtons,
liked, liked,
commentsCount, commentsCount,
@@ -35,7 +39,9 @@ const PostListItem = ({
<div onClick={() => window.location.href = `/posts/${id}`} className="postListItem"> <div onClick={() => window.location.href = `/posts/${id}`} className="postListItem">
<LikeButton <LikeButton
postId={id} postId={id}
likesCount={likesCount} likeCount={likeCount}
showLikeCount={showLikeCount}
showLikeButton={showLikeButtons}
liked={liked} liked={liked}
isLoggedIn={isLoggedIn} isLoggedIn={isLoggedIn}
authenticityToken={authenticityToken} authenticityToken={authenticityToken}

View File

@@ -8,10 +8,13 @@ import IBoard from '../../interfaces/IBoard';
import { Store } from 'redux'; import { Store } from 'redux';
import { State } from '../../reducers/rootReducer'; import { State } from '../../reducers/rootReducer';
import ITenantSetting from '../../interfaces/ITenantSetting';
interface Props { interface Props {
board: IBoard; board: IBoard;
isLoggedIn: boolean; isLoggedIn: boolean;
isPowerUser: boolean;
tenantSetting: ITenantSetting;
authenticityToken: string; authenticityToken: string;
} }
@@ -25,13 +28,21 @@ class BoardRoot extends React.Component<Props> {
} }
render() { render() {
const { board, isLoggedIn, authenticityToken } = this.props; const {
board,
isLoggedIn,
isPowerUser,
tenantSetting,
authenticityToken,
} = this.props;
return ( return (
<Provider store={this.store}> <Provider store={this.store}>
<Board <Board
board={board} board={board}
isLoggedIn={isLoggedIn} isLoggedIn={isLoggedIn}
isPowerUser={isPowerUser}
tenantSetting={tenantSetting}
authenticityToken={authenticityToken} authenticityToken={authenticityToken}
/> />
</Provider> </Provider>

View File

@@ -2,7 +2,9 @@ import * as React from 'react';
interface Props { interface Props {
postId: number; postId: number;
likesCount: number; likeCount: number;
showLikeCount?: boolean;
showLikeButton?: boolean;
liked: number; liked: number;
handleLikeSubmit( handleLikeSubmit(
postId: number, postId: number,
@@ -15,7 +17,9 @@ interface Props {
const LikeButtonP = ({ const LikeButtonP = ({
postId, postId,
likesCount, likeCount,
showLikeCount = true,
showLikeButton = true,
liked, liked,
handleLikeSubmit, handleLikeSubmit,
authenticityToken, authenticityToken,
@@ -29,9 +33,10 @@ const LikeButtonP = ({
else window.location.href = `/users/sign_in`; else window.location.href = `/users/sign_in`;
}} }}
className={`likeButton${liked ? ' liked' : ''}`} className={`likeButton${liked ? ' liked' : ''}`}
hidden={!showLikeButton}
> >
</div> </div>
<span className="likesCountLabel">{likesCount}</span> { showLikeCount && <span className="likeCountLabel">{likeCount}</span> }
</div> </div>
); );

View File

@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { getLabel } from '../../helpers/formUtils';
import IBoard from '../../interfaces/IBoard'; import IBoard from '../../interfaces/IBoard';
@@ -25,7 +26,7 @@ const PostBoardSelect = ({
id="selectPickerBoard" id="selectPickerBoard"
className="selectPicker" className="selectPicker"
> >
<optgroup label="Boards"> <optgroup label={getLabel('board')}>
{boards.map((board, i) => ( {boards.map((board, i) => (
<option value={board.id} key={i}> <option value={board.id} key={i}>
{board.name} {board.name}

View File

@@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown';
import IPost from '../../interfaces/IPost'; import IPost from '../../interfaces/IPost';
import IPostStatus from '../../interfaces/IPostStatus'; import IPostStatus from '../../interfaces/IPostStatus';
import IBoard from '../../interfaces/IBoard'; import IBoard from '../../interfaces/IBoard';
import ITenantSetting from '../../interfaces/ITenantSetting';
import PostUpdateList from './PostUpdateList'; import PostUpdateList from './PostUpdateList';
import PostEditForm from './PostEditForm'; import PostEditForm from './PostEditForm';
@@ -39,6 +40,7 @@ interface Props {
isPowerUser: boolean; isPowerUser: boolean;
currentUserFullName: string; currentUserFullName: string;
currentUserEmail: string; currentUserEmail: string;
tenantSetting: ITenantSetting;
authenticityToken: string; authenticityToken: string;
requestPost(postId: number): void; requestPost(postId: number): void;
@@ -148,6 +150,7 @@ class PostP extends React.Component<Props> {
isLoggedIn, isLoggedIn,
isPowerUser, isPowerUser,
currentUserEmail, currentUserEmail,
tenantSetting,
authenticityToken, authenticityToken,
submitFollow, submitFollow,
@@ -176,11 +179,14 @@ class PostP extends React.Component<Props> {
error={comments.error} error={comments.error}
/> />
<LikeList {
likes={likes.items} isPowerUser &&
areLoading={likes.areLoading} <LikeList
error={likes.error} likes={likes.items}
/> areLoading={likes.areLoading}
error={likes.error}
/>
}
<ActionBox <ActionBox
followed={followed} followed={followed}
@@ -213,7 +219,8 @@ class PostP extends React.Component<Props> {
<div className="postHeader"> <div className="postHeader">
<LikeButton <LikeButton
postId={post.id} postId={post.id}
likesCount={likes.items.length} likeCount={likes.items.length}
showLikeCount={isPowerUser || tenantSetting.show_vote_count}
liked={likes.items.find(like => like.email === currentUserEmail) ? 1 : 0} liked={likes.items.find(like => like.email === currentUserEmail) ? 1 : 0}
isLoggedIn={isLoggedIn} isLoggedIn={isLoggedIn}
authenticityToken={authenticityToken} authenticityToken={authenticityToken}

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import I18n from 'i18n-js'; import I18n from 'i18n-js';
import IPostStatus from '../../interfaces/IPostStatus'; import IPostStatus from '../../interfaces/IPostStatus';
import { getLabel } from '../../helpers/formUtils';
const NO_POST_STATUS_VALUE = 'none'; const NO_POST_STATUS_VALUE = 'none';
@@ -28,16 +29,16 @@ const PostStatusSelect = ({
id="selectPickerStatus" id="selectPickerStatus"
className="selectPicker" className="selectPicker"
> >
<optgroup label="Post statuses"> <optgroup label={getLabel('post_status')}>
{postStatuses.map((postStatus, i) => ( {postStatuses.map((postStatus, i) => (
<option value={postStatus.id} key={i}> <option value={postStatus.id} key={i}>
{postStatus.name} {postStatus.name}
</option> </option>
))} ))}
</optgroup> </optgroup>
<optgroup label="No post status"> <option value={NO_POST_STATUS_VALUE}>
<option value={NO_POST_STATUS_VALUE}>{I18n.t('post.post_status_select.no_post_status')}</option> {I18n.t('post.post_status_select.no_post_status')}
</optgroup> </option>
</select> </select>
); );

View File

@@ -10,6 +10,7 @@ import IPostStatus from '../../interfaces/IPostStatus';
import { Store } from 'redux'; import { Store } from 'redux';
import { State } from '../../reducers/rootReducer'; import { State } from '../../reducers/rootReducer';
import ITenantSetting from '../../interfaces/ITenantSetting';
interface Props { interface Props {
postId: number; postId: number;
@@ -19,6 +20,7 @@ interface Props {
isPowerUser: boolean; isPowerUser: boolean;
currentUserFullName: string; currentUserFullName: string;
currentUserEmail: string; currentUserEmail: string;
tenantSetting: ITenantSetting;
authenticityToken: string; authenticityToken: string;
} }
@@ -40,6 +42,7 @@ class PostRoot extends React.Component<Props> {
isPowerUser, isPowerUser,
currentUserFullName, currentUserFullName,
currentUserEmail, currentUserEmail,
tenantSetting,
authenticityToken authenticityToken
} = this.props; } = this.props;
@@ -54,6 +57,7 @@ class PostRoot extends React.Component<Props> {
isPowerUser={isPowerUser} isPowerUser={isPowerUser}
currentUserFullName={currentUserFullName} currentUserFullName={currentUserFullName}
currentUserEmail={currentUserEmail} currentUserEmail={currentUserEmail}
tenantSetting={tenantSetting}
authenticityToken={authenticityToken} authenticityToken={authenticityToken}
/> />
</Provider> </Provider>

View File

@@ -12,18 +12,23 @@ import {
TENANT_SETTING_BRAND_DISPLAY_LOGO_ONLY, TENANT_SETTING_BRAND_DISPLAY_LOGO_ONLY,
TENANT_SETTING_BRAND_DISPLAY_NONE, TENANT_SETTING_BRAND_DISPLAY_NONE,
} from '../../../interfaces/ITenantSetting'; } from '../../../interfaces/ITenantSetting';
import { DangerText } from '../../common/CustomTexts'; import { DangerText, SmallMutedText } from '../../common/CustomTexts';
import { getLabel, getValidationMessage } from '../../../helpers/formUtils'; import { getLabel, getValidationMessage } from '../../../helpers/formUtils';
import IBoardJSON from '../../../interfaces/json/IBoard';
export interface ISiteSettingsGeneralForm { export interface ISiteSettingsGeneralForm {
siteName: string; siteName: string;
siteLogo: string; siteLogo: string;
brandDisplaySetting: string; brandDisplaySetting: string;
locale: string; locale: string;
showVoteCount: boolean;
showVoteButtonInBoard: boolean;
rootBoardId?: string;
} }
interface Props { interface Props {
originForm: ISiteSettingsGeneralForm; originForm: ISiteSettingsGeneralForm;
boards: IBoardJSON[];
authenticityToken: string; authenticityToken: string;
areUpdating: boolean; areUpdating: boolean;
@@ -34,12 +39,16 @@ interface Props {
siteLogo: string, siteLogo: string,
brandDisplaySetting: string, brandDisplaySetting: string,
locale: string, locale: string,
rootBoardId: number,
showVoteCount: boolean,
showVoteButtonInBoard: boolean,
authenticityToken: string authenticityToken: string
): Promise<any>; ): Promise<any>;
} }
const GeneralSiteSettingsP = ({ const GeneralSiteSettingsP = ({
originForm, originForm,
boards,
authenticityToken, authenticityToken,
areUpdating, areUpdating,
@@ -56,6 +65,9 @@ const GeneralSiteSettingsP = ({
siteLogo: originForm.siteLogo, siteLogo: originForm.siteLogo,
brandDisplaySetting: originForm.brandDisplaySetting, brandDisplaySetting: originForm.brandDisplaySetting,
locale: originForm.locale, locale: originForm.locale,
showVoteCount: originForm.showVoteCount,
showVoteButtonInBoard: originForm.showVoteButtonInBoard,
rootBoardId: originForm.rootBoardId,
}, },
}); });
@@ -65,6 +77,9 @@ const GeneralSiteSettingsP = ({
data.siteLogo, data.siteLogo,
data.brandDisplaySetting, data.brandDisplaySetting,
data.locale, data.locale,
Number(data.rootBoardId),
data.showVoteCount,
data.showVoteButtonInBoard,
authenticityToken, authenticityToken,
).then(res => { ).then(res => {
if (res?.status !== HttpStatus.OK) return; if (res?.status !== HttpStatus.OK) return;
@@ -99,7 +114,7 @@ const GeneralSiteSettingsP = ({
</div> </div>
<div className="formGroup col-4"> <div className="formGroup col-4">
<label htmlFor="brandSetting">{ getLabel('tenant', 'brand_setting') }</label> <label htmlFor="brandSetting">{ getLabel('tenant_setting', 'brand_display') }</label>
<select <select
{...register('brandDisplaySetting')} {...register('brandDisplaySetting')}
id="brandSetting" id="brandSetting"
@@ -136,6 +151,48 @@ const GeneralSiteSettingsP = ({
</select> </select>
</div> </div>
<div className="formGroup">
<label htmlFor="rootBoardId">{ getLabel('tenant_setting', 'root_board_id') }</label>
<select
{...register('rootBoardId')}
id="rootBoardId"
className="selectPicker"
>
<option value="0">
{I18n.t('roadmap.title')}
</option>
<optgroup label={getLabel('board')}>
{boards.map((board, i) => (
<option value={board.id} key={i}>{board.name}</option>
))}
</optgroup>
</select>
</div>
<br />
<div className="formGroup">
<div className="checkboxSwitch">
<input {...register('showVoteCount')} type="checkbox" id="show_vote_count_checkbox" />
<label htmlFor="show_vote_count_checkbox">{ getLabel('tenant_setting', 'show_vote_count') }</label>
<SmallMutedText>
{ I18n.t('site_settings.general.show_vote_count_help') }
</SmallMutedText>
</div>
</div>
<br />
<div className="formGroup">
<div className="checkboxSwitch">
<input {...register('showVoteButtonInBoard')} type="checkbox" id="show_vote_button_in_board_checkbox" />
<label htmlFor="show_vote_button_in_board_checkbox">{ getLabel('tenant_setting', 'show_vote_button_in_board') }</label>
<SmallMutedText>
{ I18n.t('site_settings.general.show_vote_button_in_board_help') }
</SmallMutedText>
</div>
</div>
<br /> <br />
<Button onClick={() => null} disabled={!isDirty}> <Button onClick={() => null} disabled={!isDirty}>

View File

@@ -4,11 +4,13 @@ import { Store } from 'redux';
import GeneralSiteSettings from '../../../containers/GeneralSiteSettings'; import GeneralSiteSettings from '../../../containers/GeneralSiteSettings';
import createStoreHelper from '../../../helpers/createStore'; import createStoreHelper from '../../../helpers/createStore';
import IBoardJSON from '../../../interfaces/json/IBoard';
import { State } from '../../../reducers/rootReducer'; import { State } from '../../../reducers/rootReducer';
import { ISiteSettingsGeneralForm } from './GeneralSiteSettingsP'; import { ISiteSettingsGeneralForm } from './GeneralSiteSettingsP';
interface Props { interface Props {
originForm: ISiteSettingsGeneralForm; originForm: ISiteSettingsGeneralForm;
boards: IBoardJSON[];
authenticityToken: string; authenticityToken: string;
} }
@@ -26,6 +28,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}
boards={this.props.boards}
authenticityToken={this.props.authenticityToken} authenticityToken={this.props.authenticityToken}
/> />
</Provider> </Provider>

View File

@@ -4,6 +4,7 @@ import I18n from 'i18n-js';
import Button from '../../common/Button'; import Button from '../../common/Button';
import IUser, { UserRoles, USER_ROLE_ADMIN, USER_ROLE_MODERATOR, USER_ROLE_USER } from '../../../interfaces/IUser'; import IUser, { UserRoles, USER_ROLE_ADMIN, USER_ROLE_MODERATOR, USER_ROLE_USER } from '../../../interfaces/IUser';
import { getLabel } from '../../../helpers/formUtils';
interface Props { interface Props {
user: IUser; user: IUser;
@@ -57,7 +58,7 @@ class UserForm extends React.Component<Props, State> {
id="selectPickerUserRole" id="selectPickerUserRole"
className="selectPicker" className="selectPicker"
> >
<optgroup label="Roles"> <optgroup label={getLabel('user', 'role')}>
<option value={USER_ROLE_USER}> <option value={USER_ROLE_USER}>
{ I18n.t(`site_settings.users.role_${USER_ROLE_USER}`) } { I18n.t(`site_settings.users.role_${USER_ROLE_USER}`) }
</option> </option>

View File

@@ -16,12 +16,20 @@ const mapDispatchToProps = (dispatch: any) => ({
siteLogo: string, siteLogo: string,
brandDisplaySetting: TenantSettingBrandDisplay, brandDisplaySetting: TenantSettingBrandDisplay,
locale: string, locale: string,
rootBoardId: number,
showVoteCount: boolean,
showVoteButtonInBoard: boolean,
authenticityToken: string authenticityToken: string
): Promise<any> { ): Promise<any> {
return dispatch(updateTenant({ return dispatch(updateTenant({
siteName, siteName,
siteLogo, siteLogo,
tenantSetting: { brand_display: brandDisplaySetting }, tenantSetting: {
brand_display: brandDisplaySetting,
show_vote_count: showVoteCount,
show_vote_button_in_board: showVoteButtonInBoard,
root_board_id: rootBoardId,
},
locale, locale,
authenticityToken, authenticityToken,
})); }));

View File

@@ -3,9 +3,12 @@ import I18n from 'i18n-js';
export const getLabel = ( export const getLabel = (
entity: string, entity: string,
attribute: string, attribute: string = undefined,
) => ( ) => (
I18n.t(`activerecord.attributes.${entity}.${attribute}`) attribute ?
I18n.t(`activerecord.attributes.${entity}.${attribute}`)
:
I18n.t(`activerecord.models.${entity}.one`)
); );
export const getValidationMessage = ( export const getValidationMessage = (

View File

@@ -4,7 +4,7 @@ interface IPost {
description?: string; description?: string;
boardId: number; boardId: number;
postStatusId?: number; postStatusId?: number;
likesCount: number; likeCount: number;
liked: number; liked: number;
commentsCount: number; commentsCount: number;
hotness: number; hotness: number;

View File

@@ -12,6 +12,9 @@ export type TenantSettingBrandDisplay =
interface ITenantSetting { interface ITenantSetting {
brand_display?: TenantSettingBrandDisplay; brand_display?: TenantSettingBrandDisplay;
root_board_id?: number;
show_vote_count?: boolean;
show_vote_button_in_board?: boolean;
} }
export default ITenantSetting; export default ITenantSetting;

View File

@@ -16,7 +16,7 @@ const initialState: IPost = {
description: null, description: null,
boardId: 0, boardId: 0,
postStatusId: null, postStatusId: null,
likesCount: 0, likeCount: 0,
liked: 0, liked: 0,
commentsCount: 0, commentsCount: 0,
hotness: 0, hotness: 0,
@@ -40,7 +40,7 @@ const postReducer = (
description: action.post.description, description: action.post.description,
boardId: action.post.board_id, boardId: action.post.board_id,
postStatusId: action.post.post_status_id, postStatusId: action.post.post_status_id,
likesCount: action.post.likes_count, likeCount: action.post.likes_count,
liked: action.post.liked, liked: action.post.liked,
commentsCount: action.post.comments_count, commentsCount: action.post.comments_count,
hotness: action.post.hotness, hotness: action.post.hotness,

View File

@@ -104,9 +104,9 @@ const postsReducer = (
items: state.items.map(post => { items: state.items.map(post => {
if (action.postId === post.id) { if (action.postId === post.id) {
return action.isLike ? return action.isLike ?
{ ...post, likesCount: post.likesCount + 1, liked: 1 } { ...post, likeCount: post.likeCount + 1, liked: 1 }
: :
{ ...post, likesCount: post.likesCount - 1, liked: 0 } { ...post, likeCount: post.likeCount - 1, liked: 0 }
} else return post; } else return post;
}), }),
}; };

View File

@@ -23,7 +23,7 @@
border-bottom-color: $primary-color; border-bottom-color: $primary-color;
} }
.likesCountLabel { .likeCountLabel {
text-align: center; text-align: center;
font-size: 17px; font-size: 17px;
} }

View File

@@ -1,7 +1,7 @@
class TenantSettingPolicy < ApplicationPolicy class TenantSettingPolicy < ApplicationPolicy
def permitted_attributes_for_update def permitted_attributes_for_update
if user.admin? if user.admin?
[:brand_display] [:brand_display, :root_board_id, :show_vote_count, :show_vote_button_in_board]
else else
[] []
end end

View File

@@ -4,7 +4,9 @@
{ {
board: @board, board: @board,
isLoggedIn: user_signed_in?, isLoggedIn: user_signed_in?,
authenticityToken: form_authenticity_token, isPowerUser: user_signed_in? ? current_user.moderator? : false,
tenantSetting: @tenant_setting,
authenticityToken: form_authenticity_token
} }
) )
%> %>

View File

@@ -9,6 +9,7 @@
isPowerUser: user_signed_in? ? current_user.moderator? : false, isPowerUser: user_signed_in? ? current_user.moderator? : false,
currentUserFullName: user_signed_in? ? current_user.full_name : nil, currentUserFullName: user_signed_in? ? current_user.full_name : nil,
currentUserEmail: user_signed_in? ? current_user.email : nil, currentUserEmail: user_signed_in? ? current_user.email : nil,
tenantSetting: @tenant_setting,
authenticityToken: form_authenticity_token, authenticityToken: form_authenticity_token,
} }
) )

View File

@@ -9,8 +9,12 @@
siteName: @tenant.site_name, siteName: @tenant.site_name,
siteLogo: @tenant.site_logo, siteLogo: @tenant.site_logo,
brandDisplaySetting: @tenant_setting.brand_display, brandDisplaySetting: @tenant_setting.brand_display,
showVoteCount: @tenant_setting.show_vote_count,
showVoteButtonInBoard: @tenant_setting.show_vote_button_in_board,
rootBoardId: @tenant_setting.root_board_id.to_s,
locale: @tenant.locale locale: @tenant.locale
}, },
boards: @tenant.boards.order(order: :asc),
authenticityToken: form_authenticity_token authenticityToken: form_authenticity_token
} }
) )

View File

@@ -47,6 +47,28 @@ en:
subject: '[%{app_name}] Status change on post %{post}' subject: '[%{app_name}] Status change on post %{post}'
body: "The post you're following %{post} has a new status" body: "The post you're following %{post} has a new status"
activerecord: activerecord:
models:
board:
one: 'Board'
other: 'Boards'
comment:
one: 'Comment'
other: 'Comments'
like:
one: 'Vote'
other: 'Votes'
o_auth:
one: 'OAuth'
other: 'OAuths'
post_status:
one: 'Status'
other: 'Statuses'
post:
one: 'Post'
other: 'Posts'
user:
one: 'User'
other: 'Users'
attributes: attributes:
board: board:
name: 'Name' name: 'Name'
@@ -92,7 +114,11 @@ en:
site_logo: 'Site logo' site_logo: 'Site logo'
subdomain: 'Subdomain' subdomain: 'Subdomain'
locale: 'Language' locale: 'Language'
brand_setting: 'Display' tenant_setting:
brand_display: 'Display'
show_vote_count: 'Show vote count to users'
show_vote_button_in_board: 'Show vote buttons in board page'
root_board_id: 'Root page'
user: user:
email: 'Email' email: 'Email'
full_name: 'Full name' full_name: 'Full name'

View File

@@ -153,6 +153,8 @@ en:
brand_setting_name: 'Name only' brand_setting_name: 'Name only'
brand_setting_logo: 'Logo only' brand_setting_logo: 'Logo only'
brand_setting_none: 'None' brand_setting_none: 'None'
show_vote_count_help: 'If you enable this setting, users will be able to see the vote count of posts. This may incentivize users to vote on already popular posts, leading to a snowball effect.'
show_vote_button_in_board_help: 'If you enable this setting, users will be able to vote posts from the board page. This may incentivize users to vote on more posts, leading to a higher number of votes but of lower significance.'
boards: boards:
title: 'Boards' title: 'Boards'
empty: 'There are no boards. Create one below!' empty: 'There are no boards. Create one below!'

View File

@@ -13,7 +13,7 @@ Rails.application.routes.draw do
end end
constraints subdomain: /.*/ do constraints subdomain: /.*/ do
root to: 'static_pages#roadmap' root to: 'static_pages#root'
get '/roadmap', to: 'static_pages#roadmap' get '/roadmap', to: 'static_pages#roadmap'
get '/pending-tenant', to: 'static_pages#pending_tenant' get '/pending-tenant', to: 'static_pages#pending_tenant'

View File

@@ -0,0 +1,6 @@
class AddShowVoteAndButtonVoteToTenantSetting < ActiveRecord::Migration[6.0]
def change
add_column :tenant_settings, :show_vote_count, :boolean, null: false, default: false
add_column :tenant_settings, :show_vote_button_in_board, :boolean, null: false, default: false
end
end

View File

@@ -0,0 +1,5 @@
class AddRootBoardIdToTenantSetting < ActiveRecord::Migration[6.0]
def change
add_column :tenant_settings, :root_board_id, :integer, null: false, default: 0
end
end

View File

@@ -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: 2023_01_31_194858) do ActiveRecord::Schema.define(version: 2023_02_04_171748) 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"
@@ -129,6 +129,9 @@ ActiveRecord::Schema.define(version: 2023_01_31_194858) do
t.bigint "tenant_id", null: false t.bigint "tenant_id", null: false
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.boolean "show_vote_count", default: false, null: false
t.boolean "show_vote_button_in_board", default: false, null: false
t.integer "root_board_id", default: 0, null: false
t.index ["tenant_id"], name: "index_tenant_settings_on_tenant_id" t.index ["tenant_id"], name: "index_tenant_settings_on_tenant_id"
end end

View File

@@ -1,6 +1,5 @@
FactoryBot.define do FactoryBot.define do
factory :tenant_setting do factory :tenant_setting do
brand_display { 0 }
tenant tenant
end end
end end

View File

@@ -19,4 +19,16 @@ RSpec.describe TenantSetting, type: :model do
tenant_setting.brand_display = 'no_name_no_logo' tenant_setting.brand_display = 'no_name_no_logo'
expect(tenant_setting).to be_valid expect(tenant_setting).to be_valid
end end
it 'has a setting to show vote count' do
expect(tenant_setting.show_vote_count).to be_falsy
end
it 'has a setting to show vote button in board view' do
expect(tenant_setting.show_vote_button_in_board).to be_falsy
end
it 'has a setting that contains the board id of the root page' do
expect(tenant_setting.root_board_id).to eq(0)
end
end end

View File

@@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe 'static pages routing', :aggregate_failures, type: :routing do RSpec.describe 'static pages routing', :aggregate_failures, type: :routing do
it 'routes roadmap' do it 'routes roadmap' do
expect(get: '/').to route_to( expect(get: '/').to route_to(
controller: 'static_pages', action: 'roadmap' controller: 'static_pages', action: 'root'
) )
expect(get: '/roadmap').to route_to( expect(get: '/roadmap').to route_to(

View File

@@ -10,7 +10,7 @@ feature 'likes', type: :system, js: true do
let(:post_header_selector) { '.postHeader' } let(:post_header_selector) { '.postHeader' }
let(:like_button_container_selector) { '.likeButtonContainer' } let(:like_button_container_selector) { '.likeButtonContainer' }
let(:like_button_selector) { '.likeButton' } let(:like_button_selector) { '.likeButton' }
let(:likes_count_label_selector) { '.likesCountLabel' } let(:likes_count_label_selector) { '.likeCountLabel' }
let(:like_list_container_selector) { '.likeListContainer' } let(:like_list_container_selector) { '.likeListContainer' }
before(:each) do before(:each) do