Skip to content

スマートデザインの内部実装 ​

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

スマートデザインは、サイト設定を読み込み、ブラウザ内で配置と項目設定を編集し、保存時にサーバーへまとめて送る仕組みです。操作中の配置データと、保存されるサイト設定では表現が違います。 ここを押さえると、改行の扱い、未使用項目を追加したときの設定リセット、更新競合をコードから調べられます。

このページでは本体ソースで処理を確認しています。後半の調査サンプルはブラウザで実行するための独自コードです。実際のプリザンター環境との通信・画面操作は別途確認してください。

起動から保存までの流れ ​

図を読み込み中…

本番用の通信先は 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.svelteCustom Elementの起動と領域切替
App.svelte初期取得、編集用データへの展開、保存用JSON、応答処理
store.ts・types.ts共有状態、項目テンプレート、取得・保存データの型
EditContainer.svelteエディタへの追加、並べ替え、タブ間移動、改行の整理
EditModal.svelte項目設定の変更、一覧・フィルタへの反映、配置からの削除
GridContainer.svelte・FilterContainer.svelte一覧・フィルタの候補と並び順
Utility/UiDraggable.svelteSortableJSの生成・停止・破棄
Utility/functions.tsAxiosによる通信

サーバー側は 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.ColumnscolumnCollection項目設定のオブジェクト
SiteSettings.EditorColumnHasheditorColumnHashタブごとの項目名の並び
SiteSettings.GridColumns・FilterColumnsgridColumns・filterColumns一覧・フィルタの項目名の並び
SiteSettings.Tabs・SectionseditTabs・sectionsタブ・セクションの設定と採番情報
SmartDesignParamHashcolumnParamHash種類・分類・配置状態
DefaultColumnsdefaultColumns既定項目の情報
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: 0GeneralTabLabelText。Tabsからは除外
追加タブTabsと EditorColumnHash の _Tab-{Id} キー
セクションの項目 _Section-{Id}設定は Sections、配置は EditorColumnHash。Columnsからは除外
改行の項目 _Break-{番号}配置配列から除き、直後の項目設定を NoWrap: true にする

取得時には基本タブを編集用のタブ配列へ追加し、セクションを編集用の項目オブジェクトへ展開します。NoWrap の項目は、配置の先頭でない場合、その直前に _Break-* を挿入する形へ変換し、項目側の NoWrap を false に戻します(取得時の展開)。

保存時には上の表の変換を行います。_Break-* はそれ自体の項目設定を保存するものではありません。ドラッグで追加するときも、先頭・末尾、連続した改行、直後が編集対象でない配置などを除外しています(保存時の変換、改行の追加条件)。

例えば、編集用の並びが次の場合を考えます。

text
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にも合わせられます。基本タブだけでなく追加タブも調べます。

javascript
(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の応答分岐

関連ページ ​

変更履歴

第1版スマートデザインの状態管理と保存処理の解説を追加