要約
RedmineのWikiではページの親子関係を設定できます。
「索引(名前順)」ページで、親子関係を一望できます。
文章のアウトライン表示として使えそうですが、「索引(名前順)」ページはページですので、各ページの閲覧・編集中は見ることができません。特に親子関係を整理するときに見られないのが不便です。
そこで、サイドバーに同様の情報を常に表示することで、各ページの閲覧・編集中にもページの親子関係を確認できるようにします。
ゴール
次のイメージです。
サイドバーに「索引(名前順)」ページと同等の情報を表示します。
Github リポジトリ はこちらです。
実装
作戦
Redimneプラグインで見た目を変えるときはView Hookを使います。
Hooks List - RedmineにはWikiのSidebar用のView Hookはありません。
そこでRedmine の wiki index ページを tree view で表示する - redmine_wiki_index_tree_view - basyura's blogを参考にします。
def view_layouts_base_html_head(context) return unless context[:controller] params = context[:controller].params return unless (params[:controller] == 'wiki' && params[:action] == 'index') tags = [stylesheet_link_tag('jquery.treeview.css', :plugin => 'redmine_wiki_index_tree_view')] tags << javascript_include_tag('jquery.treeview.js', :plugin => 'redmine_wiki_index_tree_view') tags << javascript_include_tag('redmine_wiki_index_tree_view.js', :plugin => 'redmine_wiki_index_tree_view') tags.join("\n") end
view_layouts_base_sidebar
を使い、Wikiを表示する時だけページツリービューを表示します。
ソースコード
class PollsHookListener < Redmine::Hook::ViewListener
include ActionView::Helpers::DateHelper
include ActionView::Context
# サイドバーにページツリーを表示
def view_layouts_base_sidebar(context = {})
return unless context[:controller]
params = context[:controller].params
return unless params[:controller] == 'wiki'
pages = load_pages(context[:request].params[:project_id])
pages_by_parent_id = pages.group_by(&:parent_id)
content_tag(:div, class: 'page-tree') do
concat content_tag(:h2, 'ページツリービュー', class: 'page-tree__title')
concat render_page_hierarchy(pages_by_parent_id, nil, :timestamp => true)
end
end
# assetsの追加
def view_layouts_base_html_head(context)
return unless context[:controller]
params = context[:controller].params
return unless params[:controller] == 'wiki'
stylesheet_link_tag('sidebar__page-tree', :plugin => 'redmine_plugin_of_yours')
end
private
def load_pages(project_id)
Project.find(project_id)
.wiki
.pages
.with_updated_on
.reorder("#{WikiPage.table_name}.title")
.includes(:wiki => :project)
.includes(:parent)
.to_a
end
def render_page_hierarchy(pages, node=nil, options={})
content = ''
if pages[node]
content << "<ul class=\"pages-tree__list\">\n"
pages[node].each do |page|
if pages[page.id]
content << "<details open>"
content << '<summary class="pages-tree__node">'
content << link_to_wiki(page, options)
content << "</summary>\n"
content << "<li>"
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
content << "</li>\n"
content << "</details>\n"
else
content << '<li class="pages-tree__list__node--leaf">'
content << link_to_wiki(page, options)
content << "</li>\n"
end
end
content << "</ul>\n"
end
content.html_safe
end
def link_to_wiki(page, options)
link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil),
:draggable => 'true')
end
end
ページツリービューの追加
render_page_hierarchy
は索引 (名前順)ページで使われている、render_page_hierarchyを参考ししています。
元々ulタグ、liタグのみでレンダリングしていたのを、detailsタグを使い、ツリーを閉じれるように変更しています。
def render_page_hierarchy(pages, node=nil, options={})
content = ''
if pages[node]
content << "<ul class=\"pages-tree__list\">\n"
pages[node].each do |page|
if pages[page.id]
content << "<details open>"
content << '<summary class="pages-tree__node">'
content << link_to_wiki(page, options)
content << "</summary>\n"
content << "<li>"
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
content << "</li>\n"
content << "</details>\n"
else
content << '<li class="pages-tree__list__node--leaf">'
content << link_to_wiki(page, options)
content << "</li>\n"
end
end
content << "</ul>\n"
end
content.html_safe
end
スタイルシートの追加
見た目を調整するためにview_layouts_base_html_head
View Hookを使って、CSSファイルを追加します。
こちらもview_layouts_base_sidebar
と同様にWikiページの時のみ追加します。
# assetsの追加
def view_layouts_base_html_head(context)
return unless context[:controller]
params = context[:controller].params
return unless params[:controller] == 'wiki'
stylesheet_link_tag('sidebar__page-tree', :plugin => 'redmine_plugin_of_yours')
end
#sidebar .page-tree {
margin-left: -12px;
padding-top: 10px;
}
#sidebar .page-tree__title {
margin-left: 12px;
}
#sidebar .pages-tree__list {
margin: 0 12px;
}
#sidebar .pages-tree__list__node--leaf {
list-style-type: disc;
margin-left: 13px;
}
ドラッグアンドドロップ機能を追加
ページの親子関係をドラッグアンドドロップ操作で編集できるようにします。
次のイメージです。
作戦
名前変更を流用
もともとRedmineのwikiには名前変更ページに親子関係を変更する処理が入っています。
必要なパラメータを入れて /projects/:project_id/wiki/:wiki_page_id/rename
にPOSTすれば親子関係を更新できます。
redmine/rename.html.erb at master · redmine/redmine
<%= labelled_form_for :wiki_page, @page,
:url => { :action => 'rename' },
:html => { :method => :post } do |f| %>
<%= f.text_field :title, :required => true, :size => 100 %>
<% if @page.safe_attribute? 'is_start_page' %><%= f.check_box :is_start_page, :label => :field_start_page, :disabled => @page.is_start_page %>
<% end %><%= f.check_box :redirect_existing_links %>
<%= f.select :parent_id, content_tag('option', '', :value => '') + wiki_page_options_for_select( @wiki.pages.includes(:parent).to_a - @page.self_and_descendants, @page.parent), :label => :field_parent_title %>
```JavaScriptで、このフォームを真似てform要素を作ってsubmitすれば親子関係を更新できます。
<form action="/projects/:project_id/wiki/:wiki_page_id/rename" method="post">
<input type="hidden" name="wiki_page[parent_id]" value=":parent_id">
<input type="hidden" name="${csrfParam}" value="${csrfToken}">
</form>
- 移動するページのURL
- 移動先の新しく親になるページのid
が必要です。
また、RailsでのフォームPOSTにはCSRF対策のトークンも必要です。
HTMLのヘッダーに入っているので、これを使います。
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="j6FeGnAWw0x5MUBOvViQOkZNjKlgTeSpJaH9aSL0h9cy2oFrqwLRQDP00u1Jpn6lKVGv6Xft9UUZnXAAbImzmA==">
ドラッグアンドドロップ
ドラッグアンドドロップはHTML5のドラッグアンドドロップAPIを使って実装します。
実装
属性追加
- ドラッグ可能にするため
draggable
属性を追加してtrue
に設定 - ドロップ先のparent_idを知りたいので、
data-wiki-page-id
属性を追加してaタグに埋め込む
def link_to_wiki(page, options)
link_to(
h(page.pretty_title),
{:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil),
:draggable => 'true',
:data => { :wiki_page_id => page.id })
end
JavaScriptファイル追加
JavaScriptファイルを追加します。
CSSファイルと一緒です。
def view_layouts_base_html_head(context)
return unless context[:controller]
params = context[:controller].params
return unless params[:controller] == 'wiki'
tags = []
tags << stylesheet_link_tag('sidebar__page-tree', :plugin => 'redmine_plugin_of_yours')
tags << javascript_include_tag('sidebar__page-tree', :plugin => 'redmine_plugin_of_yours')
tags.join("\n")
end
ドラッグアンドドロップの実装
JavaScriptでドラッグアンドドロップの動作を実装します。
document.addEventListener('DOMContentLoaded', () => {
Array.from(document.querySelectorAll('.page-tree a'))
.forEach(a => {
// ドッラグ開始時にドラッグ中要素の情報を記録
a.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('url', e.target.getAttribute('href'))
})
// ドラッグオーバーのイベントリスナーでpreventDefaultすると、ドロップ可能になります。
a.addEventListener('dragover', (e) => {
e.preventDefault()
})
// ドロップ時の処理
a.addEventListener('drop', (e) => {
const url = e.dataTransfer.getData('url') // 移動するwikiページのURL
const parentId = e.target.getAttribute('data_wiki_page_id') // 親ページのID
// CSRF対策用トークン
const csrfParam = document.querySelector('[name="csrf-param"]').getAttribute('content')
const csrfToken = document.querySelector('[name="csrf-token"]').getAttribute('content')
// formを作る
const div = document.createElement('div')
div.innerHTML = `
<form action="${url}/rename" method="post">
<input type="hidden" name="wiki_page[parent_id]" value="${parentId}">
<input type="hidden" name="${csrfParam}" value="${csrfToken}">
</form>
`
// formをsubmitする
document.body.appendChild(div)
div.querySelector('form')
.submit()
})
})
})