スマートデザインの内部実装
スマートデザインは、サイト設定を読み込み、ブラウザ内で配置と項目設定を編集し、保存時にサーバーへまとめて送る仕組みです。操作中の配置データと、保存されるサイト設定では表現が違います。 ここを押さえると、改行の扱い、未使用項目を追加したときの設定リセット、更新競合をコードから調べられます。
このページでは本体ソースで処理を確認しています。後半の調査サンプルはブラウザで実行するための独自コードです。実際のプリザンター環境との通信・画面操作は別途確認してください。
起動から保存までの流れ
図を読み込み中…
本番用の通信先は smartdesign と UpdateSmartDesign です。開発用は末尾が .json の別設定になっています(本番設定、開発設定)。
メニューが出る条件と起動の入口
メニューの表示条件は、サイト管理権限があり、サイトの種類が Sites・Wikis・Dashboards ではなく、Mobile・Responsive がともに false、テーマのCSSバージョンが 2.0 以上であることです。メニューが見当たらないときは、まずこの条件を確認します(表示条件)。
サーバーは smart-design-link 要素を出力します。Trigger.svelte はページ内に smart-design がなければ body に追加し、クリックまたは Enter・スペースキーで appState を true にします。Main.svelte はその状態を見て App を表示し、tabState に応じてエディタ・一覧・フィルタのコンポーネントを切り替えます(要素の出力、起動、領域の切替)。
ソースを読む順番
フロント側の基点は Implem.PleasanterFrontend/components/src/lib/SmartDesign/ です。
| ファイル | 調べられること |
|---|---|
| Trigger.svelte・Main.svelte | Custom Elementの起動と領域切替 |
| App.svelte | 初期取得、編集用データへの展開、保存用JSON、応答処理 |
| store.ts・types.ts | 共有状態、項目テンプレート、取得・保存データの型 |
| EditContainer.svelte | エディタへの追加、並べ替え、タブ間移動、改行の整理 |
| EditModal.svelte | 項目設定の変更、一覧・フィルタへの反映、配置からの削除 |
| GridContainer.svelte・FilterContainer.svelte | 一覧・フィルタの候補と並び順 |
| Utility/UiDraggable.svelte | SortableJSの生成・停止・破棄 |
| Utility/functions.ts | Axiosによる通信 |
サーバー側は ItemsController → ItemModel → 取得用の SmartDesignApiModel または保存用の SiteUtilities.UpdateSmartDesign と進みます。
取得データと編集状態を区別する
App.svelte は hidden input の ApplicationPath・SiteId・JoinedSites を読み、現在のサイトが JoinedSites にあれば取得を始めます。通信には X-Requested-With: XMLHttpRequest を付けます。取得側のコントローラーは Ajax リクエストでなければ BadRequest を返し、記録テーブルの取得処理ではサイト管理権限も検証します(初期取得、コントローラー、取得権限)。
取得完了を待つ処理には、通信と並行して750ミリ秒の待ち時間が入っています。750ミリ秒でタイムアウトする処理ではありません。 両方が完了してからstoreを初期化します(Promise.all)。
| 取得データ | 主な編集用store | 役割 |
|---|---|---|
SiteSettings.Columns | columnCollection | 項目設定のオブジェクト |
SiteSettings.EditorColumnHash | editorColumnHash | タブごとの項目名の並び |
SiteSettings.GridColumns・FilterColumns | gridColumns・filterColumns | 一覧・フィルタの項目名の並び |
SiteSettings.Tabs・Sections | editTabs・sections | タブ・セクションの設定と採番情報 |
SmartDesignParamHash | columnParamHash | 種類・分類・配置状態 |
DefaultColumns | defaultColumns | 既定項目の情報 |
Timestampを含む取得結果 | rowData | 保存時に使う元データ |
storeはモジュールから共有されます。項目設定と並び順は別々のデータなので、DOMの順だけを変えても保存用の配置データは変わりません(storeの定義、保存データの組立て)。
Stateの -1・0・1 は何を表すか
SmartDesignParamHash の各項目には State.Edit・State.Grid・State.Filter があります。サーバーの基本判定は、対象として使えなければ -1、使えるが配置に含まれなければ0、配置に含まれれば1です。別タブに配置された項目には追加の判定があります(判定処理)。
フロント側でも取得後に補正し、Comments・UpdatedTime の State.Edit を -1 にし、Locked の種類と分類を指定します。したがって、サーバーのJSONだけで候補の表示を判断せず、取得後の補正も追います(補正)。
一覧・フィルタのカスタム項目候補は、Category === 'Custom' かつ State.Edit === 1 に絞っています。エディタに配置されていないカスタム項目が候補に出ない場合、この判定が確認箇所になります(一覧、フィルタ)。
ドラッグがサイト設定になるまで
UiDraggable.svelte はマウント時に Sortable.create を呼び、破棄時に destroy します。withSortablePaused はSortableを停止して処理し、Svelteの tick を待ってから、finallyで再開します。DOMの移動とSvelteの再描画を調整する箇所です(Sortableの管理)。
エディタの候補側は pull: 'clone'・put: false・sort: false です。候補からの追加では、配置先の editorColumnHash に項目名を挿入し、setAppEdited() を呼びます。Sortableが動かした要素の後処理も行っています(候補設定、追加の反映)。
未使用のカスタム項目を追加すると設定がリセットされる理由
取得用モデルは、全項目設定をそのまま SiteSettings.Columns に渡していません。エディタに配置された項目、または DefaultColumns に含まれる項目に絞ります。一方で、候補判定用のメタデータは別途作ります(Columnsの抽出、メタデータの作成)。
追加処理は、columnCollection に既存項目があればそのオブジェクトを使います。なければカスタム項目のテンプレートを使い、同じ種類で State.Edit === 0 の項目名を探して割り当てます。既存設定を改めて取得する処理ではありません。 テンプレートには IsNewEnable: true が入っています(追加の分岐、テンプレート)。
保存側の UpsertColumnsByApi は、IsNewEnable が true の項目に ResetColumn を実行してから、送られた項目設定を反映します。ResetColumn は項目定義から新しい Column を作り、元の項目設定を置き換えます。未使用項目を追加したら以前の設定が変わった場合は、保存リクエストの Columns にある IsNewEnable を調べてください(保存側のリセット、ResetColumn)。
項目設定の確定とサイト全体の保存は別
EditModal の更新処理は、一覧・フィルタの配置配列と columnCollection または sections を変更し、編集済みフラグを立てて閉じます。この処理ではサーバーへ保存しません。サイト全体の保存は App.svelte の onSubmit で行います(項目設定の更新、サイト全体の送信)。
setAppEdited は hasAppEdited を true にし、離脱確認用の window.onbeforeunload を設定します。閉じる処理は、送信中でなく取得済みの場合に状態を戻し、離脱確認を解除します(変更・閉じる処理)。
タブ・セクション・改行の変換
| 編集画面で扱うもの | 保存時の表現 |
|---|---|
基本タブの Id: 0 | GeneralTabLabelText。Tabsからは除外 |
| 追加タブ | Tabsと EditorColumnHash の _Tab-{Id} キー |
セクションの項目 _Section-{Id} | 設定は Sections、配置は EditorColumnHash。Columnsからは除外 |
改行の項目 _Break-{番号} | 配置配列から除き、直後の項目設定を NoWrap: true にする |
取得時には基本タブを編集用のタブ配列へ追加し、セクションを編集用の項目オブジェクトへ展開します。NoWrap の項目は、配置の先頭でない場合、その直前に _Break-* を挿入する形へ変換し、項目側の NoWrap を false に戻します(取得時の展開)。
保存時には上の表の変換を行います。_Break-* はそれ自体の項目設定を保存するものではありません。ドラッグで追加するときも、先頭・末尾、連続した改行、直後が編集対象でない配置などを除外しています(保存時の変換、改行の追加条件)。
例えば、編集用の並びが次の場合を考えます。
General = [Title, _Break-1, ClassA, _Section-2, NumA]保存用の並びは [Title, ClassA, _Section-2, NumA] になり、Columns の ClassA.NoWrap が true になります。_Section-2 の設定は Sections に置き、Columns に混ぜません。この例は、保存時の変換処理を読み解くためのモデルです。
保存の権限・競合・応答処理
サーバーの UpdateSmartDesign は、更新検証とレコードの存在確認を行い、JSONを SiteSettingsApiModel に変換します。更新検証ではサイト管理権限を確認します(保存の入口、権限検証)。
この保存処理で反映するのは、Columns・EditorColumnHash・GridColumns・FilterColumns・Sections・GeneralTabLabelText・TabLatestId・Tabs です。それぞれ null でなければ更新処理に進みます。フロントは Links も送りますが、ここには Links 自体を代入する分岐はありません。項目更新側には SetLinks の呼び出しがあります。送信したキーがすべてそのまま置き換えられると考えず、受け側の処理を確認します(保存対象、項目のリンク更新)。
Timestamp は取得時の値をそのまま送信し、サーバーの siteModel.Timestamp に設定します。サイト更新のSQLは、Timestampが有効な範囲内で競合チェックが有効なら、UpdatedTime を更新条件に含めます。競合した場合は UpdateConflicts へ進みます。競合を調べるときは、取得時と保存時のTimestamp、間に入った別のサイト更新を確認します(送信値、受取、更新条件、競合検出)。
フロントはAxiosでPOSTし、通信と500ミリ秒の待ち時間が完了した後、応答を次のように扱います。
Valueがあれば、送信中フラグを戻してメッセージを表示する。ValueがなくUrlがあれば、location.replaceで遷移する。- どちらもなければ、再読み込みする。
- 通信で例外が出たら、応答に
Valueがあればメッセージに設定し、送信中フラグを戻す。
この分岐は Method ではなく Value・Url を見ています。通常の画面更新の命令配列を $p.dispatch に渡す経路とも異なります。HTTPステータスだけでなく、JSONの形と、この分岐まで確認してください(Axiosの呼び出し、保存応答の処理)。
調査サンプル――取得データの配置と候補を一覧する
サイト管理権限のあるユーザーで対象テーブルを開き、ブラウザの開発者ツールのConsoleで実行します。GETで現在の設定を読み、配置配列とメタデータを表にします。ApplicationPath を使うため、サブディレクトリ配下のURLにも合わせられます。基本タブだけでなく追加タブも調べます。
(async function () {
const appPath = document.getElementById('ApplicationPath')?.value;
const siteId = document.getElementById('SiteId')?.value;
if (!appPath || !/^\d+$/.test(siteId ?? '')) {
throw new Error('対象テーブルの画面で実行してください。');
}
const response = await fetch(`${appPath}items/${siteId}/smartdesign`, {
method: 'GET',
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!data.SiteSettings || !data.SmartDesignParamHash) {
console.log(data);
throw new Error('設定を取得できませんでした。応答と権限を確認してください。');
}
const ss = data.SiteSettings;
console.log('Timestamp', data.Timestamp);
console.table(Object.entries(ss.EditorColumnHash ?? {}).flatMap(([tab, names]) =>
names.map((name, order) => ({ tab, order, name }))
));
console.table(Object.entries(data.SmartDesignParamHash).map(([name, item]) => ({
name,
type: item.Type,
category: item.Category,
edit: item.State.Edit,
grid: item.State.Grid,
filter: item.State.Filter,
hasColumnSettings: (ss.Columns ?? []).some(column => column.ColumnName === name)
})));
console.table((ss.Columns ?? []).map(column => ({
name: column.ColumnName,
label: column.LabelText,
noWrap: column.NoWrap,
fieldCss: column.FieldCss
})));
})().catch(console.error);この結果はサーバーから取得した設定です。開いているスマートデザインで未保存の変更をしていても、その編集用storeの内容を取得するコードではありません。例えば _Break-* は取得後に生成されるため、このGET結果の配置配列にあることを前提にしません。
保存されるJSONを調べる手順
保存時の変換を調べるには、開発者ツールのNetworkで UpdateSmartDesign のリクエスト本文を確認します。再現用のテーブルで、項目追加・改行追加・タブ移動を一つずつ実施して保存すると、どの配列や設定が変わるかを追えます。
| 症状 | 確認する値・処理 |
|---|---|
| メニューが出ない | 管理権限、ReferenceType、Mobile・Responsive、テーマCSSバージョン |
| 起動しても閉じる・取得エラーになる | hidden input、JoinedSites、GETのヘッダー・応答、取得後の初期化 |
| 未使用項目を追加すると設定が変わる | GETのColumnsに含まれるか、追加テンプレート、POSTのIsNewEnable |
| 一覧・フィルタの候補に出ない | CategoryとState.Edit、取得後の補正 |
| 改行が想定と違う | 編集中の_Breakの位置、POSTのNoWrap、追加時の無効な改行判定 |
| 項目設定を確定したのに保存されない | モーダル内の確定か、サイト全体のPOSTまで実行したか |
| 更新競合になる | GET・POSTのTimestamp、他のサイト更新、UpdateConflicts |
| 保存応答が画面へ反映されない | Value・Urlの有無、Axiosの例外、App.svelteの応答分岐 |
関連ページ
- フロント側の基盤JSを読む — 通常の画面操作と通信経路
- dispatchの内部動作 — 命令配列で画面を更新する経路
- サイト設定の変遷 — 保存されるSiteSettingsの構造
- CSSの動的制御 — 実際のレコード画面の項目・行にCSSを付ける方法