Pleasanterのサイトマップを作る

本記事は、[OSSのノーコード・ローコード開発ツール「プリザンター」 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pleasanter) の8日目の記事です。

# 前置き
Pleasanterでフォルダ作ってサイトをまとめて移動して、さらにつくって・・・を繰り返した結果、あのサイトどこだっけ？ってなりませんか？なりますよね？そんなときのためのサイトマップを作ってみたいと思います。

# 作るサイトのイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/e08d1e56-cffc-25a4-2f1d-9b026386fd8e.png)
使い方は、サイトID（フォルダのID）を指定し、「サイトマップ」ボタンをクリックすることで、説明欄に表示されるというもの。  
結果はマークダウン形式にして、リンクを設定します。

# 仕組み
[以前の記事](https://qiita.com/m-isik/items/64fe9cd5de0d34d0ba1c)で、サイト情報を拡張SQLで取得する方法を説明しましたが、大枠はこの仕組みの流用です。ただ、今回は階層を掘っていくため関数の再帰呼び出しでフォルダ配下の全てのサイト情報を取得します。  

〇ざっくり処理の流れ
1. サイトIDに紐づく子サイトを[拡張SQL](https://pleasanter.org/manual/extended-sql-api)で取得
1. 取得結果をループし、サイト情報を書き出し
1. ループの中でさらにサイトIDに紐づく子サイトを[拡張SQL](https://pleasanter.org/manual/extended-sql-api)で取得→2へ（以下再帰）

# テーブル設定
サイトIDとサイトマップを表示する項目を追加し、スクリプトを実行するためのボタンをプロセスで設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922382/480135ed-0ec0-1de0-edb1-c4d4cd5ac4fb.png)
後述するスクリプトの関数**SiteMap**を呼び出します。

# 実装

### 拡張SQL
SiteMap.json.sql
基本は前回のほぼ同じ。サイトマップ表示用にSiteIDだけでなく、Title、ReferenceTypeも取得するように変更。
```sql
SELECT [SiteId],[Title],[ReferenceType]
FROM [Sites]
WHERE [ParentId]=@ParentId
```

SiteMap.json
```json
{
    "Name": "SiteMap",
    "Api": true
}
```
拡張SQLのセットになるJSON

### スクリプト
ツリー構造を表現するため、再帰呼び出しするたびにインデント（スペースの数）を加味しています。
```javascript
//プロセスから呼ばれるメイン関数
const SiteMap = async () => {
    $p.set($p.getControl('DescriptionA'), '[md]  \n');
    //最初に親サイトの情報を取得・設定
    let parentSiteData = await ApiGetSite($p.getControl('NumA').val());
    SetSiteName(parentSiteData,0);
    //拡張SQLにより、子サイトを検索
    let res = await GetSites($p.getControl('NumA').val());
    //取得サイトをループ処理
    if(res.Response.Data.Table.length > 0){
        await SetSiteNames(res.Response.Data.Table,1);
        document.getElementById('UpdateCommand').click();
    }    
};

//再帰呼び出しされる関数
const SetSiteNames = async (table,indent) => {
    //サイトマップの階層を表現するためのインデント
    let num = indent;
    for (let data of table) {
        console.log(data.SiteId);
        //サイト名を設定
        SetSiteName(data, num) 
        //さらに子サイトを検索
        let res = await GetSites(data.SiteId);
        if(res.Response.Data.Table.length > 0){
            //インデントを加味した上で再帰呼び出し
            indent++;
            await SetSiteNames(res.Response.Data.Table, indent);
            indent = num;
        }
    }
};

//取得したサイト情報をサイトマップとして整形して説明欄に設定する
const SetSiteName = async (data, indent) => {
    let strIndent = '';
    //サイトマップに表示する文言を成形
    let msg = `${data.SiteId}:${data.Title}:${data.ReferenceType}`
    //設定されたインデント分スペースを設定
    for (let i = 0; i < indent; i ++){
        strIndent = strIndent + '　';
    }    
    let descA = $p.getControl('DescriptionA').val();
    //マークダウンに設定するURLを生成
    let url = SetURL(data.SiteId);
    //説明欄に追記
    $p.set($p.getControl('DescriptionA'), `${descA}${strIndent}[${msg}](${url})  \n`);
};

//サイトマップに設定するURLを生成
const SetURL = (siteId) => {
    let uri = new URL(window.location.href);    
    let url = `${uri.origin}/items/${siteId}/index`;
    return url   
}

//拡張SQLによる子サイトの検索
const GetSites = async (siteId) => {
    let data = {
        "ApiVersion": 1.1,        
        "Name": "SiteMap",
         "Params": {
            "ParentId": siteId
        }
    }
    let uri = new URL(window.location.href);
    let url = `${uri.origin}/api/extended/sql`;
    let response = await fetch(url, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: {
            'Content-Type': 'application/json'            
            },
    });
    return response.json();
};

//APIによるサイト情報取得
const ApiGetSite = async (siteId) => {
    let res;
    await $p.apiGetSite({
        id: siteId,
        done: function (data) {
            res = data.Response.Data;
        }
    });
    return res;
}
```

# 注意点
再帰的にSQLを発行する処理を実行するすため、サイト数が多ければ多いなりにシステムへの性能負荷が高まります。巨大なサイト群で実行したい場合は注意が必要です。