Skip to content

一覧画面のカスタマイズ ​

第6版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

一覧画面(およびサイト一覧)を見やすく・使いやすくするカスタマイズをまとめます。いずれも本体を改修せず、サーバースクリプト・スクリプト・拡張機能(拡張スクリプト、拡張スタイル、拡張フィールド、拡張 SQL)だけで実装できます。

カスタマイズ使う機能実行タイミング
ヘッダのフィルタ状態を表示するサーバースクリプト画面表示の前
ソート状態のアイコンを見やすくするサーバースクリプト(+ 拡張 HTML)画面表示の前
ソートの順番を表示するサーバースクリプト画面表示の前
選択件数カウンターを表示する拡張スクリプト―
一覧を一定間隔で自動更新する拡張フィールド + 拡張スクリプト―
数値項目にデータバーを表示するサーバースクリプト行表示の前
セルに斜線を引くスタイル / 拡張スタイル + サーバースクリプト行表示の前
サイト一覧にテーブルのロック状態を表示する拡張 SQL + 拡張スクリプト―

ヘッダのフィルタ状態を表示する ​

フィルタの「一覧のヘッダメニューでフィルタを使用する」を ON にすると、ヘッダから直接フィルタをかけられます。ただし、フィルタ欄に表示されていない項目にフィルタをかけると、その項目にフィルタがかかっているかどうかが画面から分かりません。フィルタがかかっている項目のヘッダに filter_alt アイコンを表示して、これを解消します。

フィルタがかかっている「作業内容」のヘッダに filter_alt アイコンが付いた一覧

仕組み ​

  • ヘッダ要素(th)の HTML は、ヘッダメニューでのフィルタが ON でも OFF でも同じで、フィルタ状態を示す属性はありません。
  • サーバースクリプトの view オブジェクトはビューの設定だけでなく、現在のビュー情報の取得にも使えます。view.Filters にはフィルタがかかっている項目が入ります。
js
context.Log($ps.JSON.stringify(view.Filters));
// ClassA にだけフィルタをかけた場合の出力
// {"ClassA":"[\"3528414\"]"}
  • アイコンはプリザンターに組み込まれている Google Icons(material-symbols-outlined)を使い、th[data-name="項目名"] div の先頭に追加します。

設定 ​

サーバースクリプト(または拡張サーバースクリプト)に、条件「画面表示の前」で次のコードを設定します。

js
if (['index', 'gridrows', 'newongrid', 'copyrow'].includes(context.Action)) 
{
 for (const [key, value] of Object.entries(view.Filters)) {
        if (value && value !== '[]') {
      context.AddResponse('Prepend', `th[data-name="${key}"] div:not(:has(.view-filter-icon))`,
       '<span class="view-filter-icon material-symbols-outlined" style="font-size: 1.5em;">filter_alt</span>');
        }
 }
}
  • context.Action を index(画面遷移)・gridrows(一覧の表の書き換え)・newongrid / copyrow(一覧画面編集での行追加・行コピー)に限定しています。
  • フィルタをクリアしたとき、選択肢項目や日付項目には空であることを示す空配列 [] が渡されるため、これを除外しています。
  • :not(:has(.view-filter-icon)) で、アイコンが二重に追加されないようにしています。

ソート状態のアイコンを見やすくする ​

ヘッダをクリックしてソートしたときに表示される昇順・降順のアイコンは小さく見にくいので、大きなアイコンに差し替えます。

仕組み ​

  • ソート中の項目では、ヘッダのタイトルの隣に ui-icon ui-icon-triangle-1-n(昇順)/ ui-icon-triangle-1-s(降順)の span が入ります。
  • data-order-type 属性には「次にクリックしたときのソート順」が入っているため、要素から現在の状態を判断するのは困難です。アイコンのクラスで判定する方法も、将来のデザイン変更で変わる可能性があるため避けます。
  • 代わりに、サーバースクリプトの view.Sorters で現在のソート状態を取得します。
js
context.Log($ps.JSON.stringify(view.Sorters));
// TitleBody を昇順、ClassA を降順にした場合の出力
// {"TitleBody":"asc","ClassA":"desc"}

設定(Google Icons を使う場合) ​

サーバースクリプト(または拡張サーバースクリプト)に、条件「画面表示の前」で次のコードを設定します。昇順に stat_3、降順に stat_minus_3 を使います。

js
if (['index', 'gridrows', 'newongrid', 'copyrow'].includes(context.Action)) {
 for (const [key, value] of Object.entries(view.Sorters)) {
  switch (value) {
   case "asc":
    context.AddResponse('ReplaceAll', `th[data-name="${key}"] div .ui-icon`,
     '<span class="material-symbols-outlined">stat_3</span>');
    break;
   case "desc":
    context.AddResponse('ReplaceAll', `th[data-name="${key}"] div .ui-icon`,
     '<span class="material-symbols-outlined">stat_minus_3</span>');
    break;
  }
 }
}

context.Action の限定はヘッダのフィルタ状態を表示すると同じ理由です。

降順ソート中の「開始」ヘッダに stat_minus_3 アイコンが表示された一覧

設定(Line Awesome を使う場合) ​

より直感的なアイコンとして、ライセンスの緩い Line Awesome の sort-alpha-down-solid(昇順)/ sort-alpha-down-alt-solid(降順)を使うこともできます。CDN を使うとインターネット接続が必要になるため、リソースをプリザンターに組み込みます。

  1. Line Awesome の公式サイトから ZIP ファイルをダウンロードし、プリザンターのインストールディレクトリの wwwroot/line-awesome/ を作成して、解凍した svg・css・fonts ディレクトリを中身ごと配置します。

    text
    [プリザンターのインストールディレクトリ]
    └ wwwroot
      └ line-awesome   ← 作成する
        ├ svg          ← 解凍したディレクトリ(全ファイル)
        ├ css          ← 同上
        └ fonts        ← 同上
  2. 拡張 HTML を用意します。ファイル名の言語サフィックス(2 レターコード)を省略すると、全言語に適用されます。

    html
    <link rel="stylesheet" href="/line-awesome/css/line-awesome.min.css" />
  3. サーバースクリプトに、条件「画面表示の前」で次のコードを設定します。

    js
    if (['index', 'gridrows', 'newongrid', 'copyrow'].includes(context.Action)) {
     for (const [key, value] of Object.entries(view.Sorters)) {
      switch (value) {
       case "asc":
        context.AddResponse('ReplaceAll', `th[data-name="${key}"] div .ui-icon`,
         '<span class="las la-sort-alpha-down" style="font-size: 1.5em;"></span>');
        break;
       case "desc":
        context.AddResponse('ReplaceAll', `th[data-name="${key}"] div .ui-icon`,
         '<span class="las la-sort-alpha-down-alt" style="font-size: 1.5em;"></span>');
        break;
      }
     }
    }

ソートの順番を表示する ​

複数の項目でソートした場合、ソートはヘッダをクリックした順に適用されますが、その順番は画面に表示されません。ソートアイコンの横に Google Icons の filter_1 〜 filter_9、10 番目以降は filter_9_plus を表示します。

作業工程(昇順)・完了(降順)の順にソートし、アイコンの横に順番 1・2 が表示された一覧

サーバースクリプトに、条件「画面表示の前」で次のコードを設定します。ソートアイコンには前項の Line Awesome を使っています。

js
if (['index', 'gridrows', 'newongrid', 'copyrow'].includes(context.Action)) {
 //環境によっては GetType などのメソッドも列挙されるため、値が asc / desc のものだけに絞る
 const sorters = Object.entries(view.Sorters)
  .filter(([key, value]) => value === 'asc' || value === 'desc');
 for (const [index, [key, value]] of sorters.entries()) {
  var number = index + 1;
  var icons = "";

  switch (value) {
   case "asc":
    icons +=
     `<span class="las la-sort-alpha-down" style="font-size: 1.5em;"></span>`;
    break;
   case "desc":
    icons +=
     `<span class="las la-sort-alpha-down-alt" style="font-size: 1.5em;"></span>`;
    break;
  }

  if (9 < number) {
   icons +=
    `<span class="material-symbols-outlined" style="font-size: 1.5em;">filter_9_plus</span>`;
  } else {
   icons +=
    `<span class="material-symbols-outlined" style="font-size: 1.5em;">filter_${number}</span>`;
  }

  context.AddResponse('ReplaceAll', `th[data-name="${key}"] div .ui-icon`,
   icons);
 }
}

WARNING

環境によっては、view.Sorters に対して Object.entries を使うと先頭に GetType・ToString・Equals・GetHashCode の 4 つが格納されることがあります。view.Sorters の実体は .NET の ExpandoObject で(ServerScriptModelView.cs#L20)、メソッドが列挙されるかどうかはスクリプトエンジン側の扱いによります。「先頭 4 件を読み飛ばす」処理にすると、メソッドが列挙されない環境では最初の 4 項目のソートが表示されません。上のコードでは値が asc / desc のものだけに絞り込み、どちらの環境でも動くようにしています。

選択件数カウンターを表示する ​

一覧画面でチェックボックスを選択すると、「○件選択中」というメッセージを画面上部のメッセージ領域にリアルタイムに表示します。拡張スクリプトだけで実装でき、拡張スタイルは不要です。

一覧で 3 行を選択し、画面下部に「3件選択中」と表示された様子

INFO

バージョン 1.5.1.0 以降を対象にしています。

仕組み ​

要素HTML参照
ヘッダの全選択チェックボックスinput#GridCheckAllHtmlGrids.cs#L103
各行のチェックボックスinput.grid-check(data-id にレコード ID)HtmlGrids.cs#L412

組み込みスクリプト(gridevents.js)でのイベントの発生は次のとおりです。

操作発生するイベント備考
個別チェック.grid-check の changeチェックボックスを直接クリックした場合
行クリック.grid-check の changeセルをクリックするとチェックが切り替わり change が発火する
全選択チェック#GridCheckAll の click.grid-check の change は 発火しない

WARNING

全選択チェックボックスをクリックした場合、各行の .grid-check は prop('checked', ...) で直接状態が設定されるため、change イベントは発火しません。

設定 ​

拡張スクリプトとして次のファイルを配置します。

js
(function () {
  /**
   * チェック済みの件数を数えてメッセージの表示を更新する。
   */
  function updateCounter() {
    var count = document.querySelectorAll('.grid-check:checked').length;
    // 前回表示したカウンターのメッセージだけを消す
    $('#Message .selection-counter').remove();
    if (count > 0) {
      $p.clearMessage();
      $p.setMessage('#Message', JSON.stringify({
        Css: 'alert-information',
        Text: count + '件選択中'
      }));
      // 追加したメッセージに目印のクラスを付ける
      $('#Message > div').last().addClass('selection-counter');
    }
  }

  // 個別チェックボックスの変更
  $(document).on('change', '.grid-check', updateCounter);

  // 全選択チェックボックスのクリック
  // (組み込みの click ハンドラで .grid-check の状態が更新された後にカウントするため遅延)
  $(document).on('click', '#GridCheckAll', function () {
    setTimeout(updateCounter, 0);
  });

  // Ajax によるグリッド再描画(ソート・フィルタ・ページ移動など)
  // (DOM の更新完了後にカウントするため遅延)
  $(document).ajaxComplete(function () {
    setTimeout(updateCounter, 0);
  });

  // 初期表示
  updateCounter();
})();

ポイントと注意点 ​

  • $p.setMessage は第 1 引数に表示先のセレクタ、第 2 引数に Css と Text を含む JSON 文字列を渡します。Css に指定できるクラスは次のとおりです。

    CSS クラス用途色
    alert-success成功通知緑
    alert-information情報メッセージ青
    alert-warning警告黄
    alert-errorエラー赤
  • 全選択は #GridCheckAll の click を監視し、setTimeout(updateCounter, 0) で組み込みのハンドラが状態を更新した後にカウントします。

  • ソート・フィルタ・ページ移動などの Ajax 再描画ではチェック状態がリセットされるため、ajaxComplete でも更新しています。

  • 選択中の ID を返す $p.selectedIds() は内部で同期的な Ajax 通信を行うため(grid.js#L55)、チェックのたびに呼ぶとサーバーに不要な負荷がかかります。ここでは DOM のチェックボックスを直接数えています。

  • 選択が 0 件のときは、カウンターが表示したメッセージ(目印の selection-counter クラスを付けたもの)だけを消します。$p.clearMessage() は class に message を含む要素をすべて空にするため(message.js#L50-L52)、0 件のときにも呼ぶと、ajaxComplete のたびに一括削除などの結果メッセージまで消えてしまいます。

WARNING

1 件以上選択している間は $p.clearMessage() でメッセージ領域を空にしてから表示するため、保存直後の「更新しました」などの組み込みメッセージも、チェックボックス操作時に置き換わります。

一覧を一定間隔で自動更新する ​

一覧画面を一定間隔で、画面全体の再遷移ではなく Ajax で更新します。拡張フィールドで「自動更新の ON/OFF」と「更新間隔(秒)」をフィルタ領域に持たせ、拡張スクリプトでタイマー実行します。

  • 更新は $p.send() で一覧の GridRows を再取得します。プリザンター標準の Ajax 経路を通るため、既存のローディング表示がそのまま使えます。
  • 更新中はメッセージ領域に「一覧を更新中...」を表示します。

拡張フィールドの準備 ​

App_Data/Parameters/ExtendedFields/ に次の 2 ファイルを配置します(名前は例です)。"FieldType": "Filter" を指定すると、一覧画面のフィルタ領域(#ViewFilters)に表示されます。フィルタ領域のコントロールの ID は ViewFilters__ + 項目名(例: ViewFilters__CheckRefreshTimer)になります(HtmlViewFilters.cs#L369)。

json
{
  "Name": "CheckRefreshTimer",
  "FieldType": "Filter",
  "TypeName": "bit",
  "LabelText": "自動更新ON/OFF"
}
json
{
  "Name": "NumRefreshSpan",
  "FieldType": "Filter",
  "TypeName": "nvarchar",
  "LabelText": "更新間隔(秒)"
}

フィルタ領域に追加された「自動更新ON/OFF」と「更新間隔(秒)」

INFO

更新間隔は TypeName を nvarchar(文字列)にしてテキストボックスで入力させます。フィルタ領域では数値型(decimal など)の項目が範囲選択のドロップダウンになり(HtmlViewFilters.cs#L432-L453)、秒数を直接入力できないためです。また、フィルタ領域の入力欄にはビューに保存された値が表示され DefaultInput は使われないため、未入力のときはスクリプトの DEFAULT_SEC(60 秒)を使います。

設定ファイルを追加・変更したら、特権ユーザー(Security.json の PrivilegedUsers に登録したユーザー)でログインした状態で次の URL にアクセスして反映します(ベースパスの有無は環境によります)。

  • ベースパスなし: https://{ドメイン}/admins/reloadparameters
  • ベースパスあり: https://{ドメイン}/{ベースパス}/admins/reloadparameters

WARNING

未ログイン状態や特権ユーザー以外のユーザーでアクセスしても反映されません(ParametersInitializer.cs#L9)。テナント管理者の権限だけでは反映されません。

拡張フィールドはフィルタ領域の先頭に追加されます。既存の項目の後ろに置きたい場合は、拡張フィールドの After に項目名を指定します(SiteSettings.cs#L2665-L2686)。

拡張スクリプト ​

App_Data/Parameters/ExtendedScripts/ に次のファイルを配置します。

js
(function () {
  'use strict';

  // ===== 設定 =====
  var MIN_SEC = 10;
  var MAX_SEC = 3600;
  var DEFAULT_SEC = 60;
  var CHECK_SELECTOR = '#ViewFilters__CheckRefreshTimer'; // 自動更新ON/OFF(拡張フィールド)
  var INTERVAL_SELECTOR = '#ViewFilters__NumRefreshSpan'; // 更新間隔秒(拡張フィールド)
  var MESSAGE_TARGET = '#Message';
  var GRID_TRIGGER_SELECTOR = '[data-action="GridRows"][data-method="post"]';

  // 一覧画面以外では何もしない
  if (!$('#Grid').length) return;

  var timerId = null;
  var busy = false;

  // 既存 Ajax と干渉しないための簡易排他
  $(document)
    .off('.autoReloadBusy')
    .on('ajaxStart.autoReloadBusy', function () {
      busy = true;
    })
    .on('ajaxStop.autoReloadBusy', function () {
      busy = false;
    });

  function toInt(value, fallback) {
    var n = parseInt(value, 10);
    return Number.isFinite(n) ? n : fallback;
  }

  function getEnabled() {
    var $check = $(CHECK_SELECTOR);
    if (!$check.length) return false;
    return $check.prop('checked') || $check.val() === '1' || $check.val() === 'true';
  }

  function getIntervalSec() {
    var $interval = $(INTERVAL_SELECTOR);
    var sec = toInt($interval.val(), DEFAULT_SEC);
    if (sec < MIN_SEC) sec = MIN_SEC;
    if (sec > MAX_SEC) sec = MAX_SEC;
    return sec;
  }

  function setInfo(text) {
    if (!$p || typeof $p.setMessage !== 'function') return;
    $p.clearMessage();
    $p.setMessage(
      MESSAGE_TARGET,
      JSON.stringify({
        Css: 'alert-information',
        Text: text
      })
    );
  }

  function reloadGridByAjax() {
    if (busy) return;
    if (!getEnabled()) return;
    // タブが非表示の間は不要な負荷を避けるため更新しない
    if (document.hidden) return;

    var $trigger = $(GRID_TRIGGER_SELECTOR).first();
    if (!$trigger.length) {
      setInfo('自動更新対象(GridRows)が見つからないため更新をスキップしました');
      return;
    }

    setInfo('一覧を更新中...');

    // プリザンター標準の Ajax 送信を使う
    // 既存のローディング表示がそのまま表示される
    $p.send($trigger);
  }

  function restartTimer() {
    if (timerId) {
      clearInterval(timerId);
      timerId = null;
    }
    if (!getEnabled()) return;

    var intervalMs = getIntervalSec() * 1000;
    timerId = setInterval(reloadGridByAjax, intervalMs);
  }

  // 初期化
  restartTimer();

  // 設定変更時にタイマーを再起動
  $(document)
    .off('change.autoReload', CHECK_SELECTOR)
    .off('change.autoReload', INTERVAL_SELECTOR)
    .on('change.autoReload', CHECK_SELECTOR + ',' + INTERVAL_SELECTOR, function () {
      restartTimer();
    });

  // タブ復帰時に取りこぼし防止
  $(document)
    .off('visibilitychange.autoReload')
    .on('visibilitychange.autoReload', function () {
      if (!document.hidden) {
        reloadGridByAjax();
      }
    });
})();

設定のポイント ​

  • ExtendedScripts フォルダに置いた .js ファイルは、条件なしですべての画面で読み込まれます(Initializer.cs#L725-L756)。そのためスクリプトの冒頭で、#Grid が無い画面では何もせずに終了しています。特定のテーブルに限定したい場合は、拡張機能テーブル(Extensions)に ExtensionType を Script として登録し、ExtensionSettings に SiteIdList などの条件、Body にスクリプトを入れます(ExtensionInitializer.cs#L83-L95)。
  • CheckRefreshTimer が OFF のときはタイマーを停止します。
  • 過負荷を防ぐため、間隔は 10 秒未満にしません(スクリプトでも 10〜3600 秒に丸めています)。まずは 30〜60 秒程度から始め、利用者数や件数に合わせて調整します。
  • タブが非表示の間は更新せず、タブに戻ったときに 1 回更新します。
  • 拡張フィールドの物理名が異なる場合は、CHECK_SELECTOR / INTERVAL_SELECTOR の ViewFilters__ の後ろを読み替えます。フィルタ領域のコントロールの ID には ViewFilters__ が付くため、#CheckRefreshTimer のように項目名だけを ID に書くと動きません。

WARNING

「自動更新対象(GridRows)が見つからない」と表示される場合は、次を確認してください。

  • 対象画面が一覧画面か(#Grid がある画面か)
  • GridRows の送信トリガー要素がカスタマイズで変更されていないか
  • CHECK_SELECTOR / INTERVAL_SELECTOR が実環境の物理名に合っているか

数値項目にデータバーを表示する ​

数値項目のセルに、Excel のデータバーのような横棒グラフを表示します。RawText を使ってセルに SVG を直接書き出します。

サーバースクリプトに、条件「行表示の前」で次のコードを設定します(NumD に百分率の値が入っている例です)。

js
columns.NumD.RawText = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="33">
   <text x="0" y="13">${model.NumD}%</text>
   <rect x="0" y="20" width="${100 - model.NumD}" height="10" fill="none" stroke="#000"></rect>
   <rect x="${100 - model.NumD}" y="20" width="${model.NumD}" height="10" fill="#f00" stroke="#000"></rect>
</svg>`;

稼働率(NumD)のセルに百分率のデータバーを表示した一覧

四角形を左右に 2 つ描画することで、百分率であることが分かりやすくなります。同じ方法で、円グラフや簡単な折れ線グラフなども描画できます。

セルに斜線を引く ​

サーバースクリプトや自動ポストバックで編集画面の項目を非表示にしていても、一覧画面ではセルがそのまま表示されるため、「未入力」なのか「入力不要(非表示)」なのかを区別できません。入力不要のセルに Excel のような斜線を表示します。

スタイルの用意 ​

次の CSS を用意します。

css
.cell-slash {
    background-image: linear-gradient(to top right, transparent calc(50% - 1px), rgb(34, 34, 34), transparent calc(50% + 1px));
}

置き場所は、対象のセルがリンクなどで他のサイトにも表示されるかどうかで決めます。

置き場所特徴
サイトのスタイル他のサイトからは呼び出せない。リンク先のサイトにも表示する場合は、リンク先のサイトのスタイルにも同じものを用意する
拡張スタイルプリザンター全体で使い回せる。ただし Pleasanter.net 環境などではセキュリティの観点から使えない

このクラスを項目のセル CSS に設定すると、斜線が表示されます。

入れ子の要素を消す ​

説明項目や添付ファイル項目などは表示の都合でセル内にタグが入れ子になっており、その要素に背景色が設定されていると斜線が隠れてしまいます。サーバースクリプトで RawText を半角スペース 1 文字にして入れ子の要素を消し、ExtendedCellCss でクラスを付けます。

js
//条件は行表示の前
columns.DescriptionA.RawText = ' ';
columns.DescriptionA.ExtendedCellCss = 'cell-slash';

RawText は空文字('')だと未設定と同じ扱いになり、通常のセルがそのまま出力されます(IssueUtilities.cs#L795)。そのため、空文字ではなく半角スペースを設定しています。

「回答」(DescriptionA)のセルに斜線を表示した一覧

サイト一覧にテーブルのロック状態を表示する ​

テーブルのロック中のテーブルは、開くと画面上部に赤帯で「読取専用です。」と表示されますが、サイト一覧(フォルダビュー)ではどれがロック中か分かりません。拡張 SQL と拡張スクリプトを組み合わせて、サイト一覧でロック中のテーブルのパネルを赤く表示します。

ロック判定の仕組み ​

  • テーブルの管理 > エディタで「テーブルのロックを許可」をオンにすると、管理メニューに「テーブルをロック」が表示されます。ロック・ロック解除は、ロックを実行したユーザーまたは特権管理者だけが操作できます。
  • ロック情報は Sites テーブルの LockedTime 列(ロック日時)と LockedUser 列(ロックしたユーザー ID)に保持されます。
  • 本体のロック判定は LockedTime IS NOT NULL ではなく、SiteSettings.LockedTable() で「ロック日時が有効な範囲内で、かつロックユーザーが匿名ユーザーでない」ことを見ています(SiteSettings.cs)。
csharp
public bool LockedTable()
{
    return LockedTableTime?.Value.InRange() == true
        && LockedTableUser?.Anonymous() == false;
}
  • ロック解除時、LockedTime は現在時刻に更新されたまま残り、LockedUser だけが null になります。
  • LockedUser は現在テナントのユーザーキャッシュから User に解決され、存在しないユーザー ID は匿名ユーザー扱いになります。ユーザーキャッシュは無効ユーザーも含むため、Users.Disabled での除外は不要です。
  • サイト一覧のパネルを作る SiteUtilities.Menu() は、Sites から SiteId・Title・ReferenceType・SiteSettings だけを取り、LockedTime・LockedUser は取っていません(SiteUtilities.cs)。画面の HTML にロック状態が含まれないので、ここでは拡張 SQL で別に取得します。本体側でパネルに locked クラスを付ける改修案は サイトメニューにテーブルのロック状態を出す(本体改修) にあります。

このため、SQL では次の 3 点を条件にして、本体の判定に近づけます。

  1. LockedTime が本体の有効日時範囲(1900-01-01〜2100-01-01)内にあること
  2. Users テーブルに該当ユーザーが存在すること
  3. LockedUser が匿名ユーザー ID(2)でないこと(旧環境では Users に Anonymous ユーザーが残っていることがあるため)

WARNING

この SQL は標準設定を前提にした実用上の近似で、本体の LockedTable() と完全に一致する保証はありません。日時範囲は標準の General.json の既定値、匿名ユーザー ID 2 は標準実装に基づく値です。これらを変更している環境では条件を見直してください。完全一致が必要な場合は、サーバー側のカスタマイズで LockedTable() 相当の処理を実行する方法を検討してください。

処理の流れ ​

図を読み込み中…

拡張 SQL ​

App_Data/Parameters/ExtendedSqls/ に定義ファイルと SQL ファイルを配置します。ブラウザから /api/extended/sql 経由で呼び出すため、"Api": true が必要です。

json
{
    "Name": "GetLockedSites",
    "Api": true,
    "CommandText": "-- Write an arbitrary SQL statement."
}
sql
SELECT
    S.[SiteId]
FROM
    [Sites] AS S
WHERE
    S.[TenantId] = @_T
    AND S.[ParentId] = @SiteId
    AND S.[LockedTime] >= '1900-01-01'
    AND S.[LockedTime] <= '2100-01-01'
    AND S.[LockedUser] <> 2
    AND EXISTS (
        SELECT 1
        FROM [Users] AS U
        WHERE U.[TenantId] = S.[TenantId]
            AND U.[UserId] = S.[LockedUser]
    )
sql
SELECT
    S."SiteId"
FROM
    "Sites" AS S
WHERE
    S."TenantId" = @ipT
    AND S."ParentId" = @SiteId
    AND S."LockedTime" >= TIMESTAMP '1900-01-01 00:00:00'
    AND S."LockedTime" <= TIMESTAMP '2100-01-01 00:00:00'
    AND S."LockedUser" <> 2
    AND EXISTS (
        SELECT 1
        FROM "Users" AS U
        WHERE U."TenantId" = S."TenantId"
            AND U."UserId" = S."LockedUser"
    )
sql
SELECT
    S.`SiteId`
FROM
    `Sites` AS S
WHERE
    S.`TenantId` = @ipT
    AND S.`ParentId` = @SiteId
    AND S.`LockedTime` >= '1900-01-01 00:00:00'
    AND S.`LockedTime` <= '2100-01-01 00:00:00'
    AND S.`LockedUser` <> 2
    AND EXISTS (
        SELECT 1
        FROM `Users` AS U
        WHERE U.`TenantId` = S.`TenantId`
            AND U.`UserId` = S.`LockedUser`
    )

SQL ファイル名はいずれも GetLockedSites.json.sql です。@_T(PostgreSQL・MySQL では @ipT)は拡張 SQL の組み込みパラメータで現在のテナント ID が入り、@SiteId は拡張スクリプトから渡すパラメータ(現在開いているサイトの ID)です。これで表示中の親サイト配下の子サイトだけに絞ります。

PostgreSQL・MySQL では @_T ではなく @ipT

組み込みパラメータの接頭辞は DBMS で変わり、SQL Server は @_、PostgreSQL・MySQL は @ip です(Parameter.json の SqlParameterPrefix が空の既定のとき。Parameter.cs、SqlIo.cs)。PostgreSQL で @_T と書くと column "_t" does not exist のエラーになり、結果が返りません。

INFO

絞り込めるのは「現在開いているサイト配下の子サイト」までです。画面上で検索やフィルタをかけた結果と完全に一致させたい場合は、クライアント側で表示中の ID を取得して別途絞り込む必要があります。

拡張スクリプト ​

App_Data/Parameters/ExtendedScripts/ に次のファイルを配置します。拡張スクリプトは JSON 定義ファイルなしで .js ファイル 1 つで動作します。

js
$(function () {
    if ($('.nav-site').length === 0) return;
    $p.apiExec(
        $('#ApplicationPath').val() + 'api/extended/sql',
        {
            data: {
                Name: 'GetLockedSites',
                Params: { SiteId: $p.siteId() }
            },
            done: function (res) {
                if (!res || !res.Response || !res.Response.Data) return;
                var tables = res.Response.Data;
                var tableKey = Object.keys(tables)[0];
                if (!tableKey) return;
                var rows = tables[tableKey];
                if (!rows || rows.length === 0) return;
                for (var i = 0; i < rows.length; i++) {
                    var siteId = rows[i].SiteId || rows[i].siteId;
                    if (!siteId) continue;
                    $('.nav-site[data-value="' + siteId + '"]')
                        .css({
                            'background': '#ffd0d0',
                            'box-shadow': 'inset 0 0 0 3px #e53935'
                        });
                }
            }
        }
    );
});
  • 拡張スクリプトはすべての画面で読み込まれるため、.nav-site が無い画面では即座に終了し、不要な API 呼び出しを防ぎます。
  • エンドポイントは $('#ApplicationPath').val() で取得したベースパスに api/extended/sql を連結します。認証トークンは $p.apiExec が自動で付与します。
  • $p.siteId() は現在開いているサイトの ID を整数で返し、SQL の @SiteId として渡ります。
  • レスポンスは次の構造で、Response.Data の最初のキー(Table)が結果セットです。カラム名は SQL の SELECT で指定した名前のままです。
json
{
    "StatusCode": 200,
    "Response": {
        "Data": {
            "Table": [
                { "SiteId": 123 },
                { "SiteId": 456 }
            ]
        }
    }
}

配置ファイルまとめ ​

ファイル配置先
GetLockedSites.jsonApp_Data/Parameters/ExtendedSqls/
GetLockedSites.json.sqlApp_Data/Parameters/ExtendedSqls/
LockedSiteIndicator.jsApp_Data/Parameters/ExtendedScripts/

ファイルを配置したら、プリザンターを再起動するか、一覧を一定間隔で自動更新すると同じ /admins/reloadparameters で拡張機能を読み込みます(拡張 SQL・拡張スクリプトもこの URL で再読み込みされます。Initializer.cs#L124-L126)。

サイト一覧でロック中のテーブル(議事録)のパネルが赤く表示された様子

関連ページ ​

変更履歴

第6版一覧画面のレシピにスクリーンショットを追加し、ロック状態表示の SQL を PostgreSQL・MySQL で動くよう修正
第5版リンク項目の列指定と JOIN の組み立て、一覧のスクロール読み込みの解説と、一覧・カレンダー・サイトメニューまわりの改修・設計メモを追加
第4版本文から元記事や以前の版への言及を除き、正しい動作だけを書く形に整理
第3版画面カスタマイズ集のコードを 1.5.8.1 のソースで検証し、動かなかったサンプルを修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「画面カスタマイズ集」セクションの記事を追加