Closed13

Notion API による simple table の更新

ピン留めされたアイテム
nikogolinikogoli

◆ Notion API 雑感

  1. bulk update と bulk delete ができないので、変更に時間がかかる
    変更のために block を1つずつ触っていくことになるのだが、1秒に3回まで?のアクセスリミットに引っかからないようにしなければならず、変更速度が遅い\\
     \\

  2. update block はタイプの変更が不可 + append blockは位置指定が不可 = どうしようもない
    『「block のタイプの変更」を伴う内容の差し替え』には、向いていない

nikogolinikogoli

ブロックの親子関係を利用することで、一応、対策ができる。
ポイント:操作したいブロックを、callout などの children を持てる block の子要素にする
問題:API を叩く前に notion 上で準備する必要があるので、面倒\\
  \\

  • bulk delete
    callout などの親要素に対して delete block を行う。親ブロックごと消去すると、子ブロックの数に関係なく1回の API 呼び出しで消去ができる\\
     \\

  • 位置を維持した差し替え
    更新後の内容のブロックを、callout などの親要素の children として追加し、 元のブロックを削除する。親が動かないので、要素の位置も変化しない

nikogolinikogoli

「親 -- 中間 -- 子」の3層構造にすると、上の2つを組み合わせることで bluk update が比較的簡単に行える[1]

  1. notion 上で、更新したい複数のブロックを中間要素の子要素にする

  2. API を使い、親要素に対して、新しいブロックを「中間要素その2」として追加する

  3. 更新後の内容のブロックを、中間要素その2を親要素として、新規に追加する[2]

  4. 中間要素のブロックを削除する (ことで更新前のブロックをまるごと削除する)\\
     \\

問題:ブロック自体が別のものになり、元のものの変更履歴等が継承されない

脚注
  1. bluk update自体は API の正式な機能として追加するという話が \beta が外れた際にあった気がする。が、見つからない ↩︎

  2. table block の場合は「table 追加 → table row 追加」のように2回APIを呼ぶ必要はなく、「table row を children として持った table 追加」で済む。他のタイプの場合、追加するブロックが children を持っているとエラーになるっぽい。エラーメッセージは body.children[0].children should be not present, instead was [{"object":"block","type":"bulleted_list_item","bullete...]だった (が他にもメッセージの内容はあるので、これが原因かどうかは不明) ↩︎

nikogolinikogoli

https://www.reddit.com/r/Notion/comments/st51hm/how_to_automate_hex_color_preview_in_the_database/

↑のアンサー的なものを探っていた過程でいろいろ触った結果

◆リンクの話の内容:ざっくり

  1. notion は KaTex が使えるので、\color{色名やhexコード}{色を付けたい文字}命令がデータベースのプロパティ(や simple table のセル)で使える

    • 例えば \color{#942343}{██████}は、 \color{#942343}{██████} という表示になる
  2. データベースのプロパティの1つにカラーコードを設定し、また別のプロパティに Tex コードで色を表示させることで、↓のような状態にしたい

  3. 手動でやるのは辛いので、hexコードを入力するだけで texコードを自動的に挿入させる方法はないだろうか?

nikogolinikogoli

◆結論:たぶん無い

  • formula を使えば texコードを生成することはできる
  • しかし、texコードを『数式』として評価させることができない (と思う)

texコードが KaTex で処理されているとき、内部的には、そのテキストが equation block として扱われていると思われる。これは rich text object の一種らしい。

これに対し、fomula はあくまでも fomula であって rich text object ではない。
したがって equation block として評価させることはできず、tex コードを生成できても texコードとしては機能しない、という状態[1]のようである。

脚注
  1. pythonのf"{fomula の生成物}"のような文字列フォーマットが可能ならできそうなんだけど ↩︎

nikogolinikogoli

で、思いついたのが、「hexコードから texコード文字列を生成し、それを equation block として API を使って追加すればよいのでは?」ってこと。
データベースでやるのは色々としんどそうだったので、simple table でやってみることにした。

やりたいこと

  1. ↓このような simple table が notion 上に存在しているとする

    日本語名 英語名 カラーコード
    ブラック black #000000
    ミッドナイトブルー midnight blue #001e43
    インクブルー ink blue #003f8e
    ボトルグリーン bottle green #004d25
    クロムグリーン chrome green #00533f
  2. このテーブルの内容を API を使って取得して、それぞれの hexコードから対応する texコードを生成する

  3. 「色」列のそれぞれのセルに、API を使って生成した texコードを数式として挿入する

nikogolinikogoli

もう少し具体的な「やること」

  1. 当該 simple table の id を取得して、notion.blocks.children.list(テーブル)で各行のデータを取得する
  2. 各行のデータから hexコードを取り出し、texコードを生成する
  3. 生成した texコードを expression として持つ、type="equation"のオブジェクトを作る
  4. 各行のデータにおいて、「色」列に対応するセルのデータを上で作成したオブジェクトに差し替える (今回の場合、空セルでデータが[]になっているのでここにオブジェクトを入れる)
  5. それぞれの行において、差し替えたデータを与えてnotion.blocks.update(行)を行う
  6. あるいは、差し替えたデータを新規行としてnotion.blocks.children.append(テーブル)でまとめて追加し、古い行を一個づつ消す
  7. あるいは、 差し替えたデータを行として持つ、新しい simple table をnotion.blocks.children.append(ページ)で追加し、古いテーブルを削除する
nikogolinikogoli

更新方法の比較

  • それぞれの列でnotion.blocks.update(行)
    ✅ 副作用なし  ❌ 全ての行に追加されるまで時間がかかる\\[7pt]
     \\
  • notion.blocks.children.append(テーブル)でまとめて追加:
    ❌ 行の記録の連続性が途切れる  ✅ 追加自体は一瞬だが、⚠ 掃除に手間がかかる\\[7pt]
     \\
  • 新しい simple table をnotion.blocks.children.append(ページ)で追加:
    ❌ テーブルの記録の連続性が途切れる  ✅ 一瞬で終わるが、⚠ 場所がページ末尾に
nikogolinikogoli

些細な追加情報

  • \color{色}{████} より \colorbox{色}{日本語空白×5個くらい}のほうが綺麗な表示になる
    unicode Full block \color{#942343}{█████}
    日本語空白 \colorbox{#942343}{   }
nikogolinikogoli

方法:テーブルに更新済みのデータを新規行として一括追加し、古い行を順次削除する

  • なぜか update が上手く機能しないので、こっちの方法で行う
    • update でやる場合は、追加処理を削除して promise の直列処理の内容を更新に変える
// notion 上で copy link to block を行って id を取得し、テーブルの id を得る
const target_url =  "https://www.notion.so/~~ページのID~~#~~テーブルのID~~";
const [page_id, table_id] = target_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);

// セルデータを作成する関数:数式はテキストと異なる prop にする必要がある
function set_celldata_obj(is_eq, text){
    const annot_props = { "bold": false, "italic": false, "strikethrough": false,
        "underline": false, "code": false, "color": "default"
    };
    let obj = new Object({"annotations": annot_props, "plain_text": text, "href": null});
    if (is_eq) {
        obj.type = "equation";
        obj.equation = {"expression": text}
    } else {
        obj.type = "text";
        obj.text = {"content": text, "link": null}
    };
    return obj }

// セルデータの更新: blocks.children.list(~) でデータを取ってきて差し替える
const new_table_rows = await notion.blocks.children.list({ block_id:table_id }).then( response => {
    // 必要な prop だけ取り出す
    const org_table_rows = response.results.map(x => { const {id, type, table_row} = x; return {id, type, table_row} });

    // カラーコードを見つける → texコードを作成 → [] のセルデータを texコードのものに差し替え
    // 空セルでは cell=[]≠[{Object}] であり、 cell[0].plain_text を適用できないので処理を分ける
    const table_rows = org_table_rows.slice(1).map( row => {
        const color_code = row.table_row.cells.map( cell => (cell.length) ? cell[0].plain_text : "" ).find(text => text.includes("#"));
        const new_cell = set_celldata_obj(true, `\\colorbox{${color_code}}{     }`);
        return {"id": row.id, "type": row.type, "table_row":{"cells": row.table_row.cells.map(cell =>  (cell.length) ? cell : [new_cell] )}}
    });
    return table_rows
});

// テーブルの末尾に一括追加 object="block" の prop を追加することが(たぶん)必要
await notion.blocks.children.append({
    block_id: table_id,
    children: new_table_rows.map(item => {let copied = {...item}; copied.object="block"; return copied })
});

// Promise の直列処理 この辺の正当なやり方がわからない
await new_table_rows.reduce((promise, item) => {
    return promise.then(async () => {
        await new Promise(s => setTimeout(s, 1000)).then( // もうちょっと早くしても大丈夫かも
            await notion.blocks.delete({ "block_id": item.id })
        ).then(
            response => console.log(response)
        )
    });
  }, Promise.resolve()
)
nikogolinikogoli

別の方法:callout の子要素にして、位置を保ちつつテーブルごと差し替える

  1. callout の id からテーブルの id を取得する
  2. テーブルの id から行データの id を取得する
  3. texコードを追加した更新後の行データを作成する
  4. 更新後の行データから、table row blockオブジェクトのリストを作成する
  5. table blockオブジェクトを作成し、そのchildren prop に上で作成したオブジェクトのリストを設定する
  6. このtable blockオブジェクトを、callout の子要素(新規テーブル)として追加する
  7. 元のテーブルを削除する

↑の5の処理が、公式doc の table block の説明にある以下の文章の具体的な実施法になっている[1]

When creating a table block via the Append block children endpoint, the table must have at least 1 table_row whose cells array has the same length as the table_width.

const parent_url = "https://www.notion.so/~~ページのid~~#~~コールアウトのid~~"
const [page_id, parent_id] = parent_url.match(/so\/(.+)#(.+)/).slice(1).map(x => x);

let table_id = ""
let headers = [false, false]; // ヘッダー色付けの設定を元のテーブルに合わせる
let header_row_obj = undefined; // ラベル行を元のテーブルから流用する

// 親要素の id から子要素(のリスト)を取得 → 子要素(テーブル)から行データを取得 → texコードの作成と挿入
const new_table_rows = await notion.blocks.children.list({ block_id:parent_id }).then( async response => {
    table_id = response.results[0].id;
    headers = [response.results[0].table.has_column_header, response.results[0].table.has_row_header];
    return await notion.blocks.children.list({ block_id:table_id })
}).then( response => { // ここ以下は前の手法と同じ
    const org_table_rows = response.results.map(x => { const {id, type, table_row} = x; return {id, type, table_row} });
    header_row_obj = org_table_rows[0]; // ラベル行を保存しておく
    const table_rows = org_table_rows.slice(1).map( row => {
        const color_code = row.table_row.cells.map( cell => (cell.length) ? cell[0].plain_text : "" ).find(text => text.includes("#"));
        const new_cell = set_celldata_obj(true, `\\colorbox{${color_code}}{     }`);
        return {"id": row.id, "type": row.type, "table_row":{"cells": row.table_row.cells.map(cell =>  (cell.length) ? cell : [new_cell] )}}
    });
    return table_rows
});

// ラベル行のデータと他の行データを結合し、table row block object のリストとして整える
const row_blocks = [header_row_obj].concat(new_table_rows).map(item => {
    return { "object": 'block', "type": item.type, "table_row": item.table_row } 
})

// 子要素として table row block object のリストを持つ、table block object を作成する
const table_object = { "object": 'block',
    "type": "table",
    "has_children": true,
    "table": { "table_width": header_row_obj.table_row.cells.length, // ラベル行のセル数を流用
        "has_column_header": headers[0], // 元のテーブルに合わせる ↓も同じ
        "has_row_header": headers[1],
        "children": row_blocks // [table row block object, ...] の形式
    }
}

// 親要素(ここでは callout)に、子要素として table block を追加する
await notion.blocks.children.append({
    block_id: parent_id,
    children: [table_object]
}).then(async () => { // 追加が終わったら、元のテーブルは削除する
    return await notion.blocks.delete({ "block_id": table_id })
}).then(
    response => console.log(response)
)
脚注
  1. children を与えずに table block を追加しようとすると、「children をちゃんと設定してね」的なエラーメッセージが出る ↩︎

このスクラップは2022/03/21にクローズされました