その他

【シン感覚】Arc ブラウザ ✖️ Redmine【Boost Edit】

その他
この記事は約18分で読めます。

Arc ブラウザには Boost という機能があるのは、みなさんご存知でしょうか?
特定のブラウザの色を変えたり、要素を消したり足したりできる機能になっています。

今回は試しに Redmine を使って、より使いやすいツールに生まれ変わらせてみようと思います。

結論のBoost内容は以下になります。

JavaScript 内容

プロジェクトの色の定義は、各自のプロジェクト名を入力してください。


// プロジェクトの色の定義
var projects = [
  { name: "ProjectA", color: "black" },
  { name: "ProjectB", color: "pink" },
  { name: "ProjectC", color: "skyblue" },
];

document.addEventListener("DOMContentLoaded", function () {
    var header = document.querySelector("#header");
    var transitions = [
        "-moz-transition: 0.8s ease",
        "-webkit-transition: 0.8s ease",
        "-o-transition: 0.8s ease",
        "-ms-transition: 0.8s ease"
    ];

    // Set initial styles for the header
    if(document.querySelector("#project-jump > span").textContent.trim() !== "プロジェクトへ移動..."){
      header.style.background = 'none'; // Set the initial background to none
    }
    header.style.cssText += ';' + transitions.join(';');

    // Function to update header color based on the current project
    function updateHeaderColor() {
        var currentProject = document.querySelector("#project-jump > span").textContent.trim();

        // Check if the current project matches any defined projects and update the header color
        projects.forEach(function (project) {
            if (currentProject.indexOf(project.name) > -1) {
                document.querySelector("#header").style.backgroundColor = project.color;
            }
        });
    }

    // Call the function on page load
    updateHeaderColor();

    // Add an event listener for changes in the project jump span
    var projectJumpSpan = document.querySelector("#project-jump > span");
    if (projectJumpSpan) {
        projectJumpSpan.addEventListener("DOMSubtreeModified", updateHeaderColor);
    }
});

// Redmine タスク詳細のカスタマイズ
document.addEventListener('DOMContentLoaded', function () {
  var changeCustomFieldPosition = function () {
    var trackerId = document.getElementById('issue_tracker_id').value;

    if (trackerId === '3') {
      // 表示画面の調整
      if (document.querySelector('.description')) {
        var moveCustomFields = function(fields) {
          document.querySelector('.description').insertAdjacentHTML('afterend', '<hr>');

          fields.forEach(function (field) {
            document.querySelector('.description').insertAdjacentHTML('beforeend', field.outerHTML);
          });
        };

        var mycf_kisyu = document.querySelector('.cf_10.attribute');
        var mycf_base_ver = document.querySelector('.cf_7.attribute');
        var mycf_mokuteki = document.querySelector('.cf_6.attribute');
        var mycf_youkyuu = document.querySelector('.cf_5.attribute');
        var mycf_kousuu = document.querySelector('.cf_11.attribute');
        var mycf_fix_ver = document.querySelector('.cf_8.attribute');
        var mycf_result = document.querySelector('.cf_9.attribute');

        moveCustomFields([
          mycf_kisyu, mycf_base_ver,
          '<div class="splitcontentleft">' + mycf_mokuteki.outerHTML + '</div>',
          '<div class="splitcontentleft">' + mycf_youkyuu.outerHTML + '</div>',
          '<hr>',
          '<p> <strong>★【作業者記入】</strong></p>',
          '<div class="splitcontent">',
          '<div class="splitcontentleft">' + mycf_kousuu.outerHTML + '</div>',
          '<div class="splitcontentleft">' + mycf_fix_ver.outerHTML + '</div>',
          '</div>',
          mycf_result.outerHTML
        ]);
      }

      // 編集画面の調整
      var customFields = [
        document.getElementById('issue_custom_field_values_10').parentElement,
        document.getElementById('issue_custom_field_values_7').parentElement,
        document.getElementById('issue_custom_field_values_6').parentElement,
        document.getElementById('issue_custom_field_values_5').parentElement,
        document.getElementById('issue_custom_field_values_11').parentElement,
        document.getElementById('issue_custom_field_values_8').parentElement,
        document.getElementById('issue_custom_field_values_9').parentElement
      ];

      document.getElementById('all_attributes').insertAdjacentHTML('beforeend', '<hr>');

      moveCustomFields(customFields);
    }
  };

  // 初期変換
  changeCustomFieldPosition();

  // ステータス変更時など、DOMが差し替えられるたびに実施
  var _replaceIssueFormWith = replaceIssueFormWith;
  replaceIssueFormWith = function (html) {
    _replaceIssueFormWith(html);
    changeCustomFieldPosition();
  };
});

// タスク詳細のリプライボタンを追加
document.addEventListener('DOMContentLoaded', function () {
  const beforeAssignedTo = document.getElementById('issue_assigned_to_id').value;

  document.querySelectorAll('#content > .contextual a.icon-edit').forEach(function (element) {
    const edit = element;
    const reply = document.createElement('a');
    reply.classList.add('icon', 'icon-reply');
    reply.textContent = 'コメントする';
    reply.setAttribute('href', edit.getAttribute('href'));
    edit.parentNode.insertBefore(reply, edit);

    reply.addEventListener('click', function () {
      let replyTo = ViewCustomize.context.issue.lastUpdatedBy
        ? ViewCustomize.context.issue.lastUpdatedBy.id
        : ViewCustomize.context.issue.author.id;

      document.getElementById('issue_assigned_to_id').value = replyTo;

      showAndScrollTo('update', 'issue_notes');
      return false;
    });

    edit.addEventListener('click', function () {
      document.getElementById('issue_assigned_to_id').value = beforeAssignedTo;
    });
  });
});

// トップバーとサイドバーをホバーしたときに表示する
function handleContentSidebarHover() {
  var sidebar = document.getElementById("sidebar");
  sidebar.addEventListener("mouseenter", function () {
    sidebar.style.width = "";

    var sidebarContent = document.querySelectorAll("#sidebar h3, #sidebar ul");
    sidebarContent.forEach(function (contentElement) {
      contentElement.style.display = "block";
      contentElement.style.opacity = "1";
      contentElement.style.transition = "opacity 0.2s";
    });

    var sidebarScripts = document.querySelectorAll("#sidebar script");
    sidebarScripts.forEach(function (scriptElement) {
      scriptElement.style.display = "none";
    });
  });

  var content = document.getElementById("content");
  content.addEventListener("mouseenter", function () {
    sidebar.style.width = "30px";
    sidebar.style.transition = "width 0.5s";

    var sidebarContent = document.querySelectorAll("#sidebar h3, #sidebar ul");
    sidebarContent.forEach(function (contentElement) {
      contentElement.style.transition = "opacity 0.2s";
      contentElement.style.opacity = "0";
    });
  });
}

// Execute the function when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", handleContentSidebarHover);

// ------------ 活動の曜日と折りたたみ ------------------
document.addEventListener('DOMContentLoaded', function () {
  var activityHeaders = document.querySelectorAll("#activity > h3");

  activityHeaders.forEach(function (header) {
    var dateString = header.textContent;
    var date = new Date(dateString);
    var daysOfWeek = ["日", "月", "火", "水", "木", "金", "土"];
    var dayOfWeek = daysOfWeek[date.getDay()];

    // 土曜日と日曜日の場合だけ文字の色を赤に変更
    var textColor = (dayOfWeek === "土" || dayOfWeek === "日") ? "red" : "";

    // テキストに曜日を追加し、スタイルを適用
    header.innerHTML =
      dateString +
      "(" +
      '<span style="color:' +
      textColor +
      ';">' +
      dayOfWeek +
      "</span>" +
      ")";

    // 非表示ボタンの追加
    header.innerHTML += "  <span class='close icon expander icon-expended'>非表示にする</span>";
  });

  // 表示、非表示ボタンの機能の実装
  document.addEventListener('click', function (event) {
    var target = event.target;
    if (target.classList.contains('close') || target.classList.contains('open')) {
      var isCloseButton = target.classList.contains('close');
      var buttonText = isCloseButton ? "表示する" : "非表示にする";
      var newClass = isCloseButton ? "open" : "close";
      var oldClass = isCloseButton ? "close" : "open";
      var newIcon = isCloseButton ? "icon-collapsed" : "icon-expended";
      var oldIcon = isCloseButton ? "icon-expended" : "icon-collapsed";

      target.textContent = buttonText;
      target.classList.remove(oldClass);
      target.classList.add(newClass);
      target.classList.remove(oldIcon);
      target.classList.add(newIcon);

      var dl = target.parentElement.nextElementSibling;
      if (dl) {
        dl.style.display = isCloseButton ? 'none' : 'block';
      }
    }
  });

  // カーソルのスタイル設定
  var expanders = document.querySelectorAll(".expander");
  expanders.forEach(function (expander) {
    expander.style.cursor = "pointer";
  });

  // 今日の日付と曜日を取得
  var today = new Date();
  var todayString = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate();
  var todayDayOfWeek = daysOfWeek[today.getDay()];

  // 「今日」の部分に日付と曜日を表示
  var todayElement = document.querySelector("#activity > h3:contains('今日')");
  if (todayElement) {
    todayElement.innerHTML =
      todayString +
      "(" +
      '<span style="color:red;">' +
      todayDayOfWeek +
      "</span>" +
      ")";
  }
});

CSS 内容

フォントサイズは各自で見やすいように変更してください。
推しポイントは、-webkit-font-smoothing: antialiased;を使用することによって Mac 上でフォントが見やすくなっています。

#header {
  font-size: 14px;
  font-weight: bolder;
  font-family: Meiryo, "Inconsolata";
  -webkit-font-smoothing: antialiased;
}

#main-menu {
  font-size: 18px;
  font-weight: bold;
  -webkit-font-smoothing: antialiased;
}

#content {
  font-size: 14px;
  font-family: Meiryo, "Inconsolata";
  -webkit-font-smoothing: antialiased;
}

.selected {
  font-weight: 900;
}

実装内容

以下に、今回の実装で反映された項目を書いていきます。

活動を見やすく

日付に紐づく曜日の表示と、日毎に折りたためる機能を実装しました。

折りたたみ機能付き

サイドバーをホバーで表示 / 非表示

サイドバーが邪魔だったので、ホバー時のみ表示するようにしています。

非ホバー時
ホバー時

プロジェクトごとにヘッダーの色を変更

プロジェクトごとにヘッダーの色を変更することで、今現在参照しているプロジェクトが何なのか一目で分かるようにしました。

これはかなり開発者体験上がったので、おすすめです。

ProjectA
ProjectB

コメントするボタンの追加

実は「編集」ボタンと役割はそう変わらないです。

Redmine ではコメント機能が「編集」に含まれているため、わかりやすくする変更になります。

「編集」ボタンの左横に生やした

以上です!

おわりに

今日では、ネット上にたくさんのカスタマイズ方法が眠っています。
例に挙げた Redmine 以外でもカスタマイズし、自分なりに使いやすくしてみてください。

参考

GitHub - farend/redmine-view-customize-examples: Redmineのプラグイン「View customize」を利用したRedmineカスタマイズ集
Redmineのプラグイン「View customize」を利用したRedmineカスタマイズ集. Contribute to farend/redmine-view-customize-examples development by creating an account on GitHub.