0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GROWIの表示ページでファイルをアップロードできるプラグインを作りました

Posted at

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

今回は、GROWIプラグインとして、GROWIの表示ページ(編集ページではなく)にてファイルのアップロードを実現するプラグインを作りました。編集ページに移動することなく、ファイルをアップロードできます。ファイルアップローダー的に使ってもらえます。

FireShot Capture 628 - Dropzone - GROWI - localhost.jpg

プラグインの動作

Remark Directiveとして、以下のように記述します。 [] ドロップゾーンで表示するテキストを入力します(デフォルトは Drag here to preview です)。

::dropzone[Drag here to preview]

そうすると、以下のように表示されます。

FireShot Capture 628 - Dropzone - GROWI - localhost.jpg

この枠の中にファイルをドラッグ&ドロップします。ファイルの場合は添付ファイルとして、画像の場合はインライン画像として表示されます。

FireShot Capture 629 - Dropzone - GROWI - localhost.jpg

プラグインを追加する

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

Admin

コードについて

コードはgoofmint/growi-plugin-dropzone: GROWI plugin for dropzoneにて公開しています。ライセンスはMIT Licenseになります。

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

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

export const remarkPlugin: Plugin = () => {
  return (tree: Node) => {
    visit(tree, 'leafDirective', (node: Node) => {
      const n = node as unknown as GrowiNode;
      if (n.name !== 'dropzone') return;
      const data = n.data || (n.data = {});
      // Render your component
      const { value } = n.children[0] || { value: '' };
      data.hName = 'a'; // Tag name
      data.hChildren = [{ type: 'text', value }]; // Children
      // Set properties
      data.hProperties = {
        href: 'https://example.com/rss',
        title: JSON.stringify({ ...n.attributes, ...{ dropzone: true } }), // Pass to attributes to the component
      };
    });
  };
};

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

GROWI 7.2から、React Hooksに対応しました。今回のプラグインではそれを使ってファイルのドロップを検知しています。

export const helloGROWI = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
  return ({ children, ...props }) => {
    try {
      const { dropzone } = JSON.parse(props.title);
      if (dropzone) {
        // 7.2から対応したReact Hooksを使っています
        const { react } = growiFacade;
        const { useEffect, useCallback, useState } = react;
        const [isDragOver, setIsDragOver] = useState(false);
        // ページID
        const pageId = window.location.pathname.split('/').pop();
        // 編集ページかどうかの判定
        const edit = window.location.hash.includes('edit');
        const onDrop = useCallback(async(event: React.DragEvent<HTMLDivElement>) => {
          // ドロップした後の処理
        }, []);
        return (
          <>
            <div id="drop-area"
              onDragEnter={() => setIsDragOver(true)}
              onDragLeave={() => setIsDragOver(false)}
              onDragOver={() => setIsDragOver(true)}
              onDrop={onDrop}
              className={isDragOver ? 'drag-over' : ''}
            >
              {children || 'Drag here to preview'}
            </div>
            <input type="file" id="file-input" multiple hidden />
            <div id="preview-container"></div>
          </>
        );
      }
    }
    catch (err) {
      // console.error(err);
    }
    // Return the original component if an error occurs
    return (
      <Tag {...props}>{children}</Tag>
    );
  };
};

スタイルシートでは、ドロップゾーンに対して枠の表示を行っています。また、ドラッグしている際の表示反転処理も追加しています。

#drop-area {
  width: 400px;
  height: 200px;
  margin: 20px auto;
  line-height: 200px;
  text-align: center;
  cursor: pointer;
  border: 2px dashed var(--bs-body-color);
  backgroundColor: var(--bs-body-bg);
}

#preview-container {
  text-align: center;
}

#drop-area.drag-over {
  color: var(--bs-body-bg);
  background-color: var(--bs-body-color);
  border: 2px dashed var(--bs-primary);
}

ドロップした後の処理

ファイルをドロップすると onDrop が走ります。ここでは、ファイルをGROWI JS SDKを使ってアップロードし、ページに追記しています。詳細はコメントを参照してください。

const onDrop = useCallback(async(event: React.DragEvent<HTMLDivElement>) => {
  event.preventDefault();
  // 編集モードなら何もしない
  if (edit) return;
  // ドロップしたファイルを取得
  const { files } = event.dataTransfer;
  // ファイルがない場合は何もしない
  if (files.length === 0) return;
  // 現在のページを取得
  const page = await growi.page({ pageId });
  // ファイルをアップロード
  const promises = [];
  for (const file of files) {
    promises.push(page.upload(file.name, file as any));
  }
  // レスポンスは添付ファイルオブジェクトの配列
  const attachments = await Promise.all(promises);
  // Markdown記法に変換
  const result = attachments.map((attachment) => {
    return attachment.fileFormat?.split('/')[0] === 'image'
      ? `![${attachment.fileName}](${attachment.filePathProxied})` // 画像の場合はインライン画像
      : `[${attachment.fileName}](${attachment.filePathProxied})`; // それ以外は添付ファイル
  });
  // 現在のページの内容を取得
  const contents = await page.contents();
  // 現在のページの内容に追記
  const newContents = `${contents}\n${result.join('\n')}`;
  // 保存
  await page.contents(newContents);
  await page.save();
  // ドラッグオーバーを解除
  setIsDragOver(false);
  // ページをリロード
  window.location.reload();
}, []);

最後にページをリロードしているのは、更新された内容をその場で表示に反映できないためです。

GROWIコミュニティについて

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

GROWI Slackへの参加はこちらから

まとめ

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

OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?