mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 03:37:56 +01:00
Improve roadmap (#405)
* Make it possible to embed the roadmap in iframe * Add board and status filters * Add query params to show/hide roadmap filters
This commit is contained in:
committed by
GitHub
parent
2e07f7b00d
commit
5780d8494e
@@ -1,51 +1,75 @@
|
||||
.roadmapColumns {
|
||||
@extend
|
||||
.d-flex,
|
||||
.justify-content-between,
|
||||
.flex-wrap;
|
||||
// parent .container is 960px wide with 15px padding on each side => 930px available
|
||||
// each roadmap column is 31% wide => 930px * 0.31 = 288.3px * 3 = 864.9px plus 16px gap between columns (2 gaps) => 864.9px + 32px = 896.9px occupied
|
||||
|
||||
.roadmapColumn {
|
||||
.roadmap {
|
||||
.filters {
|
||||
@extend
|
||||
.card,
|
||||
.my-2,
|
||||
.p-0;
|
||||
|
||||
width: 32%;
|
||||
background-color: var(--astuto-grey-light);
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.columnHeader {
|
||||
@extend
|
||||
.card-header,
|
||||
.d-flex;
|
||||
|
||||
.columnTitle {
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
overflow-y: auto;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.postLink {
|
||||
@extend .my-2;
|
||||
|
||||
&:hover { text-decoration: none; }
|
||||
}
|
||||
|
||||
.postListItem {
|
||||
@extend
|
||||
.card3D,
|
||||
.d-flex,
|
||||
.flex-column,
|
||||
.m-2,
|
||||
.py-2;
|
||||
.mb-4;
|
||||
|
||||
gap: 16px;
|
||||
|
||||
@include media-breakpoint-down(sm) { flex-direction: column; }
|
||||
|
||||
.boardSelect, .postStatusSelect {
|
||||
width: 50%;
|
||||
|
||||
@include media-breakpoint-down(sm) { width: 100%; }
|
||||
}
|
||||
|
||||
.boardSelect { @extend .align-self-end; }
|
||||
}
|
||||
}
|
||||
|
||||
.roadmapColumns {
|
||||
@extend
|
||||
.d-flex,
|
||||
.justify-content-start,
|
||||
.flex-wrap;
|
||||
|
||||
gap: 16px;
|
||||
|
||||
.roadmapColumn {
|
||||
@extend
|
||||
.card,
|
||||
.p-0;
|
||||
|
||||
width: calc(33% - 9px); // each col takes 1/3 of the space minus 9px (to account for flex-gap: 16px, otherwise it'd wrap)
|
||||
background-color: var(--astuto-grey-light);
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.columnHeader {
|
||||
@extend
|
||||
.card-header,
|
||||
.d-flex;
|
||||
|
||||
.columnTitle {
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
overflow-y: auto;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.postLink {
|
||||
@extend .my-2;
|
||||
|
||||
&:hover { text-decoration: none; }
|
||||
}
|
||||
|
||||
.postListItem {
|
||||
@extend
|
||||
.card3D,
|
||||
.d-flex,
|
||||
.flex-column,
|
||||
.m-2,
|
||||
.py-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#roadmapEmbedCode {
|
||||
@extend .mt-4;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
class StaticPagesController < ApplicationController
|
||||
skip_before_action :load_tenant_data, only: [:showcase, :pending_tenant, :blocked_tenant]
|
||||
before_action :allow_iframe_embedding, only: [:embedded_roadmap]
|
||||
|
||||
def root
|
||||
@board = Board.find_by(id: Current.tenant.tenant_setting.root_board_id)
|
||||
@@ -19,6 +20,14 @@ class StaticPagesController < ApplicationController
|
||||
get_roadmap_data
|
||||
end
|
||||
|
||||
def embedded_roadmap
|
||||
@page_title = t('roadmap.title')
|
||||
get_roadmap_data
|
||||
@is_embedded = true
|
||||
|
||||
render 'static_pages/roadmap', layout: 'embedded'
|
||||
end
|
||||
|
||||
def showcase
|
||||
render html: 'Showcase home page.'
|
||||
end
|
||||
@@ -40,4 +49,8 @@ class StaticPagesController < ApplicationController
|
||||
.find_with_post_status_in(@post_statuses)
|
||||
.select(:id, :title, :board_id, :post_status_id, :user_id, :created_at)
|
||||
end
|
||||
|
||||
def allow_iframe_embedding
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
end
|
||||
end
|
||||
@@ -10,9 +10,10 @@ import IBoard from '../../interfaces/IBoard';
|
||||
interface Props {
|
||||
posts: Array<IPostJSON>;
|
||||
boards: Array<IBoard>;
|
||||
openPostsInNewTab: boolean;
|
||||
}
|
||||
|
||||
const PostList = ({ posts, boards }: Props) => (
|
||||
const PostList = ({ posts, boards, openPostsInNewTab }: Props) => (
|
||||
<div className="postList">
|
||||
{
|
||||
posts.length > 0 ?
|
||||
@@ -21,7 +22,7 @@ const PostList = ({ posts, boards }: Props) => (
|
||||
id={post.id}
|
||||
title={post.title}
|
||||
boardName={boards.find(board => board.id === post.board_id).name}
|
||||
|
||||
openPostInNewTab={openPostsInNewTab}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -11,9 +11,10 @@ interface Props {
|
||||
postStatus: IPostStatus;
|
||||
posts: Array<IPostJSON>;
|
||||
boards: Array<IBoard>;
|
||||
openPostsInNewTab: boolean;
|
||||
}
|
||||
|
||||
const PostListByPostStatus = ({ postStatus, posts, boards }: Props) => (
|
||||
const PostListByPostStatus = ({ postStatus, posts, boards, openPostsInNewTab }: Props) => (
|
||||
<div className="roadmapColumn">
|
||||
<div className="columnHeader"
|
||||
style={{backgroundColor: postStatus.color}}>
|
||||
@@ -23,6 +24,7 @@ const PostListByPostStatus = ({ postStatus, posts, boards }: Props) => (
|
||||
<PostList
|
||||
posts={posts}
|
||||
boards={boards}
|
||||
openPostsInNewTab={openPostsInNewTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,11 @@ interface Props {
|
||||
id: number;
|
||||
title: string;
|
||||
boardName: string;
|
||||
openPostInNewTab: boolean;
|
||||
}
|
||||
|
||||
const PostListItem = ({id, title, boardName}: Props) => (
|
||||
<a href={`/posts/${id}`} className="postLink">
|
||||
const PostListItem = ({id, title, boardName, openPostInNewTab}: Props) => (
|
||||
<a href={`/posts/${id}`} className="postLink" target={openPostInNewTab ? '_blank' : '_self'}>
|
||||
<div className="postListItem">
|
||||
<TitleText>{title}</TitleText>
|
||||
<UppercaseText>{boardName}</UppercaseText>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import PostListByPostStatus from './PostListByPostStatus';
|
||||
import MultiSelect, { MultiSelectOption } from '../common/MultiSelect';
|
||||
|
||||
import IPostStatus from '../../interfaces/IPostStatus';
|
||||
import IPostJSON from '../../interfaces/json/IPost';
|
||||
@@ -10,23 +11,105 @@ interface Props {
|
||||
postStatuses: Array<IPostStatus>;
|
||||
posts: Array<IPostJSON>;
|
||||
boards: Array<IBoard>;
|
||||
isEmbedded: boolean;
|
||||
}
|
||||
|
||||
class Roadmap extends React.Component<Props> {
|
||||
render () {
|
||||
interface State {
|
||||
selectedBoards: Array<MultiSelectOption>;
|
||||
selectedPostStatuses: Array<MultiSelectOption>;
|
||||
}
|
||||
|
||||
class Roadmap extends React.Component<Props, State> {
|
||||
private showBoardFilter: boolean;
|
||||
private showPostStatusFilter: boolean;
|
||||
private boardsToShow: Array<number>;
|
||||
private postStatusesToShow: Array<number>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// read query params
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
this.showBoardFilter = queryParams.get('show_board_filter') !== 'false';
|
||||
this.showPostStatusFilter = queryParams.get('show_status_filter') !== 'false';
|
||||
this.boardsToShow = queryParams.get('show_boards') ? queryParams.get('show_boards').split(',').map(Number) : props.boards.map(board => board.id);
|
||||
this.postStatusesToShow = queryParams.get('show_statuses') ? queryParams.get('show_statuses').split(',').map(Number) : props.postStatuses.map(postStatus => postStatus.id);
|
||||
|
||||
this.state = {
|
||||
selectedBoards: props.boards.filter(board => this.boardsToShow.includes(board.id)).map(board => ({ value: board.id, label: board.name })),
|
||||
selectedPostStatuses: props.postStatuses.filter(postStatus => this.postStatusesToShow.includes(postStatus.id)).map(postStatus => ({ value: postStatus.id, label: postStatus.name, color: postStatus.color })),
|
||||
};
|
||||
|
||||
this.setSelectedBoards = this.setSelectedBoards.bind(this);
|
||||
this.setSelectedPostStatuses = this.setSelectedPostStatuses.bind(this);
|
||||
}
|
||||
|
||||
setSelectedBoards(selectedBoards: Array<MultiSelectOption>) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
selectedBoards,
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedPostStatuses(selectedPostStatuses: Array<MultiSelectOption>) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
selectedPostStatuses,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { postStatuses, posts, boards } = this.props;
|
||||
const { selectedBoards, selectedPostStatuses } = this.state;
|
||||
|
||||
const boardSelectOptions = boards.filter(board => this.boardsToShow.includes(board.id)).map(board => ({ value: board.id, label: board.name }));
|
||||
const postStatusSelectOptions = postStatuses.filter(postStatus => this.postStatusesToShow.includes(postStatus.id)).map(postStatus => ({ value: postStatus.id, label: postStatus.name, color: postStatus.color }));
|
||||
|
||||
// Filter by board
|
||||
const filteredPosts = posts.filter(post =>
|
||||
selectedBoards.some(selectedBoard => selectedBoard.value === post.board_id)
|
||||
);
|
||||
|
||||
// Filter by post status
|
||||
const filteredPostStatuses = postStatuses.filter(postStatus =>
|
||||
selectedPostStatuses.some(selectedPostStatus => selectedPostStatus.value === postStatus.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="roadmapColumns">
|
||||
{postStatuses.map((postStatus, i) => (
|
||||
<PostListByPostStatus
|
||||
postStatus={postStatus}
|
||||
posts={posts.filter(post => post.post_status_id === postStatus.id)}
|
||||
boards={boards}
|
||||
<div className="roadmap">
|
||||
<div className="filters">
|
||||
{
|
||||
this.showBoardFilter &&
|
||||
<MultiSelect
|
||||
options={boardSelectOptions}
|
||||
defaultValue={selectedBoards}
|
||||
onChange={this.setSelectedBoards}
|
||||
className="boardSelect"
|
||||
/>
|
||||
}
|
||||
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
this.showPostStatusFilter &&
|
||||
<MultiSelect
|
||||
options={postStatusSelectOptions}
|
||||
defaultValue={selectedPostStatuses}
|
||||
onChange={this.setSelectedPostStatuses}
|
||||
className="postStatusSelect"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="roadmapColumns">
|
||||
{filteredPostStatuses.map((postStatus, i) => (
|
||||
<PostListByPostStatus
|
||||
postStatus={postStatus}
|
||||
posts={filteredPosts.filter(post => post.post_status_id === postStatus.id)}
|
||||
boards={boards}
|
||||
openPostsInNewTab={this.props.isEmbedded}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import Box from '../../common/Box';
|
||||
import { MutedText } from '../../common/CustomTexts';
|
||||
import CopyToClipboardButton from '../../common/CopyToClipboardButton';
|
||||
import Switch from '../../common/Switch';
|
||||
|
||||
interface Props {
|
||||
embeddedRoadmapUrl: string;
|
||||
}
|
||||
|
||||
const RoadmapEmbedding: React.FC<Props> = ({ embeddedRoadmapUrl }) => {
|
||||
const [showBoardFilter, setShowBoardFilter] = React.useState(true);
|
||||
const [showPostStatusFilter, setShowPostStatusFilter] = React.useState(false);
|
||||
const [embedCode, setEmbedCode] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
const code = `
|
||||
<iframe
|
||||
src="${embeddedRoadmapUrl}?show_board_filter=${showBoardFilter}&show_status_filter=${showPostStatusFilter}"
|
||||
width="860"
|
||||
height="600"
|
||||
seamless
|
||||
frameborder="0"></iframe>
|
||||
`;
|
||||
setEmbedCode(code.replace(/\s+/g, ' ').trim());
|
||||
}, [embeddedRoadmapUrl, showBoardFilter, showPostStatusFilter]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<h2>{I18n.t('site_settings.roadmap.title_embed')}</h2>
|
||||
|
||||
<Switch
|
||||
label={I18n.t('site_settings.roadmap.show_board_filter')}
|
||||
onClick={() => setShowBoardFilter(!showBoardFilter)}
|
||||
checked={showBoardFilter}
|
||||
htmlId="showBoardFilterCheckbox"
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={I18n.t('site_settings.roadmap.show_post_status_filter')}
|
||||
onClick={() => setShowPostStatusFilter(!showPostStatusFilter)}
|
||||
checked={showPostStatusFilter}
|
||||
htmlId="showPostStatusFilterCheckbox"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
value={embedCode}
|
||||
onChange={event => setEmbedCode(event.target.value)}
|
||||
rows={5}
|
||||
id="roadmapEmbedCode"
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<MutedText>{I18n.t('site_settings.roadmap.embed_help')}</MutedText>
|
||||
|
||||
<div>
|
||||
<CopyToClipboardButton
|
||||
label={I18n.t('common.buttons.copy_to_clipboard')}
|
||||
textToCopy={embedCode}
|
||||
copiedLabel={I18n.t('common.copied')}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoadmapEmbedding;
|
||||
@@ -2,14 +2,16 @@ import * as React from 'react';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import { PostStatusesState } from "../../../reducers/postStatusesReducer";
|
||||
import Box from '../../common/Box';
|
||||
import RoadmapEmbedding from './RoadmapEmbedding';
|
||||
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
|
||||
import RoadmapPostStatus from './RoadmapPostStatus';
|
||||
import IPostStatus from '../../../interfaces/IPostStatus';
|
||||
import { PostStatusesState } from "../../../reducers/postStatusesReducer";
|
||||
import { MutedText } from '../../common/CustomTexts';
|
||||
|
||||
interface Props {
|
||||
embeddedRoadmapUrl: string,
|
||||
authenticityToken: string,
|
||||
postStatuses: PostStatusesState,
|
||||
settingsAreUpdating: boolean,
|
||||
@@ -83,7 +85,7 @@ class RoadmapSiteSettingsP extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { postStatuses, settingsAreUpdating, settingsError } = this.props;
|
||||
const { embeddedRoadmapUrl, postStatuses, settingsAreUpdating, settingsError } = this.props;
|
||||
const { isDragging } = this.state;
|
||||
|
||||
let statusesInRoadmap = postStatuses.items.filter(postStatus => postStatus.showInRoadmap);
|
||||
@@ -158,6 +160,8 @@ class RoadmapSiteSettingsP extends React.Component<Props, State> {
|
||||
</Droppable>
|
||||
</Box>
|
||||
|
||||
<RoadmapEmbedding embeddedRoadmapUrl={embeddedRoadmapUrl} />
|
||||
|
||||
<SiteSettingsInfoBox areUpdating={settingsAreUpdating || postStatuses.areLoading} error={settingsError} />
|
||||
</DragDropContext>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import createStoreHelper from '../../../helpers/createStore';
|
||||
import { State } from '../../../reducers/rootReducer';
|
||||
|
||||
interface Props {
|
||||
embeddedRoadmapUrl: string;
|
||||
authenticityToken: string;
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ class RoadmapSiteSettingsRoot extends React.Component<Props> {
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<RoadmapSiteSettings
|
||||
embeddedRoadmapUrl={this.props.embeddedRoadmapUrl}
|
||||
authenticityToken={this.props.authenticityToken}
|
||||
/>
|
||||
</Provider>
|
||||
|
||||
106
app/javascript/components/common/MultiSelect.tsx
Normal file
106
app/javascript/components/common/MultiSelect.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as React from 'react';
|
||||
import I18n from 'i18n-js';
|
||||
import Select, { components } from 'react-select';
|
||||
|
||||
export type MultiSelectOption = {
|
||||
value: number;
|
||||
label: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
options: Array<MultiSelectOption>;
|
||||
defaultValue: Array<MultiSelectOption>;
|
||||
onChange: (selectedOptions: Array<MultiSelectOption>) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SELECTED_COLOR = '#e5e5e5';
|
||||
|
||||
const ColoredOption = props => {
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<span style={{ backgroundColor: props.data.color, color: 'white', padding: '4px', borderRadius: '4px' }}>
|
||||
{props.data.label}
|
||||
</span>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiSelect = ({
|
||||
options,
|
||||
defaultValue,
|
||||
onChange,
|
||||
className,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
options={options}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
hideSelectedOptions={false}
|
||||
isClearable={false}
|
||||
isSearchable
|
||||
noOptionsMessage={() => I18n.t('common.select_no_options_available')}
|
||||
placeholder={I18n.t('common.select_placeholder')}
|
||||
components={{ Option: (options && options.length > 0 && 'color' in options[0]) ? ColoredOption : components.Option }}
|
||||
styles={{
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
boxShadow: 'none',
|
||||
borderColor: state.isFocused ? '#333333' : '#cdcdcd',
|
||||
'&:hover': {
|
||||
boxShadow: 'none',
|
||||
borderColor: '#333333',
|
||||
},
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isSelected ? SELECTED_COLOR : 'white',
|
||||
color: state.isSelected ? '#333333' : 'inherit',
|
||||
'&:hover': {
|
||||
filter: 'brightness(0.8)',
|
||||
},
|
||||
'&:active': {
|
||||
backgroundColor: SELECTED_COLOR,
|
||||
},
|
||||
}),
|
||||
multiValue: (provided, state) => {
|
||||
return {
|
||||
...provided,
|
||||
marginRight: '4px',
|
||||
};
|
||||
},
|
||||
multiValueLabel: (provided, state) => {
|
||||
const option = options.find(opt => opt.value === state.data.value);
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: option.color ? option.color : 'inherit',
|
||||
color: option.color ? 'white' : 'inherit',
|
||||
borderTopRightRadius: '0px',
|
||||
borderBottomRightRadius: '0px',
|
||||
};
|
||||
},
|
||||
multiValueRemove: (provided, state) => {
|
||||
const option = options.find(opt => opt.value === state.data.value);
|
||||
return {
|
||||
...provided,
|
||||
backgroundColor: option.color ? option.color : 'inherit',
|
||||
color: option.color ? 'white' : 'inherit',
|
||||
borderTopLeftRadius: '0px',
|
||||
borderBottomLeftRadius: '0px',
|
||||
'&:hover': {
|
||||
backgroundColor: option.color ? option.color : '#fbfbfb',
|
||||
color: option.color ? 'white' : 'inherit',
|
||||
filter: 'brightness(0.8)',
|
||||
},
|
||||
};
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultiSelect;
|
||||
32
app/views/layouts/embedded.html.erb
Normal file
32
app/views/layouts/embedded.html.erb
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= render 'layouts/page_title' %></title>
|
||||
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="turbolinks-cache-control" content="no-cache">
|
||||
|
||||
<%= render 'layouts/set_js_locale' %>
|
||||
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
||||
<%= favicon_link_tag asset_path('favicon.png') %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<%= 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>
|
||||
@@ -5,6 +5,7 @@
|
||||
react_component(
|
||||
'SiteSettings/Roadmap',
|
||||
{
|
||||
embeddedRoadmapUrl: get_url_for(method(:embedded_roadmap_url)),
|
||||
authenticityToken: form_authenticity_token
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
postStatuses: @post_statuses,
|
||||
posts: @posts,
|
||||
boards: @boards,
|
||||
isEmbedded: @is_embedded
|
||||
}
|
||||
)
|
||||
%>
|
||||
Reference in New Issue
Block a user