Add setting to show in filter by status only statuses present in board posts (#411)

This commit is contained in:
Riccardo Graziosi
2024-09-17 18:36:54 +02:00
committed by GitHub
parent 675a8709ef
commit cab2229e09
17 changed files with 83 additions and 18 deletions

View File

@@ -4,4 +4,8 @@
scroll-margin-top: 96px; scroll-margin-top: 96px;
} }
.generalSiteSettingsSubmit {
@extend .mb-4;
}
} }

View File

@@ -1,5 +1,6 @@
class BoardsController < ApplicationController class BoardsController < ApplicationController
include ApplicationHelper include ApplicationHelper
include BoardsHelper
before_action :authenticate_user!, only: [:create, :update, :update_order, :destroy] before_action :authenticate_user!, only: [:create, :update, :update_order, :destroy]
@@ -12,6 +13,7 @@ class BoardsController < ApplicationController
def show def show
@board = Board.friendly.find(params[:id]) @board = Board.friendly.find(params[:id])
@page_title = @board.name @page_title = @board.name
@post_statuses_to_show_in_filter = get_post_statuses_to_show_in_filter
end end
def create def create

View File

@@ -1,4 +1,6 @@
class StaticPagesController < ApplicationController class StaticPagesController < ApplicationController
include BoardsHelper
skip_before_action :load_tenant_data, only: [:showcase, :pending_tenant, :blocked_tenant] skip_before_action :load_tenant_data, only: [:showcase, :pending_tenant, :blocked_tenant]
before_action :allow_iframe_embedding, only: [:embedded_roadmap] before_action :allow_iframe_embedding, only: [:embedded_roadmap]
@@ -7,6 +9,7 @@ class StaticPagesController < ApplicationController
if @board if @board
@page_title = @board.name @page_title = @board.name
@post_statuses_to_show_in_filter = get_post_statuses_to_show_in_filter
render 'boards/show' render 'boards/show'
else else
@page_title = t('roadmap.title') @page_title = t('roadmap.title')

View File

@@ -0,0 +1,9 @@
module BoardsHelper
def get_post_statuses_to_show_in_filter
if Current.tenant.tenant_setting.hide_unused_statuses_in_filter_by_status
@board.posts.map(&:post_status_id).uniq
else
PostStatus.pluck(:id) << nil
end
end
end

View File

@@ -23,6 +23,7 @@ interface Props {
currentUserFullName: string; currentUserFullName: string;
tenantSetting: ITenantSetting; tenantSetting: ITenantSetting;
componentRenderedAt: number; componentRenderedAt: number;
postStatusesToShowInFilter: Array<number>;
authenticityToken: string; authenticityToken: string;
posts: PostsState; posts: PostsState;
postStatuses: PostStatusesState; postStatuses: PostStatusesState;
@@ -96,6 +97,7 @@ class BoardP extends React.Component<Props> {
currentUserFullName, currentUserFullName,
tenantSetting, tenantSetting,
componentRenderedAt, componentRenderedAt,
postStatusesToShowInFilter,
authenticityToken, authenticityToken,
posts, posts,
postStatuses, postStatuses,
@@ -140,14 +142,19 @@ class BoardP extends React.Component<Props> {
/> />
</> </>
} }
{
postStatusesToShowInFilter.length > 0 &&
<PostStatusFilter <PostStatusFilter
postStatuses={postStatuses.items} postStatuses={postStatuses.items.filter(postStatus => postStatusesToShowInFilter.includes(postStatus.id))}
areLoading={postStatuses.areLoading} areLoading={postStatuses.areLoading}
error={postStatuses.error} error={postStatuses.error}
currentFilter={filters.postStatusIds} currentFilter={filters.postStatusIds}
handleFilterClick={handlePostStatusFilterChange} handleFilterClick={handlePostStatusFilterChange}
showNoStatusFilter={postStatusesToShowInFilter.includes(null)}
/> />
}
</div> </div>
{ tenantSetting.show_powered_by && <PoweredByLink /> } { tenantSetting.show_powered_by && <PoweredByLink /> }

View File

@@ -15,6 +15,8 @@ interface Props {
handleFilterClick(postStatusId: number): void; handleFilterClick(postStatusId: number): void;
currentFilter: Array<number>; currentFilter: Array<number>;
showNoStatusFilter?: boolean;
} }
const PostStatusFilter = ({ const PostStatusFilter = ({
@@ -24,6 +26,8 @@ const PostStatusFilter = ({
handleFilterClick, handleFilterClick,
currentFilter, currentFilter,
showNoStatusFilter = true,
}: Props) => ( }: Props) => (
<SidebarBox title={I18n.t('board.filter_box.title')} customClass="postStatusFilterContainer"> <SidebarBox title={I18n.t('board.filter_box.title')} customClass="postStatusFilterContainer">
{ {
@@ -39,6 +43,9 @@ const PostStatusFilter = ({
/> />
)) ))
} }
{
showNoStatusFilter &&
<PostStatusListItem <PostStatusListItem
name={I18n.t('common.no_status')} name={I18n.t('common.no_status')}
color='black' color='black'
@@ -46,6 +53,8 @@ const PostStatusFilter = ({
handleClick={() => handleFilterClick(0)} handleClick={() => handleFilterClick(0)}
isCurrentFilter={currentFilter.includes(0)} isCurrentFilter={currentFilter.includes(0)}
/> />
}
{ areLoading ? <Spinner /> : null } { areLoading ? <Spinner /> : null }
{ error ? <DangerText>{error}</DangerText> : null } { error ? <DangerText>{error}</DangerText> : null }
</SidebarBox> </SidebarBox>

View File

@@ -17,6 +17,7 @@ interface Props {
currentUserFullName: string; currentUserFullName: string;
tenantSetting: ITenantSetting; tenantSetting: ITenantSetting;
componentRenderedAt: number; componentRenderedAt: number;
postStatusesToShowInFilter: Array<number>;
authenticityToken: string; authenticityToken: string;
} }
@@ -37,6 +38,7 @@ class BoardRoot extends React.Component<Props> {
currentUserFullName, currentUserFullName,
tenantSetting, tenantSetting,
componentRenderedAt, componentRenderedAt,
postStatusesToShowInFilter,
authenticityToken, authenticityToken,
} = this.props; } = this.props;
@@ -49,6 +51,7 @@ class BoardRoot extends React.Component<Props> {
currentUserFullName={currentUserFullName} currentUserFullName={currentUserFullName}
tenantSetting={tenantSetting} tenantSetting={tenantSetting}
componentRenderedAt={componentRenderedAt} componentRenderedAt={componentRenderedAt}
postStatusesToShowInFilter={postStatusesToShowInFilter}
authenticityToken={authenticityToken} authenticityToken={authenticityToken}
/> />
</Provider> </Provider>

View File

@@ -40,6 +40,7 @@ export interface ISiteSettingsGeneralForm {
collapseBoardsInHeader: string; collapseBoardsInHeader: string;
showVoteCount: boolean; showVoteCount: boolean;
showVoteButtonInBoard: boolean; showVoteButtonInBoard: boolean;
hideUnusedStatusesInFilterByStatus: boolean;
showPoweredBy: boolean; showPoweredBy: boolean;
} }
@@ -69,6 +70,7 @@ interface Props {
collapseBoardsInHeader: string, collapseBoardsInHeader: string,
showVoteCount: boolean, showVoteCount: boolean,
showVoteButtonInBoard: boolean, showVoteButtonInBoard: boolean,
hideUnusedStatusesInFilterByStatus: boolean,
showPoweredBy: boolean, showPoweredBy: boolean,
authenticityToken: string authenticityToken: string
): Promise<any>; ): Promise<any>;
@@ -107,6 +109,7 @@ const GeneralSiteSettingsP = ({
collapseBoardsInHeader: originForm.collapseBoardsInHeader, collapseBoardsInHeader: originForm.collapseBoardsInHeader,
showVoteCount: originForm.showVoteCount, showVoteCount: originForm.showVoteCount,
showVoteButtonInBoard: originForm.showVoteButtonInBoard, showVoteButtonInBoard: originForm.showVoteButtonInBoard,
hideUnusedStatusesInFilterByStatus: originForm.hideUnusedStatusesInFilterByStatus,
showPoweredBy: originForm.showPoweredBy, showPoweredBy: originForm.showPoweredBy,
}, },
}); });
@@ -129,6 +132,7 @@ const GeneralSiteSettingsP = ({
data.collapseBoardsInHeader, data.collapseBoardsInHeader,
data.showVoteCount, data.showVoteCount,
data.showVoteButtonInBoard, data.showVoteButtonInBoard,
data.hideUnusedStatusesInFilterByStatus,
data.showPoweredBy, data.showPoweredBy,
authenticityToken authenticityToken
).then(res => { ).then(res => {
@@ -344,7 +348,7 @@ const GeneralSiteSettingsP = ({
className="selectPicker" className="selectPicker"
> >
<option value={TENANT_SETTING_LOGO_LINKS_TO_ROOT_PAGE}> <option value={TENANT_SETTING_LOGO_LINKS_TO_ROOT_PAGE}>
{ I18n.t('site_settings.general.logo_links_to_root_page') } { I18n.t('site_settings.general.logo_links_to_root_page') } ({watch('rootBoardId') === '0' ? I18n.t('roadmap.title') : boards.find(board => board.id === Number(watch('rootBoardId')))?.name})
</option> </option>
<option value={TENANT_SETTING_LOGO_LINKS_TO_CUSTOM_URL}> <option value={TENANT_SETTING_LOGO_LINKS_TO_CUSTOM_URL}>
{ I18n.t('site_settings.general.logo_links_to_custom_url') } { I18n.t('site_settings.general.logo_links_to_custom_url') }
@@ -414,6 +418,16 @@ const GeneralSiteSettingsP = ({
</div> </div>
</div> </div>
<div className="formGroup">
<div className="checkboxSwitch">
<input {...register('hideUnusedStatusesInFilterByStatus')} type="checkbox" id="hide_unused_statuses_in_filter_by_status_checkbox" />
<label htmlFor="hide_unused_statuses_in_filter_by_status_checkbox">{ getLabel('tenant_setting', 'hide_unused_statuses_in_filter_by_status') }</label>
<SmallMutedText>
{ I18n.t('site_settings.general.hide_unused_statuses_in_filter_by_status_help') }
</SmallMutedText>
</div>
</div>
<div className="formGroup"> <div className="formGroup">
<div className="checkboxSwitch"> <div className="checkboxSwitch">
<input {...register('showPoweredBy')} type="checkbox" id="show_powered_by_checkbox" /> <input {...register('showPoweredBy')} type="checkbox" id="show_powered_by_checkbox" />
@@ -424,7 +438,7 @@ const GeneralSiteSettingsP = ({
<br /> <br />
<Button onClick={() => null} disabled={!isDirty}> <Button onClick={() => null} disabled={!isDirty} className="generalSiteSettingsSubmit">
{I18n.t('common.buttons.update')} {I18n.t('common.buttons.update')}
</Button> </Button>
</form> </form>

View File

@@ -33,6 +33,7 @@ const mapDispatchToProps = (dispatch: any) => ({
collapseBoardsInHeader: TenantSettingCollapseBoardsInHeader, collapseBoardsInHeader: TenantSettingCollapseBoardsInHeader,
showVoteCount: boolean, showVoteCount: boolean,
showVoteButtonInBoard: boolean, showVoteButtonInBoard: boolean,
hideUnusedStatusesInFilterByStatus: boolean,
showPoweredBy: boolean, showPoweredBy: boolean,
authenticityToken: string authenticityToken: string
): Promise<any> { ): Promise<any> {
@@ -52,6 +53,7 @@ const mapDispatchToProps = (dispatch: any) => ({
collapse_boards_in_header: collapseBoardsInHeader, collapse_boards_in_header: collapseBoardsInHeader,
show_vote_count: showVoteCount, show_vote_count: showVoteCount,
show_vote_button_in_board: showVoteButtonInBoard, show_vote_button_in_board: showVoteButtonInBoard,
hide_unused_statuses_in_filter_by_status: hideUnusedStatusesInFilterByStatus,
show_powered_by: showPoweredBy, show_powered_by: showPoweredBy,
}, },
locale, locale,

View File

@@ -61,6 +61,7 @@ interface ITenantSetting {
show_vote_count?: boolean; show_vote_count?: boolean;
show_vote_button_in_board?: boolean; show_vote_button_in_board?: boolean;
show_roadmap_in_header?: boolean; show_roadmap_in_header?: boolean;
hide_unused_statuses_in_filter_by_status?: boolean;
show_powered_by?: boolean; show_powered_by?: boolean;
collapse_boards_in_header?: TenantSettingCollapseBoardsInHeader; collapse_boards_in_header?: TenantSettingCollapseBoardsInHeader;
logo_links_to?: TenantSettingLogoLinksTo; logo_links_to?: TenantSettingLogoLinksTo;

View File

@@ -12,12 +12,13 @@ class TenantSettingPolicy < ApplicationPolicy
:feedback_approval_policy, :feedback_approval_policy,
:show_vote_count, :show_vote_count,
:show_vote_button_in_board, :show_vote_button_in_board,
:hide_unused_statuses_in_filter_by_status,
:show_powered_by, :show_powered_by,
:logo_links_to, :logo_links_to,
:logo_custom_url, :logo_custom_url,
:show_roadmap_in_header, :show_roadmap_in_header,
:collapse_boards_in_header, :collapse_boards_in_header,
:custom_css :custom_css,
] ]
else else
[] []

View File

@@ -8,6 +8,7 @@
currentUserFullName: user_signed_in? ? current_user.full_name_or_email : '', currentUserFullName: user_signed_in? ? current_user.full_name_or_email : '',
tenantSetting: @tenant_setting, tenantSetting: @tenant_setting,
componentRenderedAt: Time.now.to_i, componentRenderedAt: Time.now.to_i,
postStatusesToShowInFilter: @post_statuses_to_show_in_filter,
authenticityToken: form_authenticity_token authenticityToken: form_authenticity_token
} }
) )

View File

@@ -11,6 +11,7 @@
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,
hideUnusedStatusesInFilterByStatus: @tenant_setting.hide_unused_statuses_in_filter_by_status,
showPoweredBy: @tenant_setting.show_powered_by, showPoweredBy: @tenant_setting.show_powered_by,
rootBoardId: @tenant_setting.root_board_id.to_s, rootBoardId: @tenant_setting.root_board_id.to_s,
customDomain: @tenant.custom_domain, customDomain: @tenant.custom_domain,

View File

@@ -133,6 +133,7 @@ en:
feedback_approval_policy: 'Feedback approval policy' feedback_approval_policy: 'Feedback approval policy'
show_vote_count: 'Show vote count to users' show_vote_count: 'Show vote count to users'
show_vote_button_in_board: 'Show vote buttons in board page' show_vote_button_in_board: 'Show vote buttons in board page'
hide_unused_statuses_in_filter_by_status: 'Hide unused statuses in filter by status'
show_powered_by: 'Show "Powered by Astuto"' show_powered_by: 'Show "Powered by Astuto"'
root_board_id: 'Root page' root_board_id: 'Root page'
logo_links_to: 'Logo links to' logo_links_to: 'Logo links to'

View File

@@ -206,6 +206,7 @@ en:
custom_domain_learn_more: 'Learn how to configure a custom domain' custom_domain_learn_more: 'Learn how to configure a custom domain'
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_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.' 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.'
hide_unused_statuses_in_filter_by_status_help: 'If you enable this setting, only statuses that are assigned to at least one post in that board will be shown in the filter by status sidebar filter.'
boards: boards:
title: 'Boards' title: 'Boards'
empty: 'There are no boards. Create one below!' empty: 'There are no boards. Create one below!'

View File

@@ -0,0 +1,5 @@
class AddHideUnusedStatusesInFilterByStatusToTenantSettings < ActiveRecord::Migration[6.1]
def change
add_column :tenant_settings, :hide_unused_statuses_in_filter_by_status, :boolean, default: false, null: false
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: 2024_09_16_140807) do ActiveRecord::Schema.define(version: 2024_09_17_140122) 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"
@@ -185,6 +185,7 @@ ActiveRecord::Schema.define(version: 2024_09_16_140807) do
t.boolean "use_browser_locale", default: false, null: false t.boolean "use_browser_locale", default: false, null: false
t.integer "logo_links_to", default: 0, null: false t.integer "logo_links_to", default: 0, null: false
t.string "logo_custom_url" t.string "logo_custom_url"
t.boolean "hide_unused_statuses_in_filter_by_status", default: false, 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