LoginSignup
9
16

More than 3 years have passed since last update.

RedmineプラグインでWikiのサイドバーにページツリービューを表示する

Last updated at Posted at 2017-10-23

要約

RedmineのWikiではページの親子関係を設定できます。
「索引(名前順)」ページで、親子関係を一望できます。
文章のアウトライン表示として使えそうですが、「索引(名前順)」ページはページですので、各ページの閲覧・編集中は見ることができません。特に親子関係を整理するときに見られないのが不便です。
そこで、サイドバーに同様の情報を常に表示することで、各ページの閲覧・編集中にもページの親子関係を確認できるようにします。

ゴール

次のイメージです。

Screen Shot 2017-10-23 at 10.18.39.png

サイドバーに「索引(名前順)」ページと同等の情報を表示します。

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_headView 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;
}

ドラッグアンドドロップ機能を追加

ページの親子関係をドラッグアンドドロップ操作で編集できるようにします。
次のイメージです。
out.gif

作戦

名前変更を流用

もともと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| %>
<div class="box tabular">
<p><%= f.text_field :title, :required => true, :size => 100  %></p>
<% if @page.safe_attribute? 'is_start_page' %>
<p><%= f.check_box :is_start_page, :label => :field_start_page, :disabled => @page.is_start_page %></p>
<% end %>
<p><%= f.check_box :redirect_existing_links %></p>
<p><%= 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 %></p>

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を使って実装します。

実装

属性追加

  1. ドラッグ可能にするためdraggable属性を追加してtrueに設定
  2. ドロップ先の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()
      })
    })
})

参考

9
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
16