Notionを操作するシンプルなRaycast 拡張機能を作って色々学んだ


こんにちは、Androidエンジニア志望のHalkoです。

最近、Raycastというランチャーツールを使用し始めました。
Raycastは自分で拡張機能を保存できます。
そこで、普段Notionに記録している作業記録を、Raycastから保存できるような拡張機能を作ってみました。

# 準備
インストールすべきもの
- Raycast 1.26.0 またはそれ以上のバージョン
- Node.js 16.10 or またはそれ以上のバージョン 
  nvmのインストールが推奨されているようです
- Visual Studio Codeをコードエディタとして使用します。

## 新しく拡張機能を作る
<img width="700" alt="代替テキスト" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026395/164a5ea3-7f79-31be-f263-3cf6546f57f2.png">

Raycastを開いて、create Extensionと入力します

作成画面が出てくるので、必要最低限の項目を埋めます。


 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026395/9b1f1e88-460e-5284-5d05-8912c65f4f8f.png)
- テンプレートには、Formを指定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026395/8cabf8ae-7268-9178-27cd-2c21f46a8c97.png)

- Extension Name, Location, Command Nameが必須項目

Create Extensionのコマンドを実行すると、テンプレートに沿った拡張機能ファイルが作成されます。
Open Manage Extensions　というコマンドが出てきます。
実行すると、VSCodeが自動的に開きま拡張機能の一覧が確認できます。
<img width="700" alt="代替テキスト" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026395/a86758c5-c538-1a8f-a4cb-c4fb4f175bf2.png">
EnterでVsCodeが自動的に開きますので、
ここから、テンプレートファイルを編集していきます。

## ターミナルでの準備
開かれたファイルにある、index.tsxのimportに記載されている、@raycast/apiをインストールします。
また、今回はNotionのAPIも使用したいので、一緒にインストールしておきます。

ターミナルを開いて、
```
$ npm install @notionhq/client
$ npm install @raycast/api
```
また、拡張機能を実行できるようにするため
```
$ npm i
$ npm run dev
```
これで、ファイルを保存するたびに、自動でビルドして、変更された拡張機能を実行してくれるようになります。


# いざ、コードの記入
今回は、
- NotionDBから一行分のデータを得る
- UIに表示する
- NotionDBの行をアップデートする
- NotionDBに新しく行を挿入する

というのが主な機能です。

出来上がったものは、こんな感じです。

**アクティビティ開始**
![part 1.1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026395/4e53ed4d-4691-aeec-939c-5cd99e1eb993.gif)
 **アクティビティ終了**
![part 2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026395/2f961061-9b47-7d05-1a61-4a69a5101daf.gif)|


APIを触るのも、TypeScriptを書くのも初めてだったので、新しく学んだことだらけでした。


全てはここに書ききれないので、最終的なindex.tsxのコードを挙げておきます。
```tsx
import { Action, ActionPanel,  Form ,LaunchProps} from '@raycast/api'
import { useRef} from "react";
import { getUIDataFromNotion } from './getUIDataFromNotion'; 
import { usePromise } from "@raycast/utils";
import { EntryValues } from './EntryValues';




const Demo = () => {
  const abortable = useRef<AbortController>();
  const { isLoading, data, revalidate } = usePromise(
    async () => {
    const result = await getUIDataFromNotion()
      return result;
    },[],
    {abortable}
  );
  
  console.log("called")
  return (
    <Form 
      key={"form"}
      isLoading ={isLoading}
      actions={
      <ActionPanel
      key={"actionPanel"}
      >
        <Action.SubmitForm
        key={"Action.SubmitForm"}
    
          title={data?.submitTitle}
          onSubmit={(values: EntryValues) => {
            data?.doOnSubmit(values)
          }}
        />
      </ActionPanel>
    }>
    <Form.TextArea
        key={"Form.TextArea"}
        id="contentField"
        title={data?.contentTitle}
        value = {data?.openedRowData?.comment}
      />
      <Form.Dropdown key={"Form.Dropdown"} id="tag" title="activity tag">
        {data?.tagList.map((item:string)=>(
          <Form.Dropdown.Item key={"Form.Dropdown.Item"+item} value={item} title={item}  />
        ))}
      </Form.Dropdown>
      <Form.DatePicker 

      key={"Form.DatePicker"}
      id="dateTime" 
      title={data?.dateTitle} 
      defaultValue={new Date()}
       />
    </Form>
    );
  
  

  
};

 export default function Command(props: LaunchProps<{ draftValues: EntryValues }>) {
    return Demo()
}

```
解説のポイントは、
- `usePromise`
- `LaunchProps`

です


## usePromise

Notionからデータを得る、というように、
非同期処理でデータを得たい場合、返り値はPromise<欲しいクラス>となるようです。
```typescript
async function getNotionData():Promise<NotionData>{
  //Notionからデータを取ってくる
  const response = await notion.databases.retrieve({database_id:databaseId})
  const notionData = getNotionDataFromResponse(response)
  return new Promise((resolve,reject)=>
      resolve(notionData)
    )
}
```
Promise<>の中のクラスにアクセスしたい場合、
await という処理をくっつけなければなりません。

厄介なことに、await処理は、非同期処理の中でしか呼べません。
```typescript
//NotionDataにはアクセスできないので、エラーになる
const data:NotionData = getNotionData()

//awaitをつけると大丈夫
const data:NotionData = await getNotionData()

//非同期処理でないCommand()でawaitを呼ぶと、エラーになる
function Command(){
    const data:NotionData = await getNotionData()
}
```


最終的にRaycastから呼ばれる、Command()　のなかで、NotionDataを使いたいわけですが、
このCommand()は非同期処理(async function)にすることができません。

そこで、Raycastが提供しているusePromiseユーティリティを使用すると、
Command()を非同期処理にすることなく、awaitを使えるようになります！
```typescript
  const abortable = useRef<AbortController>();
  //dataにアクセスすればNotionDataにアクセスできる！
  const { isLoading, data, revalidate } = usePromise(
    async () => {
    const result = await getNotionData()
      return result;
    },[],
    {abortable}
    );
```
非同期処理が終わり、Promiseデータが返ってきた瞬間に、UIを更新してくれます。


## LaunchProps

これは非常にシンプルな話です。
前述の通り私は、

**Raycastの画面上に記入したテキストや日付を、Notionに送る機能**

を作りたかったです。

そのためには、Notionにデータを送る、というコマンドが呼ばれた時、
UIに記入されたデータが必要でした。

そこで、Command()の引数に、LaunchPropsを追加しました。
```typescript
  interface EntryValues {
    dateTime: Date ;
    contentField: string;
    tag: string;
  }
  export default function Command() {
    return Demo()
  }
//変更後
 export default function Command(props: LaunchProps<{ draftValues: EntryValues }>) {
    return Demo()
 }
  
```
`EntryValues`は自分で作成したinterfaceなので、自分が作ったUIに応じて変更してください。
LaunchPropsを追加した後、
UIのデータにアクセスしたい際のコマンドを書き換えます。
```tsx
 <Action.SubmitForm
        key={"Action.SubmitForm"}
          title={data?.submitTitle}
          onSubmit={
            //EntryValuesをとってくる
           (values: EntryValues) => {
            //valuesの中には、記入したデータ等が入るようになる。
            doSomethingWithEntryValues(values)
          }}
     />
```
これで、UIで記入したデータ等を利用できるようになります。


## 参考リンク

https://developers.raycast.com/

https://developers.notion.com/

https://qiita.com/n0bisuke/items/b0cf0c416aa8e46bdb1b

https://itc.tokyo/linux/install-npm/