Redmine: 長いテキストをRedmine 3.4 のワイド表示とは異なる形で、広めに表示する(View customize plugin)

Redmine 3.4 では、長いテキストに対して「ワイド表示」というオプションが追加されていて、長いテキストを見やすく表示することが可能になりました。

上記機能だと、表示位置が説明欄の下に来てしまうので、今回は説明欄の上に表示するようにView customizeで実装してみました。

なお、Redmine 3.4の機能を使っているわけではないので、Redmine 3.4 未満でワイド表示したい場合にも参考になるかと思います。

View customize の設定内容

Path pattern

チケット画面を対象にします。

/issues

Code

Type:JavaScriptとして下記を設定します。カスタムフィールドのIDに応じて、customFieldId を変更してください。

$(function() {
  // 処理したいカスタムフィールドに応じて変更
  var customFieldId = 3;

  // 詳細表示エリア
  function changeDetailArea() {

    var target = $('div.cf_' + customFieldId + '.attribute');

    // 移動
    $('div.issue > div.attributes').append(
      $('<div>', { class: 'splitcontent' }).append(target));
  }

  // 編集エリア
  function changeEditArea() {

    var target = $('#issue_custom_field_values_' + customFieldId).parent();

    // 移動
    $('#attributes').append(target);
  }

  changeDetailArea();
  changeEditArea();

  // ステータス変更時などにDOMが差し替えられるので
  // フォームの内容が書き変わるたびに調整
  var _replaceIssueFormWith = replaceIssueFormWith;
  replaceIssueFormWith = function(html){

    _replaceIssueFormWith(html);

    changeDetailArea();
    changeEditArea();
  };
})

画面イメージ

「長いテキスト1」が、View customizeで変更したものになります。

f:id:onozaty:20170809004349p:plain

Jackson 2.8.7 で、Lombokの@AllArgsConstructorと@NoArgsConstructorを指定していて、一部プロパティを@JsonIgnoreとしているとJsonMappingExceptionが起きる

下記のようなコードを書くと、Jackson 2.8.7 だとエラーとなります。

public class JacksonExample {

  public static void main(String[] args) throws IOException {

    ObjectMapper mapper = new ObjectMapper();

    User userBefore = new User(1, "Taro", "password");

    String json = mapper.writeValueAsString(userBefore);
    User userAfter = mapper.readValue(json, User.class);
  }

  @Data
  @AllArgsConstructor
  @NoArgsConstructor
  public static class User {

    private int id;

    private String name;

    @JsonIgnore
    private String password;
  }
}

スタックとレースは下記のような感じです。

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Could not find creator property with name 'id' (in class com.example.JacksonExample$User)
 at [Source: {"id":1,"name":"Taro"}; line: 1, column: 1]
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
    at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:551)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:226)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:141)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:403)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:476)
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:3899)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3794)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
    at com.example.JacksonExample.main(JacksonExample.java:32)

エラー見ても意味がわからなくて、調べていったら、結局 2.8.7 だけでの問題で、前後のバージョン(2.8.6、2.8.8)で発生しないものでした。

条件としては、下記の場合に起きるようです。

  • 引数なしコンストラクタと、引数ありコンストラクタの両方がある
  • 引数ありコンストラクタの引数の数と、JSON上のプロパティの数が異なる

なお 2.8.7 で回避したい場合には、Jacksonとしてどちらかのコンストラクタしか使わないようにさせれば大丈夫でした。ということで、コンストラクタにJsonIgnoreを指定すれば回避できます。

  @Data
  @AllArgsConstructor(onConstructor = @__(@JsonIgnore))
  @NoArgsConstructor
  public static class User {

    private int id;

    private String name;

    @JsonIgnore
    private String password;
  }

ちなみに、Jacksonのようなものって、引数なしコンストラクタしか使わないと思っていましたが、そうではないんですね。 引数ありコンストラクタの引数とプロパティ名をどうマッピングしているんだろうと思いましたが、@java.beans.ConstructorProperties を見てマッピングしてくれているようでした。(LombokだとConstructorPropertiesも付与される)

Jacksonだと Immutable なオブジェクトが扱えるのですばらしいですね。

Redmine: チケット編集時の送信ボタンにショートカットキーを割り当てる(View customize plugin)

チケット編集画面で送信ボタンに対するショートカットキーがあれば、、というのを見かけたので、View customize pluginで実装してみました。

accesskeyを設定するだけの簡単な変更です。

View customize の設定内容

Path pattern

チケット画面を対象とします。

/issues

Code

Type:JavaScriptとして、チケット作成/編集時の作成/送信ボタンに対してaccesskeyを追加するコードを設定します。

$(function() {
  $('#issue-form input[name="commit"]').attr('accesskey', 's');
});

これでAlt+Shift+sで送信ボタンが押下できます。(WindowsのFirefoxで確認)

accesskeyを起動する方法は、OS、ブラウザによって異なるので、詳しくは下記をご参照ください。

Redmine: 子チケットの一覧にボタンを追加して、REST API経由で親子関係を外す(View customize plugin)

ということで、サンプルとして「子チケットの一覧にボタンを追加して、REST API経由で親子関係を外す」ということをやってみました。

REST APIの認証は、APIキーを払い出したものをヘッダにX-Redmine-API-Keyとして設定する方法にしています。他にもいろいろ認証方式があるので、用途に応じて使い分けるのが良いかと思います。

View customize の設定内容

Path pattern

チケット画面を対象にします。

/issues/[0-9]+$

Code

Type:JavaScriptとして下記を設定します。APIキーは適宜変更してください。

追記@2017-11-12:application/jsonで送れていなかったので修正しました。以前の形(デフォルトだとapplication/x-www-form-urlencoded)だとcustom_fieldsのようなネストした構造のデータをうまく送信できませんでした。

$(function() {

  // REST APIのキー
  var apiKey = '払い出したAPIキー';

  // 子チケット一覧を対象に
  $('tr.child').each(function() {
    var target = $(this);

    // チケットIDを取得
    var issueUrl = target.find('td.subject > a').attr('href');
    var issueId = issueUrl .substr(issueUrl .lastIndexOf('/') + 1);

    // ボタンを追加して、ボタン押下時にチケットを更新
    var button = $('<input type="button" value="親子関係を外す">');
    button.on('click', function() {

      $.ajax({
          type: 'PUT',
          url: '/issues/' + issueId + '.json',
          headers: {
            'X-Redmine-API-Key': apiKey
          },
          // 更新時はレスポンスのコンテンツが無く
          // jsonだとエラーとなるのでtextにしておく
          dataType: 'text',
          contentType: 'application/json',
          data: JSON.stringify({
            'issue': {
              'parent_issue_id': '' // 親チケットIDをクリア
            }
          })
      }).done(function(data) {
        // 成功したらリロード
        location.reload();
      }).fail(function(data) {
        alert('失敗しました');
      });

    });

    target.append($('<td>').append(button));
  });
})

画面イメージ

下記のように子チケット一覧部分にボタンが追加され、ボタン押下で親子が外れます。

f:id:onozaty:20170723002608p:plain

REST APIについて

今回はチケットの更新を行いましたが、REST APIでは他にもいろいろできますので、ぜひ下記のドキュメントを参照してみてください。

Redmine 3.4.0、3.4.1 がリリースされました

Redmine 3.4.0、3.4.1 がリリースされました。(3.4.0が7月2日、バグフィックスで3.4.1が7月9日)

View Customizeだけは動作確認済みですが、他のプラグインも後ほど確認します。

いつもどおりVagrantのboxもあげておきました。

Redmine: チケット一覧でカスタムフィールドの値に応じて行のフォントを太字に変える(View customize plugin)

カスタムフィールドの値に応じて行を装飾する方法について問い合わせをいただいたので、「カスタムフィールド1の値が'A'の場合には、行のフォントを太字に変える」をView customizeで行ってみます。

チケットの優先度など、デフォルトで存在するフィールドについては、もとからclassが振られているのでCSSで装飾しやすいですが、カスタムフィールドだとclassが振られていないので、CSSだけではできません。 そのためJavaScriptにて値を判定して、スタイルを設定することによって対応します。

View customize の設定内容

Path pattern

チケット一覧を対象にします。

/issues$

Code

Type:JavaScriptとして下記を設定します。cf_1の部分は、該当のカスタムフィールドに応じて変更してください。

$(function() {
  $('table.issues td.cf_1')
    .filter(function() {
      return $(this).text() == 'A';
    })
    .parent()
    .css('color', 'red');
});

設定後のイメージ

f:id:onozaty:20170530000920p:plain

一覧に該当のカスタムフィールドが表示されていないと判定ができないので、ご注意ください。

その他

複数の属性を指定するような場合だと、JavaScriptだと面倒かと思います。そういった場合には、JavaScript側ではclassを追加するだけで、そのclassに応じたCSSを別途定義しておくような形がよいかと思います。