複雑な条件での検索を可能にするカスタマイズビュー

標準機能の絞り込みURL内のクエリだと
「(条件1 AND 条件2)OR 条件3」のような検索はできません

このような検索をするには、複数のレコードを取得する
queryパラメーターに条件を指定して、取得したレコードを
カスタマイズビューに表示することになります。

しかし、カスタマイズービューは作り込まないと
このように全く装飾されていない表になり

  • インライン編集
  • 一覧画面からのレコード削除
  • フィールド名をクリックした列でソート
  • ページネーション
  • 1ページあたりのレコード表示件数設定
  • 絞り込み結果のレコード件数表示

といった標準機能が使えなくなります。

そこで、これらの機能を持ち、CSSで見た目も整えられていて
複雑な条件での検索もできるビューのサンプルを作りました。

おまけで標準機能ではできない漢字仮名1字の検索に対応した
文字を打ち込むだけで結果が出る検索ボックスも付けています。

「いいね!」 3

HTML

一覧の設定画面でカスタマイズを選択した後で入力するHTML。
なお「ページネーションを表示する」のチェックは外します。

<div id="record-container"></div>

CSS

#record-container {
  padding: 10px;
  background-color: #ffffff;
  border: 1px solid #e0e0e0;
  border-radius: 3px;
  margin: 10px 0;
}

#record-container table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

#record-container th,
#record-container td {
  padding: 10px 15px;
  text-align: left;
  border-bottom: 1px solid #e0e0e0;
  font-size: 14px;
}

#record-container thead th {
  background-color: #f5f5f5;
  font-weight: bold;
  border-top: 2px solid #999999;
}

#record-container tbody tr:last-child td {
  border-bottom: 2px solid #999999;
}

#record-container tr:hover {
  background-color: #f9f9f9;
}

#record-container td {
  background-color: #fff;
}

#record-container td,
#record-container th {
  word-wrap: break-word;
  overflow-wrap: break-word;
}

#record-container td a {
  text-decoration: none;
  color: #0072bc;
}

#record-container td a:hover {
  text-decoration: underline;
}

#record-container button {
  padding: 5px 10px;
  margin: 2px;
  background-color: #f1f1f1;
  border: 1px solid #e0e0e0;
  border-radius: 3px;
  cursor: pointer;
}

#record-container button:hover {
  background-color: #0072bc;
  color: white;
  border-color: #0072bc;
}

#record-container button:active {
  background-color: #005a8e;
}

JavaScript

kintone REST API Client の CDN を、このコードより上に配置する必要があります。

(() => {
  'use strict';

  let allRecords = []; // 全レコード格納用配列
  let currentPage = 1; // 現在のページ番号を初期化
  let searchQuery = ''; // 検索ボックスの絞り込み条件
  let sortDirection = {}; // ソート条件
  const client = new KintoneRestAPIClient(); // kintone REST API Client のインスタンス

  // 1ページあたりの表示件数をローカルストレージから取得(取得できない場合は20件にする)
  let recordsPerPage = parseInt(localStorage.getItem('customRecordsPerPage'), 10) || 20;

  // レコード一覧画面でカスタマイズビューが選択されたら処理開始
  kintone.events.on('app.record.index.show', async (event) => {
    const recordContainer = document.getElementById('record-container'); // レコード表示用の要素
    if (!recordContainer) {
      console.warn('record-container が見つかりません。');
      return event;
    }

    try {
      const allResp = await client.record.getAllRecords({ app: kintone.app.getId() });
      allRecords = allResp; // レコード取得結果を保存

      // レコード番号で降順にソート
      allRecords.sort((a, b) => Number(b['レコード番号'].value) - Number(a['レコード番号'].value));

      currentPage = 1; // 初期ページを1ページ目にする
      renderTable(currentPage); // カスタマイズビュー描画関数を呼び出す
    } catch (error) {
      console.error('レコード取得エラー:', error);
    }

    return event;
  });

  // レコードとページネーションの描画
  const renderTable = (page) => {
    // 条件に一致するレコードをフィルタ
    const conditionFiltered = allRecords.filter(record =>
      (record['文字列1行A'].value === '条件A' && record['文字列1行B'].value === '条件B') ||
      record['文字列1行C'].value === '条件C'
    );

    // 検索ボックスの絞り込み条件でさらにフィルタ
    const filteredRecords = conditionFiltered.filter(record =>
      record['文字列1行A'].value.includes(searchQuery) ||
      record['文字列1行B'].value.includes(searchQuery) ||
      record['文字列1行C'].value.includes(searchQuery)
    );

    const start = (page - 1) * recordsPerPage;
    const end = start + recordsPerPage;
    const pageRecords = filteredRecords.slice(start, end); // 表示対象のレコードを抽出

    // ページネーションの要素を生成
    const generatePagination = (page, totalPages, position) => {
      let html = `<div style="margin:${position === 'top' ? '0 0 10px' : '10px 0'}; text-align:center;">`;
      if (page > 1) {
        html += `<button id="prevPage-${position}">◀ 前へ</button>`;
      }
      html += `<span> ${page} / ${totalPages} </span>`;
      if (page < totalPages) {
        html += `<button id="nextPage-${position}">次へ ▶</button>`;
      }
      html += `</div>`;
      return html;
    };

    const totalPages = Math.ceil(filteredRecords.length / recordsPerPage);
    let html = '';

    // レコードの上に、表示中のレコードの範囲、検索結果の件数、検索ボックス、1ページあたりの表示件数設定、ページネーションを配置
    html += `<div style="margin-bottom:10px;">
      表示中: ${start + 1} - ${Math.min(end, filteredRecords.length)}(全 ${filteredRecords.length} 件)
    </div>`;
    html += `<input type="text" id="searchBox" placeholder="検索..." style="margin-bottom: 10px;" value="${searchQuery}" />
      <select id="recordsPerPage" style="margin-left: 10px;">
        <option value="20" ${recordsPerPage === 20 ? 'selected' : ''}>20件</option>
        <option value="40" ${recordsPerPage === 40 ? 'selected' : ''}>40件</option>
        <option value="60" ${recordsPerPage === 60 ? 'selected' : ''}>60件</option>
        <option value="80" ${recordsPerPage === 80 ? 'selected' : ''}>80件</option>
        <option value="100" ${recordsPerPage === 100 ? 'selected' : ''}>100件</option>
      </select>`;
    html += generatePagination(page, totalPages, 'top'); // レコードの上に配置するページネーション

    // レコードのヘッダの内容
    html += `<table border="1" cellspacing="0" cellpadding="5">
      <thead>
        <tr>
          <th>詳細</th>
          <th class="sortable" data-field="文字列1行A">文字列1行A <span class="sort-icon" id="sort-文字列1行A"></span></th>
          <th class="sortable" data-field="文字列1行B">文字列1行B <span class="sort-icon" id="sort-文字列1行B"></span></th>
          <th class="sortable" data-field="文字列1行C">文字列1行C <span class="sort-icon" id="sort-文字列1行C"></span></th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>`;

    // 各レコード行の生成
    pageRecords.forEach(record => {
      const recordId = record['レコード番号'].value;
      const recordUrl = `${location.origin}/k/${kintone.app.getId()}/show#record=${recordId}`;
      html += `<tr data-record-id="${recordId}">
        <td><a href="${recordUrl}" target="_blank">🔗</a></td>
        <td>${record['文字列1行A'].value}</td>
        <td>${record['文字列1行B'].value}</td>
        <td>${record['文字列1行C'].value}</td>
        <td>
          <button class="edit-button">編集</button>
          <button class="save-button" style="display:none;">保存</button>
          <button class="cancel-button" style="display:none;">キャンセル</button>
          <button class="delete-button">削除</button>
        </td>
      </tr>`;
    });

    html += `</tbody></table>`;
    html += generatePagination(page, totalPages, 'bottom'); // レコードの下に配置するページネーション

    // DOMにHTMLを挿入
    document.getElementById('record-container').innerHTML = html;

    // ページネーションのボタンにクリックイベントを設定
    const setPaginationHandler = (id, handler) => {
      const btn = document.getElementById(id);
      if (btn) btn.onclick = handler;
    };

    setPaginationHandler('prevPage-top', () => renderTable(--currentPage));
    setPaginationHandler('nextPage-top', () => renderTable(++currentPage));
    setPaginationHandler('prevPage-bottom', () => renderTable(--currentPage));
    setPaginationHandler('nextPage-bottom', () => renderTable(++currentPage));

    // 1ページあたりの表示件数を変更したときの処理
    document.getElementById('recordsPerPage').onchange = (e) => {
      recordsPerPage = parseInt(e.target.value, 10);
      localStorage.setItem('customRecordsPerPage', recordsPerPage);
      renderTable(1);
    };

    // 検索ボックスの入力処理
    let isComposing = false;
    const searchBox = document.getElementById('searchBox');
    if (searchBox) {
      searchBox.value = searchQuery;

      searchBox.addEventListener('compositionstart', () => { isComposing = true; });
      searchBox.addEventListener('compositionend', (e) => {
        isComposing = false;
        searchQuery = e.target.value;
        renderTable(1);
      });
      searchBox.addEventListener('input', (e) => {
        if (isComposing) return;
        const pos = e.target.selectionStart;
        searchQuery = e.target.value;
        renderTable(1);
        setTimeout(() => {
          const newBox = document.getElementById('searchBox');
          if (newBox) {
            newBox.focus();
            newBox.setSelectionRange(pos, pos);
          }
        }, 0);
      });
    }

    setupRowHandlers();
    setupSortableColumns();
  };

  // ソート可能な列のヘッダにクリックイベントを設定
  const setupSortableColumns = () => {
    document.querySelectorAll('.sortable').forEach(header => {
      const field = header.dataset.field;
      const icon = document.getElementById(`sort-${field}`);
      let isAsc = sortDirection[field] === 'asc';

      header.onclick = () => {
        isAsc = isAsc === undefined ? false : !isAsc;
        sortDirection = {}; // ソート状態をリセット
        sortDirection[field] = isAsc ? 'asc' : 'desc';

        const sortedRecords = [...allRecords].sort((a, b) => {
          const valueA = a[field].value;
          const valueB = b[field].value;
          if (valueA > valueB) return isAsc ? 1 : -1;
          if (valueA < valueB) return isAsc ? -1 : 1;
          return 0;
        });

        allRecords = sortedRecords;
        renderTable(currentPage);
      };

      // ソートアイコンの表示を制御
      if (sortDirection[field] !== undefined) {
        updateSortIcon(icon, sortDirection[field] === 'asc');
      } else {
        icon.innerHTML = '';
      }
    });
  };

  // ソートアイコンを ▲ か ▼ に更新
  const updateSortIcon = (icon, isAsc) => {
    if (icon) icon.innerHTML = isAsc ? '▲' : '▼';
  };

  // 各行の編集、保存、キャンセル、削除ボタンの動作設定
  const setupRowHandlers = () => {
    document.querySelectorAll('tr').forEach(row => {
      const recordId = row.getAttribute('data-record-id');
      if (!recordId) return;

      const editBtn = row.querySelector('.edit-button');
      const saveBtn = row.querySelector('.save-button');
      const cancelBtn = row.querySelector('.cancel-button');
      const deleteBtn = row.querySelector('.delete-button');
      const editableCells = Array.from(row.querySelectorAll('td')).slice(1, 4); // 編集対象のセル

      // インライン編集に切替
      editBtn.onclick = () => {
        editableCells.forEach(cell => {
          const value = cell.textContent;
          const input = document.createElement('input');
          input.value = value;
          input.className = 'editable-input';
          input.setAttribute('data-field-code', cell.dataset.fieldCode || '');
          cell.setAttribute('data-original-value', value);
          cell.innerHTML = '';
          cell.appendChild(input);
        });
        editBtn.style.display = 'none';
        saveBtn.style.display = 'inline-block';
        cancelBtn.style.display = 'inline-block';
        deleteBtn.style.display = 'none';
      };

      // 編集をキャンセル
      cancelBtn.onclick = () => {
        editableCells.forEach(cell => {
          cell.textContent = cell.getAttribute('data-original-value');
        });
        editBtn.style.display = 'inline-block';
        saveBtn.style.display = 'none';
        cancelBtn.style.display = 'none';
        deleteBtn.style.display = 'inline-block';
      };

      // 保存処理
      saveBtn.onclick = async () => {
        const updatedRecord = {};
        editableCells.forEach((cell, index) => {
          const input = cell.querySelector('input');
          const fieldCode = ['文字列1行A', '文字列1行B', '文字列1行C'][index];
          updatedRecord[fieldCode] = { value: input.value };
        });

        try {
          await client.record.updateRecord({
            app: kintone.app.getId(),
            id: recordId,
            record: updatedRecord
          });

          editableCells.forEach(cell => {
            const input = cell.querySelector('input');
            cell.textContent = input.value;
          });

          editBtn.style.display = 'inline-block';
          saveBtn.style.display = 'none';
          cancelBtn.style.display = 'none';
          deleteBtn.style.display = 'inline-block';
        } catch (e) {
          console.error('更新エラー:', e);
          alert('レコードの更新に失敗しました。');
        }
      };

      // 削除処理
      deleteBtn.onclick = async () => {
        if (!confirm(`レコードID ${recordId} を削除してよろしいですか?`)) return;
        try {
          await client.record.deleteRecords({
            app: kintone.app.getId(),
            ids: [recordId]
          });
          allRecords = allRecords.filter(r => r['レコード番号'].value !== recordId);
          renderTable(currentPage);
        } catch (e) {
          console.error('削除エラー:', e);
          alert('削除に失敗しました');
        }
      };
    });
  };
})();
「いいね!」 1

このトピックはベストアンサーに選ばれた返信から 3 日が経過したので自動的にクローズされました。新たに返信することはできません。