GROWIでSSSAPIのデータを表示するプラグインを作りました

[オープンソースのWikiであるGROWI](https://growi.org/ja/)にはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。

今回は、GROWIプラグインとしてGoogleスプレッドシートをAPI化するSSSAPIのデータを表示するプラグインを作成しました。業務システムでGoogleスプレッドシートを利用している企業は多いので、そのデータをGROWI内で表示できます。

## プラグインの動作

Remark Directiveとして、以下のように記述します。 `[]` 内には、SSSAPIのURLを記述します。

```markdown
::sssapi[https://api.sssapi.app/ID]
```

オプションとして、 `show` を使って一覧表示に利用するカラムを指定できます。カラム名は、SSSAPIのデータに合わせてください。

```markdown
::sssapi[https://api.sssapi.app/ID]{show=id,title,date}
```

![FireShot Capture 618 - SSSAPI - GROWI - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/d0931de1-07ad-4a42-b3c5-b3b1f0bd8559.jpeg)


各行をクリックすると、テーブルの下に詳細データを表示します。ここではすべてのフィールドが表示対象になります。

![FireShot Capture 619 - SSSAPI - GROWI - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/851e79f8-426e-45be-8313-bb65049a5182.jpeg)


## プラグインを追加する

利用する際には、GROWIの管理画面の `プラグイン` にて追加してください。URLは `https://github.com/goofmint/growi-plugin-sssapi` です。

![Admin](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f5b3e2d3-ccf3-d536-d59e-7301ea1b2f7e.jpeg)

## コードについて

コードは[goofmint/growi\-plugin\-sssapi: GROWI plugin integrated with SSSAPI](https://github.com/goofmint/growi-plugin-sssapi)にて公開しています。ライセンスはMIT Licenseになります。

最初に `::sssapi` というRemark Directiveを処理します。この記述があれば、 `a` タグに変換します。また、 `sssapi = true` を追加して、他の `a` タグとの区別をつけています。

`vist` メソッドで、RemarkのAST `leafDirective` を処理します。2つ目の引数を指定すると、そのディレクティブの場合のみ呼び出されるので便利です。 `leafDirective` は、行頭で `::` から始まるディレクティブです。

```tsx
export const remarkPlugin: Plugin = () => {
  return (tree: Node) => {
    // ::plugin[xxx]{hello=growi} -> leafDirective
    visit(tree, 'leafDirective', (node: Node) => {
      const n = node as unknown as GrowiNode;
      if (n.name !== 'sssapi') return;
      const data = n.data || (n.data = {});
      // Render your component
      const { url } = n.children[0] || { url: '' };
      data.hName = 'a'; // Tag name
      data.hChildren = []; // Children
      // Set properties
      data.hProperties = {
        href: url,
        title: JSON.stringify({ ...n.attributes, ...{ sssapi: true } }), // Pass to attributes to the component
      };
    });
  };
};
```

### 外部データを取得して表示

[GROWI 7.2から、React Hooksに対応しました](https://qiita.com/goofmint/items/865d24b88e3641375d51)。今回のプラグインではそれを使ってSSSAPIからデータを取得し、表示に反映しています。

```tsx
export const helloGROWI = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
  return ({ children, ...props }) => {
    try {
      const { react } = growiFacade;
      const { useEffect, useState } = react;
      // 外部データを取得して適用するためのstate
      const [rows, setRows] = useState<{[key: string]: string}[]>([]);
      // ヘッダー情報
      const [headers, setHeaders] = useState<string[]>([]);
      // 行をクリックした際に表示する詳細情報
      const [detail, setDetail] = useState<{[key: string]: string} | null>(null);
      // SSSAPIからデータを取得する
      const getData = async(url: string) => {
        const response = await fetch(url);
        const json = await response.json();
        setRows(json);
        setHeaders(Object.keys(json[0] || {}));
      };
      const { sssapi, show } = JSON.parse(props.title);
      // sssapiがtrueの場合のみ処理を行う
      if (sssapi) {
        const { href } = props;
        // データの取得
        useEffect(() => {
          getData(href);
        }, [href]);
        // 表示するフィールド（オプション）
        const displayFields = show ? show.split(',') : [];
        return (
          { /* 表示処理 */}
        );
      }
    }
    catch (err) {
      // console.error(err);
    }
    // Return the original component if an error occurs
    return (
      <Tag {...props}>{children}</Tag>
    );
  };
};
```

### 表示処理

表示についてはReactで表示し、行のクリックで `detail` にデータを入れているだけです。

```tsx
return (
  <>
    <table className='table'>
      <thead>
        <tr>
          {headers.map(header => (
            (displayFields.length > 0 && !displayFields.includes(header)) ? null
              : <th key={header}>{header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, rowIndex) => (
          <tr
            key={rowIndex}
            onClick={() => setDetail(row)}
          >
            {headers.map(header => (
              (displayFields.length > 0 && !displayFields.includes(header)) ? null
                : <td key={header}>{row[header]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
    {detail && (
      <div>
        <ul>
          {Object.entries(detail).map(([key, value]) => (
            <li key={key}>{key}:<br />
              {value}
            </li>
          ))}
        </ul>
      </div>
    )}
  </>
);
```

## GROWIコミュニティについて

プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください！

[GROWI Slackへの参加はこちらから](https://communityinviter.com/apps/wsgrowi/invite)

## まとめ

GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。

[OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ](https://growi.org/ja/)