Redmineでタグを使う

JQueryプラグインのTag-it!を使って、Redmineにタグ機能をつけて見ました。

Tag-it!
http://aehlke.github.io/tag-it/

Redmineのタグ関連プラグインに比べると機能的に劣りますが、SaaSのサービスでRedmineを運用していて、自由にプラグインのインストールができないなどの場合には、スクリプトだけで簡単に実現できるので、使ってみてはどうでしょうか。

１．まず最初に、「管理 » カスタムフィールド » 新しいカスタムフィールド」で、「タグ」の入力項目を作成します。
![カスタムフィールド作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/761560/5bbac60e-35e2-58a5-866b-9c2d384d2a8b.png)

２．この時点では、チケット一覧画面での表示は以下のようになっています（通常のテキスト入力項目です）。
![スクリプト適用前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/761560/0f4fe9a2-004b-88db-5dab-9d41b3bc735f.png)

３．View Customize Pluginを使って、作成したカスタムフィールドにTag-it!を適用します。
外部JavaScriptを読み込むため、View Customize Pluginはv2.1.0以上を使用してください。

```html
<!--
パスのパターン:/issues
挿入位置:全ページのヘッダ
種別:HTML
-->

<style>
    /* 作成したカスタムフィールドの番号に合わせて変更してください */
    .cf_52 {
        display: none;
    }

    p.tagbox {
        padding: 0.1em 0.5em;
        background: #fcfcfc;
        border: solid 1px #ccc;
        border-radius: 5px;
        float: left;
        margin: 2px 3px 0px 0;
        font-weight: 500;
        color: #269;
        line-height: 17px;
    }

    ul.tagit {
        padding: 1px 5px;
        overflow: auto;
        margin-left: inherit;
        /* usually we don't want the regular ul margins. */
        margin-right: inherit;
    }

    ul.tagit li {
        display: block;
        float: left;
        margin: 2px 5px 2px 0;
    }

    ul.tagit li.tagit-choice {
        position: relative;
        line-height: inherit;
    }

    input.tagit-hidden-field {
        display: none;
    }

    ul.tagit li.tagit-choice-read-only {
        padding: .2em .5em .2em .5em;
    }

    ul.tagit li.tagit-choice-editable {
        padding: .2em 18px .2em .5em;
    }

    ul.tagit li.tagit-new {
        padding: .25em 4px .25em 0;
    }

    ul.tagit li.tagit-choice a.tagit-label {
        cursor: pointer;
        text-decoration: none;
    }

    ul.tagit li.tagit-choice .tagit-close {
        cursor: pointer;
        position: absolute;
        right: .1em;
        top: 50%;
        margin-top: -8px;
        line-height: 17px;
    }

    /* used for some custom themes that don't need image icons */
    ul.tagit li.tagit-choice .tagit-close .text-icon {
        display: none;
    }

    ul.tagit li.tagit-choice input {
        display: block;
        float: left;
        margin: 2px 5px 2px 0;
    }

    ul.tagit input[type="text"] {
        -moz-box-sizing: border-box;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;

        -moz-box-shadow: none;
        -webkit-box-shadow: none;
        box-shadow: none;

        border: none;
        margin: 0;
        padding: 0;
        width: inherit;
        background-color: inherit;
        outline: none;
    }
</style>

<script src="https://cdn.jsdelivr.net/npm/tag-it@2.0.0/js/tag-it.min.js"></script>

<script>
    $(function() {
        tags_list = [];
        tagitem_list = [];
        const apiKey = ViewCustomize.context.user.apiKey;
        //プロジェクトIDを取得
        if (ViewCustomize.context.project) {
            project_id = ViewCustomize.context.project.identifier;
            project_id_tag = '/projects/' + project_id;
        } else {
            project_id_tag = "";
        }
        // タグ入力項目追加（初期表示）
        makeavailableTags();

        //チケット一覧グリッド・チケット詳細表示項目にタグ表示デザインを適用
        //作成したカスタムフィールドの番号に合わせて変更してください(cf_52の部分)
        $("td.cf_52, .cf_52 .value").each(function() {
            const txt = $(this).text();
            if (txt.length > 0) {
                const resArray = txt.split(",");
                let ret = "";
                for (let i = 0; i < resArray.length; i++) {
                    ret += '<p class="tagbox"><a href="' + project_id_tag + '/search?utf8=✓&scope=&q=' + resArray[i] + '">' + resArray[i] + '</a></p>';
                }
                $(this).html(ret);
                $('.cf_52').show();
            }
        });

        //チケット一覧グリッドで表示項目の幅を確保
        if ($('td.cf_52').length) {
            $('td.cf_52').show();
            $(".cf_52").css('min-width', '250px');
        }

        // ステータス変更時など、フォームの内容が書き変わるたびにタグ再処理
        const _replaceIssueFormWith = replaceIssueFormWith;
        replaceIssueFormWith = function(html) {
            _replaceIssueFormWith(html);
            makeavailableTags();
        };
    })

    //カスタムフィールドをタグ入力可能にする
    function tagit(tagitem_list) {
        const tagAll = unique(tagitem_list);
        $("#issue_custom_field_values_52").tagit({
            singleField: true,
            //自動補完するワードを設定
            availableTags: tagAll
        });
    }

    //自動補完するワードを準備
    function makeavailableTags() {
        if (tagitem_list.length === 0) { //画面初期表示時に登録済みタグを取得する
            $.when(
                gettagList100()
            ).done(function() {
                //取得したタグをリスト化する
                for (let i = 0; i < tags_list.length; i++) {
                    const tagArray = tags_list[i].split(',');
                    for (let j = 0; j < tagArray.length; j++) {
                        tagitem_list.push(tagArray[j]);
                    }
                }
                tagit(tagitem_list);
            })
        } else { //トラッカー・ステータス変更時には再取得しない
            tagit(tagitem_list);

        }
    }

    let tags_list = [];
    let tagitem_list = [];
    //チケットに登録されているタグを取得 
    function gettagList100() {
        const deferred = new $.Deferred();
        if (ViewCustomize.context.project) {
            url = '' + '/issues.json' + '?project_id=' + project_id + '&status_id=*&cf_52=*&limit=100'; //最新の100件のみ取得
        } else {
            url = '' + '/issues.json' + '?status_id=*&cf_52=*&limit=100';
        }
        $.ajax({
            type: "GET",
            url: url,
            headers: {
                'X-Redmine-API-Key': apiKey
            },
            dataType: "text",
            contentType: 'application/json',
        }).done(function(data) {
            data = JSON.parse(data);
            data1 = data.issues;
            for (i in data1) {
                custom_fields = data1[i].custom_fields;
                for (j in custom_fields) {
                    targetfield = custom_fields[j].id;
                    //作成したカスタムフィールドの番号に合わせて変更してください(52の部分)
                    if (targetfield === 52 && custom_fields[j].value !== "") {
                        tags_list.push(custom_fields[j].value);
                    }
                };
            };
            deferred.resolve();
        });
        return deferred;
    }

    //タグリストの重複を削除
    function unique(list) {
        let result = [];
        $.each(list, function(i, e) {
            if ($.inArray(e, result) == -1) result.push(e);
        });
        return result;
    }
</script>
```

４．スクリプト適用後の画面表示は以下のようになります。

チケット詳細画面
![スクリプト適用後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/761560/f5abc30c-5f5c-ee53-f569-4714046b493b.png)
チケット一覧画面
![チケット一覧画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/761560/00b9db84-2215-46c0-1bf6-bddd50172aa5.png)

５．タグをクリックすると、タグをキーワードにして検索画面に遷移します。
![検索画面に遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/761560/bb470c1c-b5aa-5fca-105c-2723f392c1dc.png)

### Selectize.jsを使用する場合 ###
Selectize.js
https://selectize.github.io/selectize.js/

View Customize Pluginで読み込むjQueryプラグインを以下のように変更してください。

```html
<!--
<script src="https://cdn.jsdelivr.net/npm/tag-it@2.0.0/js/tag-it.min.js"></script>
 -->
<script src="https://cdn.jsdelivr.net/npm/selectize@0.12.6/dist/js/standalone/selectize.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/selectize@0.12.6/dist/css/selectize.bootstrap3.css">
```
スクリプトを以下のように変更してください。

```javascript
    //カスタムフィールドをタグ入力可能にする（selectizeを使用）
    function selectize(tagitem_list) {
        const tagAll = unique(tagitem_list);
        let listresult = [];
        for (i in tagAll) {
            listresult.push({
                name: tagAll[i]
            });
        }
        $('#issue_custom_field_values_52').selectize({
            plugins: ['remove_button', 'restore_on_backspace'],
            valueField: 'name',
            labelField: 'name',
            searchField: 'name',
            preload: 'focus',
            delimiter: ',',
            dropdownParent: 'body',
            closeAfterSelect: true,
            create: true,
            options: listresult,
            sortField: {
                field: 'name'
            }
        });
    }

    //自動補完するワードを準備
    function makeavailableTags() {
        if (tagitem_list.length === 0) { //画面初期表示
            $.when(
                gettagList100()
            ).done(function() {
                //取得したタグをリスト化する
                for (let i = 0; i < tags_list.length; i++) {
                    const tagArray = tags_list[i].split(',');
                    for (let j = 0; j < tagArray.length; j++) {
                        tagitem_list.push(tagArray[j]);
                    }
                }
                //tagit(tagitem_list);
                selectize(tagitem_list);
            })
        } else { //トラッカー・ステータス変更時
            //tagit(tagitem_list);
            selectize(tagitem_list);
        }
    }
```
![Selectize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/761560/a68de7e5-13d3-38ef-cc21-6f2831332c5e.png)